@vellumai/assistant 0.12.4 → 0.12.5-dev.202609251017.e467a6c
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 +9 -1
- package/docs/architecture/scheduling.md +13 -5
- package/docs/desktop-browser-cli.md +3 -2
- package/docs/guardian-request-flow.md +1 -1
- package/node_modules/@vellumai/avatar-manifest/src/__tests__/notification-avatar.test.ts +48 -4
- package/node_modules/@vellumai/avatar-manifest/src/notification-avatar.ts +12 -8
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/__tests__/guardian-requests.test.ts +26 -0
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +77 -3
- package/openapi.yaml +152 -3
- package/package.json +1 -1
- package/src/__tests__/agent-loop.test.ts +76 -0
- package/src/__tests__/anthropic-error-formatting.test.ts +9 -0
- package/src/__tests__/background-shell-bash.test.ts +34 -0
- package/src/__tests__/background-shell-host-bash.test.ts +98 -1
- package/src/__tests__/background-tool-registry.test.ts +39 -0
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +4 -20
- package/src/__tests__/conversation-agent-loop.test.ts +529 -1
- package/src/__tests__/conversation-error.test.ts +18 -0
- package/src/__tests__/conversation-pairing.test.ts +69 -0
- package/src/__tests__/conversation-queue.test.ts +257 -4
- package/src/__tests__/conversation-runtime-assembly.test.ts +27 -0
- package/src/__tests__/emit-signal-routing-intent.test.ts +62 -0
- package/src/__tests__/evict-conversations-for-reload.test.ts +21 -0
- package/src/__tests__/gemini-provider.test.ts +9 -0
- package/src/__tests__/host-cu-proxy.test.ts +79 -0
- package/src/__tests__/host-cu-routes-targeted.test.ts +14 -5
- package/src/__tests__/introduction-card-resolver.test.ts +53 -8
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/non-member-access-request.test.ts +48 -0
- package/src/__tests__/notification-events-source-context-probe.test.ts +48 -1
- package/src/__tests__/notification-platform-adapter.test.ts +97 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +103 -3
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -0
- package/src/__tests__/scheduler-result-notification.test.ts +521 -7
- package/src/__tests__/scheduler-wake.test.ts +1 -0
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/subagent-cron-run-attribution.test.ts +41 -2
- package/src/__tests__/subagent-manager-notify.test.ts +48 -0
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +6 -6
- package/src/__tests__/usage-attribution.test.ts +31 -0
- package/src/__tests__/wake-schedule-escalation.test.ts +1 -0
- package/src/agent/loop.ts +23 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/generation-handoff.ts +4 -0
- package/src/api/events/message-complete.ts +4 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +11 -0
- package/src/api/requests/guardian-actions.test.ts +45 -0
- package/src/api/requests/guardian-actions.ts +20 -0
- package/src/api/responses/conversation-message.ts +4 -0
- package/src/api/responses/home.ts +27 -0
- package/src/approvals/guardian-decision-primitive.ts +6 -4
- package/src/approvals/guardian-request-resolvers.ts +35 -30
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/call-controller.test.ts +18 -0
- package/src/calls/__tests__/voice-control-protocol.test.ts +6 -1
- package/src/calls/__tests__/voice-leg-coordinator.test.ts +70 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +116 -4
- package/src/calls/__tests__/voice-triage-escalate.test.ts +77 -11
- package/src/calls/call-controller.ts +3 -1
- package/src/calls/voice-control-protocol.ts +12 -4
- package/src/calls/voice-escalation-judge.ts +2 -2
- package/src/calls/voice-leg-coordinator.ts +23 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +63 -20
- package/src/calls/voice-triage-escalate.ts +82 -27
- package/src/cli/commands/__tests__/conversations-clear.test.ts +156 -0
- package/src/cli/commands/conversations.help.ts +7 -2
- package/src/cli/commands/conversations.ts +36 -30
- package/src/cli/commands/oauth/index.help.ts +8 -2
- package/src/cli/commands/oauth/request.test.ts +32 -0
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +60 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/bundled-marketplace.json +35 -0
- package/src/cli/lib/bundled-plugin-packages.json +21 -0
- package/src/cli/lib/plugin-catalog-cache.ts +17 -2
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/config/__tests__/default-profile-catalog.test.ts +28 -0
- package/src/config/__tests__/platform-identity.test.ts +59 -0
- package/src/config/__tests__/profile-order.test.ts +19 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/computer-use/SKILL.md +20 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +11 -11
- package/src/config/bundled-skills/screen-annotation/SKILL.md +41 -29
- package/src/config/bundled-skills/screen-annotation/TOOLS.json +5 -5
- package/src/config/call-site-defaults.ts +6 -0
- package/src/config/default-profile-catalog.ts +30 -1
- package/src/config/default-profile-names.ts +36 -2
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/platform-identity.ts +22 -5
- package/src/config/profile-order.ts +7 -1
- package/src/config/schemas/call-site-catalog.ts +7 -0
- package/src/config/schemas/llm.ts +21 -5
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/abort-null-controller.test.ts +135 -1
- package/src/daemon/__tests__/auto-profile-router.test.ts +380 -0
- package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +16 -0
- package/src/daemon/activity-trail.test.ts +206 -0
- package/src/daemon/activity-trail.ts +190 -0
- package/src/daemon/auto-profile-router.ts +431 -0
- package/src/daemon/computer-use-observation.test.ts +81 -0
- package/src/daemon/computer-use-observation.ts +23 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
- package/src/daemon/conversation-agent-loop.ts +157 -32
- package/src/daemon/conversation-error.ts +19 -0
- package/src/daemon/conversation-lifecycle.ts +70 -7
- package/src/daemon/conversation-media-retry.ts +1 -2
- package/src/daemon/conversation-messaging.ts +31 -2
- package/src/daemon/conversation-process.ts +107 -16
- package/src/daemon/conversation-queue-manager.ts +7 -0
- package/src/daemon/conversation-runtime-assembly.ts +1 -0
- package/src/daemon/conversation-turn-finalize.ts +14 -0
- package/src/daemon/conversation.ts +25 -6
- package/src/daemon/event-loop-watchdog.test.ts +201 -2
- package/src/daemon/event-loop-watchdog.ts +182 -6
- package/src/daemon/host-cu-proxy.ts +34 -3
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/daemon/wake-conversation-ops.ts +4 -2
- package/src/desktop/desktop-computer-use.test.ts +14 -4
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- package/src/live-voice/__tests__/live-voice-flux-turn-end.test.ts +83 -0
- package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +48 -1
- package/src/live-voice/__tests__/live-voice-vad.test.ts +34 -2
- package/src/live-voice/__tests__/protocol.test.ts +22 -0
- package/src/live-voice/continuation-judge.ts +1 -1
- package/src/live-voice/live-voice-session.ts +25 -19
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/discord/withdraw.ts +4 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/messaging/providers/slack/withdraw.ts +5 -5
- package/src/messaging/providers/telegram-bot/withdraw.ts +4 -5
- package/src/monitoring/__tests__/proc-fixtures.ts +28 -0
- package/src/monitoring/__tests__/proc-wait-state.test.ts +232 -0
- package/src/monitoring/__tests__/process-usage.test.ts +243 -0
- package/src/monitoring/proc-wait-state.ts +366 -0
- package/src/monitoring/process-memory.ts +4 -3
- package/src/monitoring/process-usage.ts +215 -0
- package/src/monitoring/resource-sample-types.ts +8 -0
- package/src/monitoring/resource-sampler.ts +17 -2
- package/src/monitoring/stall-capture.ts +12 -0
- package/src/notifications/README.md +88 -9
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +390 -119
- package/src/notifications/__tests__/background-result-producer.test.ts +1272 -0
- package/src/notifications/__tests__/broadcaster.test.ts +292 -1
- package/src/notifications/__tests__/completion-policy.test.ts +243 -0
- package/src/notifications/__tests__/decision-engine.test.ts +51 -4
- package/src/notifications/__tests__/has-pending-background-work.test.ts +281 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +161 -3
- package/src/notifications/__tests__/resolve-visible-in-source.test.ts +110 -1
- package/src/notifications/__tests__/schedule-result-producer.test.ts +11 -4
- package/src/notifications/adapters/macos.ts +24 -12
- package/src/notifications/adapters/platform.ts +16 -5
- package/src/notifications/assistant-reply-producer.ts +96 -71
- package/src/notifications/background-result-producer.ts +369 -0
- package/src/notifications/broadcaster.ts +28 -8
- package/src/notifications/completion-policy.ts +115 -0
- package/src/notifications/completion-work.ts +46 -0
- package/src/notifications/conversation-pairing.ts +32 -13
- package/src/notifications/decision-engine.ts +18 -24
- package/src/notifications/emit-signal.ts +16 -1
- package/src/notifications/events-store.ts +4 -0
- package/src/notifications/has-pending-background-work.ts +91 -0
- package/src/notifications/home-feed-mirror.ts +44 -0
- package/src/notifications/home-feed-side-effect.ts +54 -28
- package/src/notifications/resolve-visible-in-source.ts +56 -2
- package/src/notifications/result-output.ts +154 -0
- package/src/notifications/schedule-result-producer.ts +6 -187
- package/src/notifications/types.ts +2 -0
- package/src/oauth/AGENTS.md +9 -1
- package/src/oauth/byo-connection.test.ts +28 -0
- package/src/persistence/__tests__/recent-conversation-messages.test.ts +41 -0
- package/src/persistence/conversation-crud.ts +26 -0
- package/src/persistence/conversation-types.ts +15 -37
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +85 -14
- package/src/persistence/jobs-store.ts +229 -0
- package/src/persistence/migrations/383-normalize-opencode-host-connections.ts +44 -0
- package/src/persistence/migrations/__tests__/383-normalize-opencode-host-connections.test.ts +102 -0
- package/src/persistence/steps.ts +12 -0
- package/src/platform/sync-avatar.test.ts +29 -0
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-boundary-guard.test.ts +45 -0
- package/src/plugins/defaults/memory/__tests__/skill-update-receipt-job.test.ts +580 -0
- package/src/plugins/defaults/memory/__tests__/turn-retrieval.test.ts +21 -0
- package/src/plugins/defaults/memory/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +7 -3
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/turn-retrieval.ts +8 -0
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +24 -0
- package/src/plugins/defaults/memory/v3/injector.ts +4 -4
- package/src/plugins/types.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +4 -0
- package/src/providers/gemini/client.ts +7 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/inference/profile-probe.ts +1 -0
- package/src/{calls/__tests__/typesafe-noul.test.ts → providers/jev/ask.test.ts} +4 -4
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +67 -34
- package/src/providers/openai/__tests__/api-error-normalization.test.ts +12 -0
- package/src/providers/openai/api-error-normalization.ts +4 -0
- package/src/providers/retry.ts +2 -0
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/types.ts +7 -0
- package/src/providers/usage-tracking.ts +5 -3
- package/src/runtime/__tests__/agent-wake.test.ts +308 -0
- package/src/runtime/access-request-helper.ts +16 -9
- package/src/runtime/agent-wake-queue.ts +52 -0
- package/src/runtime/agent-wake.ts +111 -18
- package/src/runtime/channel-approval-types.ts +6 -38
- package/src/runtime/channel-approvals.ts +4 -5
- package/src/runtime/http-server.ts +4 -1
- package/src/runtime/introduction-policy.test.ts +17 -1
- package/src/runtime/introduction-policy.ts +25 -7
- package/src/runtime/migrations/vbundle-builder.ts +9 -0
- package/src/runtime/routes/__tests__/inference-profiles-routes.test.ts +21 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/oauth-proxy-routes.test.ts +61 -3
- package/src/runtime/routes/auto-profile-routes.ts +83 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -3
- package/src/runtime/routes/conversation-routes.ts +9 -0
- package/src/runtime/routes/guardian-action-routes.ts +8 -23
- package/src/runtime/routes/host-cu-routes.ts +3 -1
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-profiles-routes.ts +5 -2
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/migration-routes.ts +8 -2
- package/src/runtime/routes/oauth-proxy-passthrough.test.ts +32 -0
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/user-route-dispatcher.ts +1 -2
- package/src/runtime/web-presence.ts +3 -4
- package/src/schedule/schedule-store.ts +10 -0
- package/src/schedule/scheduler.ts +92 -24
- package/src/subagent/manager.ts +32 -12
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/telemetry/telemetry-wire-source.json +1 -1
- package/src/telemetry/telemetry-wire.generated.ts +1 -0
- package/src/tools/background-tool-registry.ts +30 -3
- package/src/tools/browser/cdp-client/local-cdp-client.ts +1 -2
- package/src/tools/host-terminal/host-shell.ts +7 -0
- package/src/tools/network/__tests__/web-fetch-firecrawl.test.ts +28 -1
- package/src/tools/network/__tests__/web-fetch-metadata.test.ts +34 -0
- package/src/tools/network/__tests__/web-fetch-tinyfish.test.ts +35 -1
- package/src/tools/network/__tests__/web-search.test.ts +96 -0
- package/src/tools/network/web-fetch.ts +30 -9
- package/src/tools/network/web-search.ts +235 -4
- package/src/tools/skills/load.ts +13 -17
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/terminal/shell-launch.test.ts +27 -1
- package/src/tools/terminal/shell.ts +4 -0
- package/src/tools/workspace-tools/loader.ts +1 -2
- package/src/usage/attribution.ts +18 -1
- package/src/util/abort-reasons.ts +2 -0
- package/src/util/errors.ts +1 -0
- package/src/util/oauth-request-body.test.ts +5 -1
- package/src/util/provider-error-patterns.ts +11 -0
- package/src/util/single-flight.test.ts +6 -0
- package/src/util/single-flight.ts +2 -0
- package/src/util/worker-pipe.test.ts +56 -10
- package/src/util/worker-pipe.ts +9 -4
- package/src/workspace/git-service.ts +6 -1
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +2 -0
|
@@ -54,6 +54,62 @@ function brokenPipeProc(pid = 4242) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/**
|
|
58
|
+
* A stand-in for a Bun subprocess whose stdin write outruns the pipe buffer:
|
|
59
|
+
* `write` and `flush` hand back one pending promise, and killing the worker
|
|
60
|
+
* rejects it with EPIPE, the way Bun settles a pending write once the child
|
|
61
|
+
* is reaped.
|
|
62
|
+
*/
|
|
63
|
+
function pendingWriteProc(pid = 4244) {
|
|
64
|
+
const pending = Promise.withResolvers<number>();
|
|
65
|
+
return {
|
|
66
|
+
pid,
|
|
67
|
+
killed: false,
|
|
68
|
+
exited: Promise.resolve(0),
|
|
69
|
+
kill() {
|
|
70
|
+
this.killed = true;
|
|
71
|
+
pending.reject(
|
|
72
|
+
Object.assign(new Error("EPIPE: broken pipe, write"), {
|
|
73
|
+
code: "EPIPE",
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
},
|
|
77
|
+
stdin: {
|
|
78
|
+
write: () => pending.promise,
|
|
79
|
+
flush: () => pending.promise,
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* A subprocess whose stdout the test closes, to drive the reader loop that
|
|
86
|
+
* settles requests the pipe never reported a failure for.
|
|
87
|
+
*/
|
|
88
|
+
function stdoutProc(pid = 4245) {
|
|
89
|
+
let closeStdout: () => void = () => {};
|
|
90
|
+
const stdout = new ReadableStream<Uint8Array>({
|
|
91
|
+
start(controller) {
|
|
92
|
+
closeStdout = () => controller.close();
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
let resolveExit: (code: number) => void = () => {};
|
|
96
|
+
const exited = new Promise<number>((r) => {
|
|
97
|
+
resolveExit = r;
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
pid,
|
|
101
|
+
killed: false,
|
|
102
|
+
exited,
|
|
103
|
+
stdout,
|
|
104
|
+
closeStdout: () => closeStdout(),
|
|
105
|
+
kill() {
|
|
106
|
+
this.killed = true;
|
|
107
|
+
resolveExit(0);
|
|
108
|
+
},
|
|
109
|
+
stdin: { write: () => 0, flush: () => 0 },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
57
113
|
/** A subprocess that stays "alive" until killed, so exit can be observed. */
|
|
58
114
|
function liveProc(pid = 4243) {
|
|
59
115
|
let resolveExit: (code: number) => void = () => {};
|
|
@@ -263,31 +319,46 @@ describe("broken worker pipe", () => {
|
|
|
263
319
|
});
|
|
264
320
|
|
|
265
321
|
/**
|
|
266
|
-
* A
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
322
|
+
* A batch larger than the pipe buffer, sent to a worker too busy to drain
|
|
323
|
+
* stdin, leaves the write pending, and the pending write rejects after
|
|
324
|
+
* `sendRequest` has returned, once the worker dies. A synchronous guard
|
|
325
|
+
* never sees that rejection.
|
|
270
326
|
*/
|
|
271
327
|
test("a write still pending when the worker dies resolves the request as an error", async () => {
|
|
272
328
|
const backend = newBackend();
|
|
273
|
-
const proc =
|
|
274
|
-
cmd: [process.execPath, "-e", "setTimeout(() => {}, 60_000)"],
|
|
275
|
-
windowsHide: true,
|
|
276
|
-
stdin: "pipe",
|
|
277
|
-
stdout: "ignore",
|
|
278
|
-
stderr: "ignore",
|
|
279
|
-
});
|
|
280
|
-
spawned.push(proc);
|
|
329
|
+
const proc = pendingWriteProc();
|
|
281
330
|
backend.workerProc = proc;
|
|
282
331
|
|
|
283
|
-
const request = backend.sendRequest(["
|
|
284
|
-
proc.kill(
|
|
332
|
+
const request = backend.sendRequest(["hello"]);
|
|
333
|
+
proc.kill();
|
|
285
334
|
const response = await request;
|
|
286
335
|
|
|
287
336
|
expect(response.error).toContain("worker pipe write failed");
|
|
288
337
|
expect(backend.pendingRequests.size).toBe(0);
|
|
289
338
|
});
|
|
290
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Bun does not always report a dead worker through the write: on Linux it
|
|
342
|
+
* occasionally resolves the pending write with a short count instead of
|
|
343
|
+
* rejecting it, and `writeWorkerLine` then stays silent. Nothing times a
|
|
344
|
+
* request out, so the stdout reader ending is the only thing that settles
|
|
345
|
+
* it.
|
|
346
|
+
*/
|
|
347
|
+
test("stdout ending settles a request the pipe never reported a failure for", async () => {
|
|
348
|
+
const backend = newBackend();
|
|
349
|
+
const proc = stdoutProc();
|
|
350
|
+
backend.workerProc = proc;
|
|
351
|
+
backend.startStdoutReader();
|
|
352
|
+
|
|
353
|
+
const inFlight = backend.sendRequest(["hello"]);
|
|
354
|
+
proc.closeStdout();
|
|
355
|
+
|
|
356
|
+
await expect(inFlight).resolves.toMatchObject({
|
|
357
|
+
error: expect.stringContaining("exited unexpectedly"),
|
|
358
|
+
});
|
|
359
|
+
expect(backend.pendingRequests.size).toBe(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
291
362
|
test("a failed write does not leak the pending request", async () => {
|
|
292
363
|
const backend = newBackend();
|
|
293
364
|
backend.workerProc = brokenPipeProc();
|
|
@@ -72,6 +72,7 @@ export type MemoryJobType =
|
|
|
72
72
|
| "delete_message_lexical"
|
|
73
73
|
| "backfill_lexical_index"
|
|
74
74
|
| "skill_card_insert"
|
|
75
|
+
| "skill_update_receipt"
|
|
75
76
|
| "memory_retrospective"
|
|
76
77
|
| "memory_retrospective_sweep"
|
|
77
78
|
// Retired/legacy — no live handler; persisted rows drop via LEGACY_JOB_TYPES.
|
|
@@ -379,6 +380,234 @@ function mergeSkillCardEntries(
|
|
|
379
380
|
return merged;
|
|
380
381
|
}
|
|
381
382
|
|
|
383
|
+
/**
|
|
384
|
+
* One rewrite of a managed skill by a background pass, as the receipt job's
|
|
385
|
+
* payload carries it. `entryId` is the producing tool call's, so a replayed
|
|
386
|
+
* call merges as a no-op; the conversation ids are what the receipt links
|
|
387
|
+
* to and what a conversation delete purges by.
|
|
388
|
+
*/
|
|
389
|
+
export interface SkillUpdateReceiptEntry {
|
|
390
|
+
entryId: string;
|
|
391
|
+
skillId: string;
|
|
392
|
+
name: string;
|
|
393
|
+
changeSummary: string;
|
|
394
|
+
sourceConversationId?: string;
|
|
395
|
+
runConversationId: string;
|
|
396
|
+
createdAt: number;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/** Payload of a `skill_update_receipt` job: one burst's rewrites so far. */
|
|
400
|
+
export interface SkillUpdateReceiptJobPayload {
|
|
401
|
+
/** When the burst's earliest rewrite landed; the cap is measured from it. */
|
|
402
|
+
firstEntryAt: number;
|
|
403
|
+
/** When its latest rewrite landed; the quiet window is measured from it. */
|
|
404
|
+
lastEntryAt: number;
|
|
405
|
+
entries: SkillUpdateReceiptEntry[];
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Upsert a pending `skill_update_receipt` job: the one open receipt for the
|
|
410
|
+
* burst of background skill rewrites in progress. Every append merges into
|
|
411
|
+
* the pending row rather than creating a duplicate; the receipt handler
|
|
412
|
+
* re-upserts the same payload when it defers, and merges into a sibling
|
|
413
|
+
* that opened while it evaluated (see `skill-update-receipt-job.ts`).
|
|
414
|
+
*
|
|
415
|
+
* Merge rules, each load-bearing: entries dedupe by `entryId`, with the
|
|
416
|
+
* pending entry winning so a replay changes nothing; `firstEntryAt` is the
|
|
417
|
+
* EARLIEST of the two, so a continuous stream of rewrites cannot ratchet the
|
|
418
|
+
* cap forward and hold the burst's first entries past their own cap;
|
|
419
|
+
* `lastEntryAt` is the latest; and `runAfter` is the earliest, mirroring
|
|
420
|
+
* `upsertMemoryRetrospectiveJob`. A running row never matches (its payload
|
|
421
|
+
* is frozen at claim), so an append during evaluation opens the sibling the
|
|
422
|
+
* handler merges into.
|
|
423
|
+
*/
|
|
424
|
+
export function upsertSkillUpdateReceiptJob(
|
|
425
|
+
payload: SkillUpdateReceiptJobPayload,
|
|
426
|
+
runAfter: number = Date.now(),
|
|
427
|
+
): void {
|
|
428
|
+
const db = memoryDb();
|
|
429
|
+
const existing = db
|
|
430
|
+
.select()
|
|
431
|
+
.from(memoryJobs)
|
|
432
|
+
.where(
|
|
433
|
+
and(
|
|
434
|
+
eq(memoryJobs.type, "skill_update_receipt"),
|
|
435
|
+
eq(memoryJobs.status, "pending"),
|
|
436
|
+
),
|
|
437
|
+
)
|
|
438
|
+
.orderBy(asc(memoryJobs.createdAt))
|
|
439
|
+
.get();
|
|
440
|
+
if (existing) {
|
|
441
|
+
const existingPayload = parseSkillUpdateReceiptPayload(existing.payload);
|
|
442
|
+
const merged: SkillUpdateReceiptJobPayload = {
|
|
443
|
+
firstEntryAt: Math.min(
|
|
444
|
+
existingPayload?.firstEntryAt ?? payload.firstEntryAt,
|
|
445
|
+
payload.firstEntryAt,
|
|
446
|
+
),
|
|
447
|
+
lastEntryAt: Math.max(
|
|
448
|
+
existingPayload?.lastEntryAt ?? payload.lastEntryAt,
|
|
449
|
+
payload.lastEntryAt,
|
|
450
|
+
),
|
|
451
|
+
entries: mergeSkillUpdateReceiptEntries(
|
|
452
|
+
existingPayload?.entries ?? [],
|
|
453
|
+
payload.entries,
|
|
454
|
+
),
|
|
455
|
+
};
|
|
456
|
+
db.update(memoryJobs)
|
|
457
|
+
.set({
|
|
458
|
+
payload: JSON.stringify(merged),
|
|
459
|
+
runAfter: Math.min(existing.runAfter, runAfter),
|
|
460
|
+
updatedAt: Date.now(),
|
|
461
|
+
})
|
|
462
|
+
.where(eq(memoryJobs.id, existing.id))
|
|
463
|
+
.run();
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
466
|
+
enqueueMemoryJob(
|
|
467
|
+
"skill_update_receipt",
|
|
468
|
+
payload as unknown as Record<string, unknown>,
|
|
469
|
+
runAfter,
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Validate a `skill_update_receipt` payload back into its typed shape, or
|
|
475
|
+
* null for a corrupted or foreign row. Entries missing a usable id, skill,
|
|
476
|
+
* name, summary, or run are dropped rather than failing the whole payload.
|
|
477
|
+
*/
|
|
478
|
+
export function parseSkillUpdateReceiptPayload(
|
|
479
|
+
raw: unknown,
|
|
480
|
+
): SkillUpdateReceiptJobPayload | null {
|
|
481
|
+
let payload: unknown = raw;
|
|
482
|
+
if (typeof raw === "string") {
|
|
483
|
+
try {
|
|
484
|
+
payload = JSON.parse(raw);
|
|
485
|
+
} catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
if (!payload || typeof payload !== "object") {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
const rec = payload as Record<string, unknown>;
|
|
493
|
+
if (
|
|
494
|
+
typeof rec.firstEntryAt !== "number" ||
|
|
495
|
+
typeof rec.lastEntryAt !== "number" ||
|
|
496
|
+
!Array.isArray(rec.entries)
|
|
497
|
+
) {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
return {
|
|
501
|
+
firstEntryAt: rec.firstEntryAt,
|
|
502
|
+
lastEntryAt: rec.lastEntryAt,
|
|
503
|
+
entries: mergeSkillUpdateReceiptEntries([], rec.entries),
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function mergeSkillUpdateReceiptEntries(
|
|
508
|
+
existing: unknown,
|
|
509
|
+
incoming: unknown,
|
|
510
|
+
): SkillUpdateReceiptEntry[] {
|
|
511
|
+
const merged: SkillUpdateReceiptEntry[] = [];
|
|
512
|
+
const seen = new Set<string>();
|
|
513
|
+
const candidates = [
|
|
514
|
+
...(Array.isArray(existing) ? existing : []),
|
|
515
|
+
...(Array.isArray(incoming) ? incoming : []),
|
|
516
|
+
];
|
|
517
|
+
for (const entry of candidates) {
|
|
518
|
+
if (!entry || typeof entry !== "object") {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
const rec = entry as Record<string, unknown>;
|
|
522
|
+
if (
|
|
523
|
+
typeof rec.entryId !== "string" ||
|
|
524
|
+
rec.entryId.length === 0 ||
|
|
525
|
+
typeof rec.skillId !== "string" ||
|
|
526
|
+
typeof rec.name !== "string" ||
|
|
527
|
+
typeof rec.changeSummary !== "string" ||
|
|
528
|
+
typeof rec.runConversationId !== "string" ||
|
|
529
|
+
typeof rec.createdAt !== "number" ||
|
|
530
|
+
seen.has(rec.entryId)
|
|
531
|
+
) {
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
seen.add(rec.entryId);
|
|
535
|
+
merged.push({
|
|
536
|
+
entryId: rec.entryId,
|
|
537
|
+
skillId: rec.skillId,
|
|
538
|
+
name: rec.name,
|
|
539
|
+
changeSummary: rec.changeSummary,
|
|
540
|
+
runConversationId: rec.runConversationId,
|
|
541
|
+
createdAt: rec.createdAt,
|
|
542
|
+
...(typeof rec.sourceConversationId === "string"
|
|
543
|
+
? { sourceConversationId: rec.sourceConversationId }
|
|
544
|
+
: {}),
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
// Rewrite order is the receipt's order, and a merge into a sibling puts
|
|
548
|
+
// the older payload behind the newer one; the stamp restores it, with
|
|
549
|
+
// the id as a deterministic tie-break.
|
|
550
|
+
return merged.sort(
|
|
551
|
+
(a, b) => a.createdAt - b.createdAt || a.entryId.localeCompare(b.entryId),
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Drop a deleted source conversation's entries from every pending
|
|
557
|
+
* `skill_update_receipt` row, deleting a row left with none. The plugin's
|
|
558
|
+
* `conversation-deleted` hook calls this beside its other per-conversation
|
|
559
|
+
* purges; the entries carry that conversation's id and a summary distilled
|
|
560
|
+
* from it. The run conversation is never a purge key: a run is an ephemeral
|
|
561
|
+
* fork that the next successful retrospective garbage-collects, and the
|
|
562
|
+
* handler reads a missing run as finished precisely so its entries still
|
|
563
|
+
* get announced. The burst's bounds are recomputed from what remains, so a
|
|
564
|
+
* dropped edge entry neither holds the rest past their quiet window nor
|
|
565
|
+
* caps them early.
|
|
566
|
+
*/
|
|
567
|
+
export function removeSkillUpdateReceiptEntriesForConversation(
|
|
568
|
+
conversationId: string,
|
|
569
|
+
): void {
|
|
570
|
+
const db = memoryDb();
|
|
571
|
+
const rows = db
|
|
572
|
+
.select()
|
|
573
|
+
.from(memoryJobs)
|
|
574
|
+
.where(
|
|
575
|
+
and(
|
|
576
|
+
eq(memoryJobs.type, "skill_update_receipt"),
|
|
577
|
+
eq(memoryJobs.status, "pending"),
|
|
578
|
+
),
|
|
579
|
+
)
|
|
580
|
+
.all();
|
|
581
|
+
for (const row of rows) {
|
|
582
|
+
const payload = parseSkillUpdateReceiptPayload(row.payload);
|
|
583
|
+
if (!payload) {
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
const kept = payload.entries.filter(
|
|
587
|
+
(entry) => entry.sourceConversationId !== conversationId,
|
|
588
|
+
);
|
|
589
|
+
if (kept.length === payload.entries.length) {
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
if (kept.length === 0) {
|
|
593
|
+
db.delete(memoryJobs).where(eq(memoryJobs.id, row.id)).run();
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
const stamps = kept.map((entry) => entry.createdAt);
|
|
597
|
+
db.update(memoryJobs)
|
|
598
|
+
.set({
|
|
599
|
+
payload: JSON.stringify({
|
|
600
|
+
firstEntryAt: Math.min(...stamps),
|
|
601
|
+
lastEntryAt: Math.max(...stamps),
|
|
602
|
+
entries: kept,
|
|
603
|
+
} satisfies SkillUpdateReceiptJobPayload),
|
|
604
|
+
updatedAt: Date.now(),
|
|
605
|
+
})
|
|
606
|
+
.where(eq(memoryJobs.id, row.id))
|
|
607
|
+
.run();
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
382
611
|
/**
|
|
383
612
|
* Source-conversation ids of every PENDING `memory_retrospective` job. The
|
|
384
613
|
* retrospective sweep skips these up front: a pending row already covers the
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type DrizzleDb, getSqliteFrom } from "../db-connection.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Restamp openai-compatible rows whose endpoint host is opencode.ai (or a
|
|
5
|
+
* subdomain) as provider "opencode". The generic adapter never sends the
|
|
6
|
+
* session headers zen/go requires, so such rows fail every request; the
|
|
7
|
+
* create route applies the same normalization on write. Hosts are judged
|
|
8
|
+
* by URL parsing, not pattern matching, so a path that merely mentions
|
|
9
|
+
* opencode.ai is left alone. Idempotent: a restamped row no longer matches.
|
|
10
|
+
* Workspace migration 158 repoints legacy-shape profile fragments bound to
|
|
11
|
+
* these rows.
|
|
12
|
+
*/
|
|
13
|
+
export function migrateNormalizeOpencodeHostConnections(
|
|
14
|
+
database: DrizzleDb,
|
|
15
|
+
): void {
|
|
16
|
+
const raw = getSqliteFrom(database);
|
|
17
|
+
|
|
18
|
+
const rows = raw
|
|
19
|
+
.query(
|
|
20
|
+
`SELECT name, base_url FROM provider_connections
|
|
21
|
+
WHERE provider = 'openai-compatible' AND base_url IS NOT NULL`,
|
|
22
|
+
)
|
|
23
|
+
.all() as Array<{ name: string; base_url: string }>;
|
|
24
|
+
|
|
25
|
+
const restamp = raw.prepare(
|
|
26
|
+
`UPDATE provider_connections SET provider = 'opencode', updated_at = ? WHERE name = ?`,
|
|
27
|
+
);
|
|
28
|
+
const now = Date.now();
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
if (isOpenCodeHost(row.base_url)) {
|
|
31
|
+
restamp.run(now, row.name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isOpenCodeHost(baseUrl: string): boolean {
|
|
37
|
+
let host: string;
|
|
38
|
+
try {
|
|
39
|
+
host = new URL(baseUrl).hostname.toLowerCase();
|
|
40
|
+
} catch {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
return host === "opencode.ai" || host.endsWith(".opencode.ai");
|
|
44
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { describe, expect, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
5
|
+
|
|
6
|
+
import * as schema from "../../schema.js";
|
|
7
|
+
import { migrateNormalizeOpencodeHostConnections } from "../383-normalize-opencode-host-connections.js";
|
|
8
|
+
|
|
9
|
+
function createTestDb() {
|
|
10
|
+
const sqlite = new Database(":memory:");
|
|
11
|
+
sqlite.exec(/*sql*/ `
|
|
12
|
+
CREATE TABLE provider_connections (
|
|
13
|
+
name TEXT PRIMARY KEY,
|
|
14
|
+
provider TEXT NOT NULL,
|
|
15
|
+
auth TEXT NOT NULL,
|
|
16
|
+
label TEXT,
|
|
17
|
+
base_url TEXT,
|
|
18
|
+
models TEXT,
|
|
19
|
+
created_at INTEGER NOT NULL,
|
|
20
|
+
updated_at INTEGER NOT NULL
|
|
21
|
+
);
|
|
22
|
+
`);
|
|
23
|
+
return { sqlite, db: drizzle(sqlite, { schema }) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function insertRow(
|
|
27
|
+
sqlite: Database,
|
|
28
|
+
name: string,
|
|
29
|
+
provider: string,
|
|
30
|
+
baseUrl: string | null,
|
|
31
|
+
): void {
|
|
32
|
+
sqlite
|
|
33
|
+
.query(
|
|
34
|
+
/*sql*/ `INSERT INTO provider_connections (name, provider, auth, base_url, created_at, updated_at)
|
|
35
|
+
VALUES (?, ?, '{"type":"api_key","credential":"cred"}', ?, 1, 1)`,
|
|
36
|
+
)
|
|
37
|
+
.run(name, provider, baseUrl);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function readProvider(sqlite: Database, name: string): string | undefined {
|
|
41
|
+
const row = sqlite
|
|
42
|
+
.query(`SELECT provider FROM provider_connections WHERE name = ?`)
|
|
43
|
+
.get(name) as { provider: string } | null;
|
|
44
|
+
return row?.provider;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe("migration 383: normalize opencode-host connections", () => {
|
|
48
|
+
test("restamps openai-compatible rows whose host is opencode.ai", () => {
|
|
49
|
+
const { sqlite, db } = createTestDb();
|
|
50
|
+
insertRow(
|
|
51
|
+
sqlite,
|
|
52
|
+
"go",
|
|
53
|
+
"openai-compatible",
|
|
54
|
+
"https://opencode.ai/zen/go/v1",
|
|
55
|
+
);
|
|
56
|
+
insertRow(sqlite, "zen", "openai-compatible", "https://opencode.ai/zen/v1");
|
|
57
|
+
insertRow(sqlite, "sub", "openai-compatible", "https://api.opencode.ai/v1");
|
|
58
|
+
insertRow(sqlite, "port", "openai-compatible", "https://opencode.ai:443");
|
|
59
|
+
insertRow(sqlite, "vllm", "openai-compatible", "http://localhost:8080/v1");
|
|
60
|
+
insertRow(sqlite, "no-url", "openai-compatible", null);
|
|
61
|
+
insertRow(
|
|
62
|
+
sqlite,
|
|
63
|
+
"lookalike",
|
|
64
|
+
"openai-compatible",
|
|
65
|
+
"https://opencode.ai.evil.example/v1",
|
|
66
|
+
);
|
|
67
|
+
insertRow(
|
|
68
|
+
sqlite,
|
|
69
|
+
"path-only",
|
|
70
|
+
"openai-compatible",
|
|
71
|
+
"https://proxy.example/cache/api.opencode.ai/v1",
|
|
72
|
+
);
|
|
73
|
+
insertRow(sqlite, "garbage", "openai-compatible", "not a url");
|
|
74
|
+
insertRow(sqlite, "ollama", "ollama", "https://opencode.ai/v1");
|
|
75
|
+
|
|
76
|
+
migrateNormalizeOpencodeHostConnections(db);
|
|
77
|
+
|
|
78
|
+
expect(readProvider(sqlite, "go")).toBe("opencode");
|
|
79
|
+
expect(readProvider(sqlite, "zen")).toBe("opencode");
|
|
80
|
+
expect(readProvider(sqlite, "sub")).toBe("opencode");
|
|
81
|
+
expect(readProvider(sqlite, "port")).toBe("opencode");
|
|
82
|
+
expect(readProvider(sqlite, "vllm")).toBe("openai-compatible");
|
|
83
|
+
expect(readProvider(sqlite, "no-url")).toBe("openai-compatible");
|
|
84
|
+
expect(readProvider(sqlite, "lookalike")).toBe("openai-compatible");
|
|
85
|
+
expect(readProvider(sqlite, "path-only")).toBe("openai-compatible");
|
|
86
|
+
expect(readProvider(sqlite, "garbage")).toBe("openai-compatible");
|
|
87
|
+
expect(readProvider(sqlite, "ollama")).toBe("ollama");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("is idempotent", () => {
|
|
91
|
+
const { sqlite, db } = createTestDb();
|
|
92
|
+
insertRow(
|
|
93
|
+
sqlite,
|
|
94
|
+
"go",
|
|
95
|
+
"openai-compatible",
|
|
96
|
+
"https://opencode.ai/zen/go/v1",
|
|
97
|
+
);
|
|
98
|
+
migrateNormalizeOpencodeHostConnections(db);
|
|
99
|
+
migrateNormalizeOpencodeHostConnections(db);
|
|
100
|
+
expect(readProvider(sqlite, "go")).toBe("opencode");
|
|
101
|
+
});
|
|
102
|
+
});
|
package/src/persistence/steps.ts
CHANGED
|
@@ -493,6 +493,7 @@ import {
|
|
|
493
493
|
migrateCreateConversationModeSessions,
|
|
494
494
|
} from "./migrations/381-create-conversation-mode-sessions.js";
|
|
495
495
|
import { migrateCreateClientConnectionEvents } from "./migrations/382-create-client-connection-events.js";
|
|
496
|
+
import { migrateNormalizeOpencodeHostConnections } from "./migrations/383-normalize-opencode-host-connections.js";
|
|
496
497
|
import type { MigrationStep } from "./migrations/run-migrations.js";
|
|
497
498
|
|
|
498
499
|
export const migrationSteps: MigrationStep[] = [
|
|
@@ -1649,4 +1650,15 @@ export const migrationSteps: MigrationStep[] = [
|
|
|
1649
1650
|
],
|
|
1650
1651
|
},
|
|
1651
1652
|
migrateCreateClientConnectionEvents,
|
|
1653
|
+
{
|
|
1654
|
+
name: "migrateNormalizeOpencodeHostConnections",
|
|
1655
|
+
run: migrateNormalizeOpencodeHostConnections,
|
|
1656
|
+
// Reads and rewrites provider_connections.base_url; the table and the
|
|
1657
|
+
// column come from these steps, so a database where either failed must
|
|
1658
|
+
// not checkpoint this one against nothing.
|
|
1659
|
+
dependsOn: [
|
|
1660
|
+
"migrateCreateProviderConnections",
|
|
1661
|
+
"migrateProviderConnectionBaseUrlAndModels",
|
|
1662
|
+
],
|
|
1663
|
+
},
|
|
1652
1664
|
];
|
|
@@ -709,6 +709,35 @@ describe("syncAvatarToPlatform", () => {
|
|
|
709
709
|
expect(patches).toHaveLength(1);
|
|
710
710
|
});
|
|
711
711
|
|
|
712
|
+
test("an older rendering spec re-uploads the same avatar once", async () => {
|
|
713
|
+
mockResvgAvailable = true;
|
|
714
|
+
syncAvatarToPlatform();
|
|
715
|
+
await settle();
|
|
716
|
+
const current = JSON.parse(readFileSync(syncStatePath, "utf-8"));
|
|
717
|
+
const previous = {
|
|
718
|
+
...current,
|
|
719
|
+
key: current.key.replace(
|
|
720
|
+
`:${NOTIFICATION_AVATAR_SPEC_VERSION}:`,
|
|
721
|
+
`:${NOTIFICATION_AVATAR_SPEC_VERSION - 1}:`,
|
|
722
|
+
),
|
|
723
|
+
};
|
|
724
|
+
expect(previous.key).not.toBe(current.key);
|
|
725
|
+
writeFileSync(syncStatePath, JSON.stringify(previous));
|
|
726
|
+
|
|
727
|
+
_resetSyncAvatarStateForTests();
|
|
728
|
+
syncAvatarToPlatform();
|
|
729
|
+
await settle();
|
|
730
|
+
_resetSyncAvatarStateForTests();
|
|
731
|
+
syncAvatarToPlatform();
|
|
732
|
+
await settle();
|
|
733
|
+
|
|
734
|
+
expect(patches).toHaveLength(2);
|
|
735
|
+
expect(patches[1].body.notification_avatar_base64).toBeDefined();
|
|
736
|
+
expect(JSON.parse(readFileSync(syncStatePath, "utf-8")).key).toBe(
|
|
737
|
+
current.key,
|
|
738
|
+
);
|
|
739
|
+
});
|
|
740
|
+
|
|
712
741
|
test("a restart re-uploads when the destination or raster changed", async () => {
|
|
713
742
|
syncAvatarToPlatform();
|
|
714
743
|
await settle();
|
|
@@ -82,8 +82,10 @@ Everything else under the plugin root is **spine**:
|
|
|
82
82
|
`conversation-memory-orphan-sweep`, `fork-conversation-memory`,
|
|
83
83
|
`find-most-recent-retrospective-for`.
|
|
84
84
|
- The `memory-retrospective-*` family — tier-agnostic, gated on
|
|
85
|
-
`memory.enabled` alone. The
|
|
86
|
-
`memory-retrospective-skill-card.ts
|
|
85
|
+
`memory.enabled` alone. The exceptions are
|
|
86
|
+
`memory-retrospective-skill-card.ts` and `skill-update-receipt-job.ts` (one
|
|
87
|
+
pending job collects a burst of background skill rewrites and announces it
|
|
88
|
+
once it settles), whose upstream producers only fire when
|
|
87
89
|
`isV3TierActive()` holds (memory on and v3 live).
|
|
88
90
|
- The rest of the spine: `startup.ts`, `jobs-worker.ts`, `job-handlers.ts`,
|
|
89
91
|
`job-handler-registration.ts`, `indexer.ts`, `injectors.ts`, `tools.ts`,
|
|
@@ -499,7 +501,7 @@ Persisted in job rows; unknown types either throw or get silently drained.
|
|
|
499
501
|
- v2 engine: `memory_v2_migrate`, `memory_v2_activation_recompute`
|
|
500
502
|
- v3: `memory_v3_maintain`
|
|
501
503
|
- Retrospective: `memory_retrospective`, `memory_retrospective_sweep`,
|
|
502
|
-
`skill_card_insert`
|
|
504
|
+
`skill_card_insert`, `skill_update_receipt`
|
|
503
505
|
- v1: `graph_extract`, `graph_decay`, `graph_consolidate`, `graph_pattern_scan`,
|
|
504
506
|
`graph_narrative_refine`, `graph_bootstrap`, `embed_pkb_file`, `pkb_filing`,
|
|
505
507
|
`pkb_compaction`, `rebuild_index`, `backfill`, `delete_qdrant_vectors`,
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The receipt job announces a receipt through the notification pipeline and
|
|
3
|
+
* reads what the pipeline did; it never writes the home feed itself and
|
|
4
|
+
* never releases a dedupe key the pipeline kept. A verdict the pipeline
|
|
5
|
+
* reached (a block, a suppression, a no-notify decision) is the pipeline's to
|
|
6
|
+
* make, and a home-feed row written from here would be a notification the
|
|
7
|
+
* routing declined. This guard pins the import surface that keeps that so.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { describe, expect, test } from "bun:test";
|
|
13
|
+
|
|
14
|
+
const MODULE_DIR = join(import.meta.dir, "..");
|
|
15
|
+
|
|
16
|
+
function importedNames(file: string): string[] {
|
|
17
|
+
const source = readFileSync(join(MODULE_DIR, file), "utf-8");
|
|
18
|
+
const names: string[] = [];
|
|
19
|
+
for (const match of source.matchAll(
|
|
20
|
+
/import\s*(?:type\s*)?\{([^}]*)\}\s*from/g,
|
|
21
|
+
)) {
|
|
22
|
+
for (const raw of match[1]!.split(",")) {
|
|
23
|
+
const name = raw
|
|
24
|
+
.replace(/^\s*type\s+/, "")
|
|
25
|
+
.split(/\s+as\s+/)[0]!
|
|
26
|
+
.trim();
|
|
27
|
+
if (name) {
|
|
28
|
+
names.push(name);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return names;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("skill-update receipt boundary", () => {
|
|
36
|
+
for (const file of ["skill-update-receipt-job.ts"]) {
|
|
37
|
+
test(`${file} neither writes the home feed nor releases a dedupe key`, () => {
|
|
38
|
+
const names = importedNames(file);
|
|
39
|
+
expect(names).not.toContain("appendFeedItem");
|
|
40
|
+
expect(names).not.toContain("patchFeedItemContent");
|
|
41
|
+
expect(names).not.toContain("writeHomeFeedItemForSignal");
|
|
42
|
+
expect(names).not.toContain("setEventDedupeKey");
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|