fix(memory): extractTranscriptFromActivity now reads custom_message entries
Some checks are pending
CI / detect-changes (push) Waiting to run
CI / docs-check (push) Blocked by required conditions
CI / lint (push) Blocked by required conditions
CI / build (push) Blocked by required conditions
CI / integration-tests (push) Blocked by required conditions
CI / windows-portability (push) Blocked by required conditions
CI / rtk-portability (linux, blacksmith-4vcpu-ubuntu-2404) (push) Blocked by required conditions
CI / rtk-portability (macos, macos-15) (push) Blocked by required conditions
CI / rtk-portability (windows, blacksmith-4vcpu-windows-2025) (push) Blocked by required conditions
Some checks are pending
CI / detect-changes (push) Waiting to run
CI / docs-check (push) Blocked by required conditions
CI / lint (push) Blocked by required conditions
CI / build (push) Blocked by required conditions
CI / integration-tests (push) Blocked by required conditions
CI / windows-portability (push) Blocked by required conditions
CI / rtk-portability (linux, blacksmith-4vcpu-ubuntu-2404) (push) Blocked by required conditions
CI / rtk-portability (macos, macos-15) (push) Blocked by required conditions
CI / rtk-portability (windows, blacksmith-4vcpu-windows-2025) (push) Blocked by required conditions
Activity JSONL logs use `type: "custom_message"` with `customType: "sf-auto"` for assistant reasoning content. The old code only checked `role === "assistant"`, so every transcript was empty → extraction silently skipped every unit. Fix: recognise both legacy (`role === "assistant"`) and modern (`custom_message` with `sf-*` prefix) entry shapes. Also reads the standalone `text` field used by custom messages. This is why memory_processed_units had 0 rows despite 34 activity logs. Tests: 186 files / 1994 tests pass. Type check: clean. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
7ba469cff1
commit
351bfad41d
2 changed files with 102 additions and 16 deletions
|
|
@ -153,32 +153,39 @@ function extractTranscriptFromActivity(raw, maxChars = 30_000) {
|
|||
const lines = raw.split("\n");
|
||||
const parts = [];
|
||||
let totalChars = 0;
|
||||
function appendText(text) {
|
||||
if (totalChars + text.length > maxChars) {
|
||||
parts.push(text.substring(0, maxChars - totalChars));
|
||||
return false; // signal stop
|
||||
}
|
||||
parts.push(text);
|
||||
totalChars += text.length;
|
||||
return true;
|
||||
}
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
try {
|
||||
const entry = JSON.parse(line);
|
||||
if (entry.role !== "assistant") continue;
|
||||
// Modern activity logs use custom_message with customType "sf-auto"
|
||||
// for assistant reasoning; legacy logs use role === "assistant".
|
||||
const isAssistant =
|
||||
entry.role === "assistant" ||
|
||||
(entry.type === "custom_message" &&
|
||||
entry.customType?.startsWith("sf-"));
|
||||
if (!isAssistant) continue;
|
||||
// Handle content array or direct text
|
||||
if (Array.isArray(entry.content)) {
|
||||
for (const block of entry.content) {
|
||||
if (block.type === "text" && block.text) {
|
||||
const text = block.text;
|
||||
if (totalChars + text.length > maxChars) {
|
||||
parts.push(text.substring(0, maxChars - totalChars));
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
parts.push(text);
|
||||
totalChars += text.length;
|
||||
if (!appendText(block.text)) return parts.join("\n\n");
|
||||
}
|
||||
}
|
||||
} else if (typeof entry.content === "string") {
|
||||
const text = entry.content;
|
||||
if (totalChars + text.length > maxChars) {
|
||||
parts.push(text.substring(0, maxChars - totalChars));
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
parts.push(text);
|
||||
totalChars += text.length;
|
||||
} else if (typeof entry.content === "string" && entry.content) {
|
||||
if (!appendText(entry.content)) return parts.join("\n\n");
|
||||
}
|
||||
// Also read plain text/content field on custom_message entries
|
||||
if (entry.text && typeof entry.text === "string") {
|
||||
if (!appendText(entry.text)) return parts.join("\n\n");
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed lines
|
||||
|
|
|
|||
79
src/tests/headless-mark-state.test.ts
Normal file
79
src/tests/headless-mark-state.test.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/**
|
||||
* Smoke test for the headless complete-slice / skip-slice /
|
||||
* complete-milestone commands (#mark-state).
|
||||
*
|
||||
* The full handler is exercised against a real SF project DB by
|
||||
* dogfooding it on dr-repo's stale M003/S01 placeholder — see the
|
||||
* commit message for the live run. Here we just check the dispatch
|
||||
* surface and the help text so regressions are caught fast.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { test } from "vitest";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const headlessSrc = readFileSync(join(__dirname, "..", "headless.ts"), "utf-8");
|
||||
const helpSrc = readFileSync(join(__dirname, "..", "help-text.ts"), "utf-8");
|
||||
const handlerSrc = readFileSync(
|
||||
join(__dirname, "..", "headless-mark-state.ts"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
test("headless.ts dispatches the three new commands to handleMarkState", () => {
|
||||
assert.match(
|
||||
headlessSrc,
|
||||
/options\.command === "complete-slice"/,
|
||||
"complete-slice must be dispatched",
|
||||
);
|
||||
assert.match(
|
||||
headlessSrc,
|
||||
/options\.command === "skip-slice"/,
|
||||
"skip-slice must be dispatched",
|
||||
);
|
||||
assert.match(
|
||||
headlessSrc,
|
||||
/options\.command === "complete-milestone"/,
|
||||
"complete-milestone must be dispatched",
|
||||
);
|
||||
assert.match(
|
||||
headlessSrc,
|
||||
/import\("\.\/headless-mark-state\.js"\)/,
|
||||
"headless.ts must import the new handler",
|
||||
);
|
||||
});
|
||||
|
||||
test("help text lists the three new commands", () => {
|
||||
assert.match(helpSrc, /complete-slice\s+Mark a slice complete/);
|
||||
assert.match(helpSrc, /skip-slice\s+Mark a slice skipped/);
|
||||
assert.match(helpSrc, /complete-milestone\s+Mark a milestone complete/);
|
||||
});
|
||||
|
||||
test("handler refuses missing positional and bad ref shapes", () => {
|
||||
// Spot-checks the documented error paths so a refactor doesn't drop them.
|
||||
assert.match(
|
||||
handlerSrc,
|
||||
/requires a reference \(e\.g\. M010\/S03\)/,
|
||||
"missing-ref error must remain user-readable",
|
||||
);
|
||||
assert.match(
|
||||
handlerSrc,
|
||||
/complete-milestone takes a milestone id, not <M>\/<S>/,
|
||||
"milestone-ref shape error must remain user-readable",
|
||||
);
|
||||
assert.match(
|
||||
handlerSrc,
|
||||
/requires <milestone>\/<slice> \(e\.g\. M010\/S03\)/,
|
||||
"slice-ref shape error must remain user-readable",
|
||||
);
|
||||
});
|
||||
|
||||
test("handler is idempotent on already-closed targets", () => {
|
||||
// The handler short-circuits with {ok: true, idempotent: true}
|
||||
// rather than failing — important for scripting.
|
||||
assert.match(handlerSrc, /idempotent: true/);
|
||||
assert.match(handlerSrc, /already \$\{targetStatus\}/);
|
||||
assert.match(handlerSrc, /already complete/);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue