@vellumai/assistant 0.12.3 → 0.12.4-dev.202609231620.a15f321
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/Dockerfile +7 -0
- package/README.md +12 -12
- package/docker/mousepad/90-vellum.gschema.override +4 -0
- 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/openapi.yaml +97 -4
- package/package.json +1 -1
- package/src/__tests__/approval-routes-http.test.ts +4 -2
- package/src/__tests__/call-routes-http.test.ts +4 -2
- package/src/__tests__/channel-readiness-discord.test.ts +8 -0
- package/src/__tests__/channel-readiness-slack-remote.test.ts +8 -0
- package/src/__tests__/config-loader-backfill.test.ts +1 -0
- package/src/__tests__/conversation-pairing.test.ts +10 -0
- package/src/__tests__/conversation-resurface-done.test.ts +289 -0
- package/src/__tests__/default-profile-catalog-fallback.test.ts +1 -0
- package/src/__tests__/gemini-provider.test.ts +31 -0
- package/src/__tests__/inference-profile-validation-jev.test.ts +17 -0
- package/src/__tests__/job-handler-registration-guard.test.ts +1 -0
- package/src/__tests__/llm-catalog-parity.test.ts +4 -0
- package/src/__tests__/llm-resolver-override-or-default.test.ts +43 -0
- package/src/__tests__/openai-responses-prompt-cache.test.ts +3 -1
- package/src/__tests__/platform-callback-registration.test.ts +46 -1
- package/src/__tests__/plugin-api-model-profiles.test.ts +7 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
- package/src/__tests__/pricing.test.ts +33 -0
- package/src/__tests__/provider-error-scenarios.test.ts +46 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +45 -15
- package/src/__tests__/retry-fallback-escalation.test.ts +22 -0
- package/src/__tests__/runtime-attachment-metadata.test.ts +4 -2
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +116 -66
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -0
- package/src/__tests__/send-endpoint-busy.test.ts +4 -2
- package/src/__tests__/skill-load-inline-command.test.ts +35 -0
- package/src/__tests__/skill-load-tool.test.ts +42 -11
- package/src/__tests__/skill-tool-factory.test.ts +50 -2
- package/src/__tests__/steer-on-enqueue-question.test.ts +265 -34
- package/src/__tests__/validate-input.test.ts +73 -0
- package/src/__tests__/workspace-migration-159-rename-colliding-jev-profile-name.test.ts +197 -0
- package/src/api/ask-question-tool.ts +50 -0
- package/src/api/events/tool-result.ts +11 -0
- package/src/api/index.ts +7 -0
- package/src/api/requests/dictation.ts +2 -2
- package/src/api/responses/home.ts +27 -0
- package/src/avatar/notification-avatar.test.ts +42 -34
- package/src/avatar/notification-avatar.ts +1 -0
- package/src/calls/__tests__/voice-session-bridge.test.ts +63 -1
- package/src/calls/voice-escalation-judge.ts +6 -5
- package/src/calls/voice-screen-annotation.ts +44 -0
- package/src/calls/voice-session-bridge.ts +22 -0
- package/src/cli/commands/__tests__/plugins.test.ts +29 -1
- package/src/cli/commands/channel-verification-sessions.help.ts +12 -2
- package/src/cli/commands/platform/index.help.ts +2 -1
- package/src/cli/commands/platform/index.ts +8 -2
- package/src/cli/commands/plugins.ts +16 -0
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +29 -0
- package/src/cli/lib/__tests__/plugin-catalog-cache.test.ts +92 -2
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +15 -0
- package/src/cli/lib/__tests__/plugin-catalog-platform.test.ts +66 -2
- package/src/cli/lib/__tests__/plugin-catalog-resolve.test.ts +21 -0
- package/src/cli/lib/__tests__/plugin-catalog-visibility.test.ts +65 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +43 -0
- package/src/cli/lib/__tests__/plugins-install-offline.test.ts +26 -0
- package/src/cli/lib/bundled-marketplace.json +4 -4
- package/src/cli/lib/inspect-plugin.ts +5 -0
- package/src/cli/lib/plugin-catalog-cache.ts +41 -4
- package/src/cli/lib/plugin-catalog-local.ts +9 -0
- package/src/cli/lib/plugin-catalog-platform.ts +48 -9
- package/src/cli/lib/plugin-catalog-visibility.ts +41 -0
- package/src/cli/lib/plugin-details.ts +10 -7
- package/src/cli/lib/search-plugins.ts +2 -0
- package/src/config/__tests__/default-profile-catalog.test.ts +61 -1
- package/src/config/__tests__/jev-managed-profile.test.ts +82 -0
- package/src/config/__tests__/sync-gated-profiles.test.ts +71 -0
- package/src/config/bundled-skills/screen-annotation/SKILL.md +6 -3
- package/src/config/call-site-defaults.ts +17 -11
- package/src/config/default-profile-catalog.ts +118 -19
- package/src/config/default-profile-names.ts +69 -0
- package/src/config/feature-flag-registry.json +25 -9
- package/src/config/inference-profile-validation.ts +7 -6
- package/src/config/llm-resolver.ts +16 -0
- package/src/config/profile-text-generation.test.ts +9 -0
- package/src/config/schemas/call-site-catalog.ts +8 -1
- package/src/config/schemas/llm.ts +70 -9
- package/src/config/sidebar-done-gate.ts +27 -0
- package/src/config/sync-gated-profiles.ts +96 -44
- package/src/daemon/__tests__/auto-profile-router.test.ts +228 -0
- package/src/daemon/auto-profile-router.ts +220 -0
- package/src/daemon/conversation-agent-loop.ts +77 -4
- package/src/daemon/conversation-slash.ts +2 -2
- package/src/daemon/conversation-turn-finalize.ts +12 -0
- package/src/daemon/dictation-text-processing.ts +4 -4
- package/src/daemon/handlers/__tests__/conversation-history-search.test.ts +61 -0
- package/src/daemon/handlers/config-channels.ts +3 -1
- package/src/daemon/handlers/conversation-history.ts +11 -1
- package/src/daemon/handlers/conversations.ts +118 -10
- package/src/daemon/pkb-reminder-builder.test.ts +10 -3
- package/src/daemon/pkb-reminder-builder.ts +1 -1
- package/src/desktop/desktop-browser-endpoint.ts +1 -0
- package/src/home/feed-types.ts +2 -0
- package/src/inbound/platform-callback-registration.ts +19 -6
- package/src/ipc/routes/__tests__/conversation-sync-ipc-routes.test.ts +49 -1
- package/src/ipc/routes/conversation-sync-ipc-routes.ts +38 -2
- package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +27 -0
- 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 +7 -6
- package/src/live-voice/protocol.ts +16 -5
- package/src/messaging/providers/gmail/client.ts +4 -0
- package/src/notifications/README.md +11 -3
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +87 -3
- package/src/notifications/conversation-pairing.ts +23 -8
- package/src/notifications/home-feed-mirror.ts +39 -0
- package/src/notifications/home-feed-side-effect.ts +46 -19
- package/src/oauth/AGENTS.md +7 -1
- package/src/permissions/question-prompter.ts +14 -4
- package/src/persistence/conversation-crud.ts +114 -3
- package/src/persistence/conversation-queries.ts +48 -3
- package/src/persistence/embeddings/__tests__/embedding-local-lifecycle.test.ts +228 -32
- package/src/persistence/embeddings/embedding-backend-shutdown.test.ts +47 -0
- package/src/persistence/embeddings/embedding-backend.ts +23 -41
- package/src/persistence/embeddings/embedding-gemini.test.ts +17 -0
- package/src/persistence/embeddings/embedding-gemini.ts +3 -1
- package/src/persistence/embeddings/embedding-local.ts +72 -17
- package/src/persistence/embeddings/embedding-types.ts +0 -19
- 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/plugin-api/model-profiles.ts +5 -3
- package/src/plugins/defaults/memory/AGENTS.md +5 -3
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-skill-card.test.ts +2 -0
- 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/graph/tools.ts +9 -5
- package/src/plugins/defaults/memory/hooks/conversation-deleted.ts +4 -0
- package/src/plugins/defaults/memory/job-handlers.ts +8 -2
- package/src/plugins/defaults/memory/memory-retrospective-skill-card.ts +5 -0
- package/src/plugins/defaults/memory/skill-update-receipt-job.ts +334 -0
- package/src/plugins/defaults/memory/substrate/skill-content.ts +1 -1
- package/src/plugins/defaults/memory/tools.test.ts +17 -0
- package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +3 -0
- package/src/providers/__tests__/connection-model-compat.test.ts +2 -0
- package/src/providers/__tests__/search-provider-catalog.test.ts +8 -0
- package/src/providers/anthropic/client.ts +2 -2
- package/src/providers/gemini/client.ts +17 -1
- package/src/providers/inference/auth.ts +25 -0
- package/src/providers/jev/ask.test.ts +66 -0
- package/src/{calls/typesafe-noul.ts → providers/jev/ask.ts} +75 -36
- package/src/providers/malformed-tool-call.ts +55 -0
- package/src/providers/model-catalog.ts +257 -4
- package/src/providers/openai/__tests__/chat-completions-malformed-tool-call.test.ts +105 -0
- package/src/providers/openai/chat-completions-provider.ts +30 -0
- package/src/providers/openai/codex-models.ts +2 -0
- package/src/providers/platform-proxy/constants.ts +5 -0
- package/src/providers/retry.ts +68 -36
- package/src/providers/search-provider-catalog.ts +10 -0
- package/src/providers/vellum-model-routing.test.ts +1 -0
- package/src/runtime/__tests__/agent-wake-archived-gate.test.ts +90 -0
- package/src/runtime/__tests__/question-resolution.test.ts +32 -0
- package/src/runtime/agent-wake.ts +9 -2
- package/src/runtime/auth/__tests__/middleware.test.ts +138 -0
- package/src/runtime/auth/middleware.ts +15 -11
- package/src/runtime/middleware/auth.ts +7 -1
- package/src/runtime/pending-interactions.ts +8 -8
- package/src/runtime/question-resolution.ts +54 -8
- package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +146 -0
- package/src/runtime/routes/__tests__/dictation-processing.test.ts +174 -0
- package/src/runtime/routes/__tests__/global-search-routes.test.ts +74 -1
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +60 -0
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +16 -1
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +8 -0
- package/src/runtime/routes/conversation-list-routes.ts +16 -14
- package/src/runtime/routes/conversation-query-routes.ts +4 -0
- package/src/runtime/routes/conversation-routes.ts +4 -2
- package/src/runtime/routes/diagnostics-routes.ts +66 -229
- package/src/runtime/routes/global-search-routes.ts +22 -7
- package/src/runtime/routes/inference-provider-connection-routes.ts +25 -8
- package/src/runtime/routes/llm-call-sites-routes.ts +19 -10
- package/src/runtime/routes/platform-routes.ts +6 -1
- package/src/runtime/routes/plugins-routes.ts +5 -2
- package/src/runtime/sync/documents-sidecar-publish.test.ts +50 -4
- package/src/runtime/sync/resource-sync-events.ts +11 -0
- package/src/runtime/sync/worker-daemon-notify.test.ts +30 -0
- package/src/runtime/sync/worker-daemon-notify.ts +44 -0
- package/src/runtime/verification-outbound-actions.ts +2 -2
- package/src/skills/validate-input.ts +75 -11
- package/src/telegram/__tests__/webhook-health.test.ts +8 -0
- package/src/tools/ask-question/ask-question-tool.test.ts +38 -0
- package/src/tools/ask-question/ask-question-tool.ts +12 -1
- 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/execute.ts +1 -1
- package/src/tools/skills/format-input-schema.ts +15 -0
- package/src/tools/skills/load.ts +15 -43
- package/src/tools/skills/scaffold-managed.ts +86 -71
- package/src/tools/skills/skill-tool-factory.ts +2 -1
- package/src/tools/workflows/manage-workflows.ts +4 -2
- package/src/util/worker-pipe.test.ts +146 -0
- package/src/util/worker-pipe.ts +49 -0
- package/src/workspace/migrations/158-repoint-opencode-host-provider-fragments.ts +163 -0
- package/src/workspace/migrations/159-rename-colliding-jev-profile-name.ts +344 -0
- package/src/workspace/migrations/__tests__/158-repoint-opencode-host-provider-fragments.test.ts +188 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/runtime/routes/__tests__/dictation-cleanup-guard.test.ts +0 -107
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from "node:fs";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
|
|
10
|
+
import type { Subprocess } from "bun";
|
|
11
|
+
|
|
10
12
|
import { getIsContainerized } from "../../config/env-registry.js";
|
|
11
13
|
import { getLogger } from "../../util/logger.js";
|
|
12
14
|
import {
|
|
@@ -22,6 +24,7 @@ import {
|
|
|
22
24
|
listWorkerProcesses,
|
|
23
25
|
pid1OwnsWorkers,
|
|
24
26
|
} from "../../util/worker-ownership.js";
|
|
27
|
+
import { writeWorkerLine } from "../../util/worker-pipe.js";
|
|
25
28
|
import { EmbeddingRuntimeManager } from "./embedding-runtime-manager.js";
|
|
26
29
|
import {
|
|
27
30
|
type EmbeddingBackend,
|
|
@@ -106,9 +109,7 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
106
109
|
readonly provider = "local" as const;
|
|
107
110
|
readonly model: string;
|
|
108
111
|
|
|
109
|
-
|
|
110
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
111
|
-
private workerProc: any = null;
|
|
112
|
+
private workerProc: Subprocess<"pipe", "pipe", "pipe"> | null = null;
|
|
112
113
|
private stdoutBuffer = "";
|
|
113
114
|
private requestCounter = 0;
|
|
114
115
|
private pendingRequests = new Map<
|
|
@@ -119,7 +120,10 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
119
120
|
>();
|
|
120
121
|
private stdoutReaderActive = false;
|
|
121
122
|
private activeEmbeds = 0;
|
|
123
|
+
/** Release the worker once idle. {@link forModel} withdraws the request. */
|
|
122
124
|
private disposeRequested = false;
|
|
125
|
+
/** Set by {@link shutdown}. The process is exiting, so nothing reopens this. */
|
|
126
|
+
private processExiting = false;
|
|
123
127
|
|
|
124
128
|
private readonly initGuard = new PromiseGuard<void>();
|
|
125
129
|
private initInFlight: Promise<void> | null = null;
|
|
@@ -130,7 +134,67 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
130
134
|
/** Overridable so tests can exercise the escalation path without the wait. */
|
|
131
135
|
private terminateGraceMs = WORKER_TERMINATE_GRACE_MS;
|
|
132
136
|
|
|
133
|
-
|
|
137
|
+
/**
|
|
138
|
+
* Every backend this process has handed out, one per model.
|
|
139
|
+
*
|
|
140
|
+
* {@link reclaimOwnedWorkers} terminates a same-model worker parented to this
|
|
141
|
+
* process that the instance holds no handle for. That is sound only while no
|
|
142
|
+
* other instance in this process can hold that worker, so instances come from
|
|
143
|
+
* {@link forModel} and are never constructed by callers.
|
|
144
|
+
*
|
|
145
|
+
* This is also the complete list of workers the process owns. The backend
|
|
146
|
+
* cache is not: it forgets a backend whose disposal is still pending.
|
|
147
|
+
*/
|
|
148
|
+
private static readonly byModel = new Map<string, LocalEmbeddingBackend>();
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* The backend for `model`. A pending {@link dispose} is withdrawn, so embeds
|
|
152
|
+
* still running on the instance keep their worker.
|
|
153
|
+
*/
|
|
154
|
+
static forModel(model: string): LocalEmbeddingBackend {
|
|
155
|
+
let backend = LocalEmbeddingBackend.byModel.get(model);
|
|
156
|
+
if (!backend) {
|
|
157
|
+
backend = new LocalEmbeddingBackend(model);
|
|
158
|
+
LocalEmbeddingBackend.byModel.set(model, backend);
|
|
159
|
+
}
|
|
160
|
+
if (!backend.processExiting) {
|
|
161
|
+
backend.disposeRequested = false;
|
|
162
|
+
}
|
|
163
|
+
return backend;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** {@link shutdown} every backend, then reap any worker still parented here. */
|
|
167
|
+
static async shutdownAll(): Promise<void> {
|
|
168
|
+
await Promise.all(
|
|
169
|
+
[...LocalEmbeddingBackend.byModel.values()].map(async (backend) => {
|
|
170
|
+
try {
|
|
171
|
+
await backend.shutdown();
|
|
172
|
+
await backend.sweepOwnedWorkers();
|
|
173
|
+
} catch (err) {
|
|
174
|
+
log.warn(
|
|
175
|
+
{ err, model: backend.model },
|
|
176
|
+
"Failed to shut down local embedding backend",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}),
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** {@link terminateNow} every backend. */
|
|
184
|
+
static terminateAllNow(): void {
|
|
185
|
+
for (const backend of LocalEmbeddingBackend.byModel.values()) {
|
|
186
|
+
try {
|
|
187
|
+
backend.terminateNow();
|
|
188
|
+
} catch (err) {
|
|
189
|
+
log.warn(
|
|
190
|
+
{ err, model: backend.model },
|
|
191
|
+
"Failed to terminate local embedding worker",
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private constructor(model: string) {
|
|
134
198
|
this.model = model;
|
|
135
199
|
}
|
|
136
200
|
|
|
@@ -193,19 +257,9 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
193
257
|
}
|
|
194
258
|
this.pendingRequests.set(id, { resolve });
|
|
195
259
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
// which tears down the whole process (JARVIS-1125).
|
|
200
|
-
//
|
|
201
|
-
// The guard covers `write` as well as `flush`: both are synchronous on
|
|
202
|
-
// Bun's FileSink, and `write` is the call that raises the broken pipe.
|
|
203
|
-
try {
|
|
204
|
-
proc.stdin.write(JSON.stringify({ id, texts }) + "\n");
|
|
205
|
-
proc.stdin.flush();
|
|
206
|
-
} catch (err) {
|
|
207
|
-
this.failPendingRequest(id, err);
|
|
208
|
-
}
|
|
260
|
+
writeWorkerLine(proc.stdin, JSON.stringify({ id, texts }), (err) =>
|
|
261
|
+
this.failPendingRequest(id, err),
|
|
262
|
+
);
|
|
209
263
|
});
|
|
210
264
|
}
|
|
211
265
|
|
|
@@ -929,6 +983,7 @@ export class LocalEmbeddingBackend implements EmbeddingBackend {
|
|
|
929
983
|
* the OS confirms the worker is gone.
|
|
930
984
|
*/
|
|
931
985
|
async shutdown(): Promise<void> {
|
|
986
|
+
this.processExiting = true;
|
|
932
987
|
this.disposeRequested = true;
|
|
933
988
|
|
|
934
989
|
// An initialization already in flight has not necessarily assigned
|
|
@@ -113,25 +113,6 @@ export interface EmbeddingBackend {
|
|
|
113
113
|
options?: EmbeddingRequestOptions,
|
|
114
114
|
): Promise<number[][]>;
|
|
115
115
|
dispose?(): void;
|
|
116
|
-
/**
|
|
117
|
-
* Deterministic teardown for daemon shutdown: release any OS resources the
|
|
118
|
-
* backend owns (e.g. a worker subprocess) and resolve only once they are
|
|
119
|
-
* confirmed gone. Unlike {@link dispose}, which may defer while embeds are
|
|
120
|
-
* in flight, this must not leave a child process behind.
|
|
121
|
-
*/
|
|
122
|
-
shutdown?(): Promise<void>;
|
|
123
|
-
/**
|
|
124
|
-
* Release owned OS resources synchronously, for a process that must exit this
|
|
125
|
-
* tick. Unlike {@link shutdown} it cannot wait for confirmation, so it uses
|
|
126
|
-
* the uncatchable signal rather than the graceful one.
|
|
127
|
-
*/
|
|
128
|
-
terminateNow?(): void;
|
|
129
|
-
/**
|
|
130
|
-
* Reap any owned child still attributable to this process, without needing a
|
|
131
|
-
* handle for it. Backstop for a teardown that could not resolve its handle in
|
|
132
|
-
* time.
|
|
133
|
-
*/
|
|
134
|
-
sweepOwnedWorkers?(): Promise<void>;
|
|
135
116
|
}
|
|
136
117
|
|
|
137
118
|
/**
|
|
@@ -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();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getConversationProfilesForProvider } from "../config/default-profile-catalog.js";
|
|
2
2
|
import { getConfig } from "../config/loader.js";
|
|
3
3
|
import { isDispatchableProfile } from "../config/profile-dispatchability.js";
|
|
4
4
|
import { orderProfileKeys } from "../config/profile-order.js";
|
|
@@ -7,7 +7,9 @@ import type { ModelProfileInfo } from "./types.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* List the workspace inference profiles a plugin can route to, in the order a
|
|
9
9
|
* user-facing model picker presents them (`llm.profileOrder` first, then the
|
|
10
|
-
* rest alphabetically). Managed backup routes
|
|
10
|
+
* rest alphabetically). Managed backup routes and the managed Jev profile
|
|
11
|
+
* are omitted with the rest of the conversation view: a plugin routes
|
|
12
|
+
* conversation turns, and a decision model cannot answer them.
|
|
11
13
|
* Metadata-only entries without a provider, model, or mix are not routing
|
|
12
14
|
* targets, so plugins never see them. Disabled profiles are included and flagged via
|
|
13
15
|
* {@link ModelProfileInfo.isDisabled}; weighted "mix" profiles are included and
|
|
@@ -20,7 +22,7 @@ import type { ModelProfileInfo } from "./types.js";
|
|
|
20
22
|
export function getModelProfiles(): ModelProfileInfo[] {
|
|
21
23
|
const { llm } = getConfig();
|
|
22
24
|
const { activeProfile } = llm;
|
|
23
|
-
const profiles =
|
|
25
|
+
const profiles = getConversationProfilesForProvider(
|
|
24
26
|
llm.profiles,
|
|
25
27
|
llm.defaultProvider ?? null,
|
|
26
28
|
);
|
|
@@ -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`,
|
|
@@ -238,6 +238,7 @@ describe("memory-retrospective skill card", () => {
|
|
|
238
238
|
metadata: { kind: "skill-authored-card", automated: true },
|
|
239
239
|
skipIndexing: true,
|
|
240
240
|
clientMessageId: "skill-card-run-conv-1",
|
|
241
|
+
skipResurface: true,
|
|
241
242
|
});
|
|
242
243
|
// Disk view sync mirrors the wake-persist path, then clients are told to
|
|
243
244
|
// refetch the message list.
|
|
@@ -423,6 +424,7 @@ describe("memory-retrospective skill card", () => {
|
|
|
423
424
|
metadata: { kind: "skill-authored-card", automated: true },
|
|
424
425
|
skipIndexing: true,
|
|
425
426
|
clientMessageId: "skill-card-run-conv-1",
|
|
427
|
+
skipResurface: true,
|
|
426
428
|
});
|
|
427
429
|
expect(syncedToDisk).toHaveLength(1);
|
|
428
430
|
expect(publishedConversationIds).toEqual(["src-conv-9"]);
|