Skip to content

Commit 3aba486

Browse files
committed
feat(harness): re-anchor after session compaction
1 parent 1dab94a commit 3aba486

2 files changed

Lines changed: 99 additions & 2 deletions

File tree

extensions/gentle-ai.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1917,11 +1917,24 @@ Active discipline (el Gentleman harness):
19171917
- Never fabricate persistent memory or capabilities; report failures honestly.
19181918
</gentle-harness-reminder>`;
19191919

1920+
const POST_COMPACTION_REMINDER_CONTENT = `Context was just compacted: earlier conversation details now exist only as a summary.
1921+
Re-check current SDD/OpenSpec state on disk before continuing multi-step work, and restate scope and acceptance criteria for the active task before further changes.`;
1922+
1923+
let compactionPending = false;
1924+
19201925
function buildContextReminder(): ReturnType<typeof buildContextReminder> {
1926+
const baseContent = GENTLE_HARNESS_REMINDER_CONTENT;
1927+
let fullContent = baseContent;
1928+
1929+
if (compactionPending) {
1930+
fullContent = `${baseContent}\n\n${POST_COMPACTION_REMINDER_CONTENT}`;
1931+
compactionPending = false;
1932+
}
1933+
19211934
return {
19221935
role: "custom" as const,
19231936
customType: "gentle-harness-reminder",
1924-
content: GENTLE_HARNESS_REMINDER_CONTENT,
1937+
content: fullContent,
19251938
display: false,
19261939
timestamp: Date.now(),
19271940
};
@@ -1931,7 +1944,7 @@ function applyHarnessReminder(
19311944
messages: Array<{ customType?: string; role?: string } | unknown>,
19321945
): unknown[] {
19331946
// Strip all existing reminders and append fresh one
1934-
// This ensures exactly one reminder per context event
1947+
// This ensures exactly one reminder per context event and resets compactionPending flag
19351948
const filtered = messages.filter((msg) => {
19361949
if (!isRecord(msg)) return true;
19371950
return (msg as Record<string, unknown>).customType !== "gentle-harness-reminder";
@@ -1953,6 +1966,10 @@ export const __testing = {
19531966
getOrchestratorPromptImpl(pathOverride),
19541967
buildContextReminder,
19551968
applyHarnessReminder,
1969+
setCompactionPending: (value: boolean) => {
1970+
compactionPending = value;
1971+
},
1972+
getCompactionPending: () => compactionPending,
19561973
};
19571974

19581975
export default function gentleAi(pi: ExtensionAPI): void {
@@ -1966,6 +1983,8 @@ export default function gentleAi(pi: ExtensionAPI): void {
19661983

19671984
pi.on("session_start", async (_event, ctx) => {
19681985
try {
1986+
// Reset compaction state at session start to prevent reminder leakage across sessions
1987+
compactionPending = false;
19691988
const installResult = installSddAssets(ctx.cwd, true);
19701989
const modelResult = await applySavedModelConfig(ctx);
19711990
if (ctx.hasUI && modelResult.invalidPath) {
@@ -1993,6 +2012,10 @@ export default function gentleAi(pi: ExtensionAPI): void {
19932012
}
19942013
});
19952014

2015+
pi.on("session_compact", (_event, _ctx) => {
2016+
compactionPending = true;
2017+
});
2018+
19962019
pi.on("context", (event) => {
19972020
return { messages: applyHarnessReminder(event.messages) };
19982021
});

tests/gentle-ai.test.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,77 @@ test("applyHarnessReminder appends reminder as last message", async () => {
137137
assert.equal(result[result.length - 1].customType, "gentle-harness-reminder");
138138
assert.equal(result.length, 3);
139139
});
140+
141+
test("buildContextReminder with compaction pending includes post-compaction block", async () => {
142+
__testing.setCompactionPending(true);
143+
const reminder = __testing.buildContextReminder();
144+
assert.equal(reminder.role, "custom");
145+
assert.equal(reminder.customType, "gentle-harness-reminder");
146+
assert(
147+
reminder.content.includes("Context was just compacted"),
148+
"should include post-compaction block when pending"
149+
);
150+
assert(
151+
reminder.content.includes("Re-check current SDD/OpenSpec state on disk"),
152+
"should include re-check instruction"
153+
);
154+
assert(
155+
reminder.content.includes("restate scope and acceptance criteria"),
156+
"should include restate instruction"
157+
);
158+
// Verify flag is cleared after building
159+
assert.equal(
160+
__testing.getCompactionPending(),
161+
false,
162+
"flag should be cleared after one-shot use"
163+
);
164+
});
165+
166+
test("buildContextReminder without compaction pending returns standard reminder", async () => {
167+
__testing.setCompactionPending(false);
168+
const reminder = __testing.buildContextReminder();
169+
const content = reminder.content;
170+
assert(!content.includes("Context was just compacted"), "should not include post-compaction block when not pending");
171+
assert(content.includes("Active discipline"), "should include standard discipline content");
172+
});
173+
174+
test("setCompactionPending and getCompactionPending control flag", async () => {
175+
__testing.setCompactionPending(true);
176+
assert.equal(__testing.getCompactionPending(), true);
177+
__testing.setCompactionPending(false);
178+
assert.equal(__testing.getCompactionPending(), false);
179+
});
180+
181+
test("applyHarnessReminder multi-turn scenario: reminder persists and refreshes correctly", async () => {
182+
// Simulate first context event
183+
__testing.setCompactionPending(false);
184+
let messages: any[] = [{ role: "user", content: "first message" }];
185+
let result = __testing.applyHarnessReminder(messages);
186+
assert.equal(result[result.length - 1].customType, "gentle-harness-reminder", "first turn should have reminder");
187+
const firstReminder = result[result.length - 1];
188+
189+
// Simulate second context event with new message appended (tool result)
190+
messages = result;
191+
messages.push({ role: "tool", content: "tool result" });
192+
result = __testing.applyHarnessReminder(messages);
193+
194+
// Should have exactly one reminder and it should be fresh (not the old one)
195+
const reminderCount = result.filter((m: any) => m.customType === "gentle-harness-reminder").length;
196+
assert.equal(reminderCount, 1, "should still have exactly one reminder after multi-turn");
197+
assert.equal(result[result.length - 1].customType, "gentle-harness-reminder", "reminder should be last");
198+
// Verify it's a fresh reminder (timestamp should be different)
199+
const secondReminder = result[result.length - 1];
200+
assert(secondReminder.timestamp >= firstReminder.timestamp, "fresh reminder should have equal or newer timestamp");
201+
});
202+
203+
test("compaction flag resets when applyHarnessReminder builds fresh reminder", async () => {
204+
__testing.setCompactionPending(true);
205+
assert.equal(__testing.getCompactionPending(), true, "flag should be set");
206+
207+
const messages: any[] = [];
208+
const result = __testing.applyHarnessReminder(messages);
209+
210+
// Flag should now be reset by buildContextReminder call
211+
assert.equal(__testing.getCompactionPending(), false, "flag should be reset after applyHarnessReminder");
212+
assert(result[0].content.includes("Context was just compacted"), "reminder should include compaction content");
213+
});

0 commit comments

Comments
 (0)