Skip to content

Commit 7c25fad

Browse files
committed
fix(daemon): tighten section-heading match, fix session:notes subcommand, drop unused import
- Writer splitBlocks uses exact line equality instead of trimEnd() to avoid re-detecting section headings inside list items (e.g. bullets containing 'Key steps:' or 'Outcome:' as quoted text). - Move session:notes from top-level program command to session subcommand (session notes <key>) and update tests accordingly. - Remove unused SESSION_NOTES_FILENAME import from consolidator.
1 parent d6ef43b commit 7c25fad

5 files changed

Lines changed: 50 additions & 10 deletions

File tree

platform/daemon/src/session-notes-consolidator.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import { logger } from "./logger";
2626
import type { LlmProvider } from "./pipeline/provider";
2727
import { redactSecrets } from "./session-checkpoints";
2828
import {
29-
SESSION_NOTES_FILENAME,
3029
appendTaskSection,
3130
findLowestMissingIndex,
3231
isNotesFileFresh,

platform/daemon/src/session-notes-writer.test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,4 +256,40 @@ Twelfth task.
256256
expect(existsSync(dir)).toBe(true);
257257
expect(existsSync(join(dir, SESSION_NOTES_FILENAME))).toBe(true);
258258
});
259+
260+
test("does not misparse section labels that appear inside list items", () => {
261+
// Regression: the previous splitBlocks used line.trimEnd() to match
262+
// section headings, so a bullet like `- "Key steps:"` inside a
263+
// Failures block would re-detect as a new section header and shuffle
264+
// every line below it into the wrong bucket.
265+
appendTaskSection({
266+
sessionKey: "labels-in-bullets",
267+
agentId: "default",
268+
harness: "opencode",
269+
cwd: "/tmp",
270+
task: {
271+
taskIndex: 1,
272+
outcome: "Validated the parser.",
273+
failures: [
274+
'The user said "Key steps:" should be lowercase.',
275+
'A bullet that ends with "Outcome:" is just text.',
276+
],
277+
keySteps: ["Tightened the heading match."],
278+
},
279+
agentsDir: tmpRoot,
280+
});
281+
const read = readSessionNotes("labels-in-bullets", tmpRoot);
282+
expect(read.ok).toBe(true);
283+
if (!read.ok) return;
284+
expect(read.file.tasks).toHaveLength(1);
285+
const task = read.file.tasks[0]!;
286+
// Both quoted-label bullets are preserved as failures, not mis-bucketed.
287+
expect(task.failures).toEqual([
288+
'The user said "Key steps:" should be lowercase.',
289+
'A bullet that ends with "Outcome:" is just text.',
290+
]);
291+
expect(task.keySteps).toEqual(["Tightened the heading match."]);
292+
// Outcome was not contaminated by the "Outcome:" substring.
293+
expect(task.outcome).toBe("Validated the parser.");
294+
});
259295
});

platform/daemon/src/session-notes-writer.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,9 +318,15 @@ function splitBlocks(body: string): Record<string, string> {
318318
const lines = body.split("\n");
319319
const result: Record<string, string[]> = {};
320320
let current: string | null = null;
321+
// Match section headings only when the line is exactly one of the six
322+
// canonical labels (no leading whitespace, no trailing content). A loose
323+
// trimEnd-based match would re-detect headings inside list items, e.g.
324+
// `- "Key steps:"` in a Failures block, and shuffle every line below
325+
// it into the wrong bucket. The writer's renderer always emits at
326+
// column 0 with no trailing whitespace, so exact equality is safe.
321327
for (const line of lines) {
322-
if ((REQUIRED_TASK_SECTIONS as readonly string[]).includes(line.trimEnd())) {
323-
current = line.trimEnd();
328+
if ((REQUIRED_TASK_SECTIONS as readonly string[]).includes(line)) {
329+
current = line;
324330
result[current] = [];
325331
} else if (current) {
326332
result[current].push(line);

surfaces/cli/src/commands/session.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe("registerSessionCommands search", () => {
9595
});
9696
});
9797

98-
describe("registerSessionCommands session:notes", () => {
98+
describe("registerSessionCommands session notes", () => {
9999
test("calls the notes route and prints json", async () => {
100100
const lines: string[] = [];
101101
console.log = (line?: unknown) => {
@@ -135,7 +135,7 @@ describe("registerSessionCommands session:notes", () => {
135135
},
136136
});
137137

138-
await program.parseAsync(["node", "test", "session:notes", "test", "--json"]);
138+
await program.parseAsync(["node", "test", "session", "notes", "test", "--json"]);
139139

140140
expect(capturedPath).toBe("/api/sessions/test/notes");
141141
expect(lines).toHaveLength(1);
@@ -182,7 +182,7 @@ describe("registerSessionCommands session:notes", () => {
182182
},
183183
});
184184

185-
await program.parseAsync(["node", "test", "session:notes", "test", "--task", "2", "--json"]);
185+
await program.parseAsync(["node", "test", "session", "notes", "test", "--task", "2", "--json"]);
186186

187187
expect(capturedPath).toBe("/api/sessions/test/notes?task=2");
188188
});
@@ -208,7 +208,7 @@ describe("registerSessionCommands session:notes", () => {
208208
});
209209

210210
try {
211-
await program.parseAsync(["node", "test", "session:notes", "missing"]);
211+
await program.parseAsync(["node", "test", "session", "notes", "missing"]);
212212
} catch {
213213
/* swallow the synthetic exit */
214214
}

surfaces/cli/src/commands/session.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,9 @@ export function registerSessionCommands(program: Command, deps: SessionDeps): vo
136136
console.log(chalk.green(` Session ${sessionKey.slice(0, 12)} bypass removed — hooks re-enabled`));
137137
});
138138

139-
program
140-
.command("session:notes")
139+
session
140+
.command("notes <session-key>")
141141
.description("Read the structured per-session notes file (frontmatter + numbered Task N sections)")
142-
.argument("<session-key>", "Session key (thread_id) to read")
143142
.option("--task <n>", "Scope to a single task index", (v) => Number.parseInt(v, 10))
144143
.option("--json", "Output as JSON", false)
145144
.action(async (sessionKey: string, options: { task?: number; json?: boolean }) => {

0 commit comments

Comments
 (0)