@vellumai/assistant 0.8.2 → 0.8.3
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/ARCHITECTURE.md +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
package/src/ipc/cli-client.ts
CHANGED
|
@@ -135,19 +135,26 @@ export async function cliIpcCall<T = unknown>(
|
|
|
135
135
|
|
|
136
136
|
const reqId = crypto.randomUUID();
|
|
137
137
|
|
|
138
|
-
opts?.signal?.addEventListener(
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
opts?.signal?.addEventListener(
|
|
139
|
+
"abort",
|
|
140
|
+
() => {
|
|
141
|
+
finish({ ok: false, error: "Request aborted" });
|
|
142
|
+
},
|
|
143
|
+
{ once: true },
|
|
144
|
+
);
|
|
141
145
|
|
|
142
146
|
const reader = new IpcFrameReader(
|
|
143
147
|
(envelope) => {
|
|
144
148
|
if (envelope.id !== reqId) return;
|
|
145
149
|
const msg = envelope as IpcResponse;
|
|
146
150
|
if (msg.error) {
|
|
147
|
-
finish({
|
|
151
|
+
finish({
|
|
152
|
+
ok: false,
|
|
153
|
+
error: msg.error,
|
|
148
154
|
...(msg.statusCode != null && { statusCode: msg.statusCode }),
|
|
149
155
|
...(msg.errorCode != null && { errorCode: msg.errorCode }),
|
|
150
|
-
...(msg.errorDetails != null && { errorDetails: msg.errorDetails })
|
|
156
|
+
...(msg.errorDetails != null && { errorDetails: msg.errorDetails }),
|
|
157
|
+
});
|
|
151
158
|
} else {
|
|
152
159
|
finish({ ok: true, result: msg.result as T });
|
|
153
160
|
}
|
|
@@ -199,7 +206,13 @@ export async function cliIpcCallBinary(
|
|
|
199
206
|
opts?: { timeoutMs?: number; signal?: AbortSignal },
|
|
200
207
|
): Promise<
|
|
201
208
|
| { ok: true; headers: Record<string, string>; bytes: Uint8Array }
|
|
202
|
-
| {
|
|
209
|
+
| {
|
|
210
|
+
ok: false;
|
|
211
|
+
error: string;
|
|
212
|
+
statusCode?: number;
|
|
213
|
+
errorCode?: string;
|
|
214
|
+
errorDetails?: unknown;
|
|
215
|
+
}
|
|
203
216
|
> {
|
|
204
217
|
if (opts?.signal?.aborted) {
|
|
205
218
|
throw opts.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
@@ -215,7 +228,13 @@ export async function cliIpcCallBinary(
|
|
|
215
228
|
const finish = (
|
|
216
229
|
result:
|
|
217
230
|
| { ok: true; headers: Record<string, string>; bytes: Uint8Array }
|
|
218
|
-
| {
|
|
231
|
+
| {
|
|
232
|
+
ok: false;
|
|
233
|
+
error: string;
|
|
234
|
+
statusCode?: number;
|
|
235
|
+
errorCode?: string;
|
|
236
|
+
errorDetails?: unknown;
|
|
237
|
+
},
|
|
219
238
|
) => {
|
|
220
239
|
if (settled) return;
|
|
221
240
|
settled = true;
|
|
@@ -226,8 +245,14 @@ export async function cliIpcCallBinary(
|
|
|
226
245
|
};
|
|
227
246
|
|
|
228
247
|
const connectTimer = setTimeout(() => {
|
|
229
|
-
log.debug(
|
|
230
|
-
|
|
248
|
+
log.debug(
|
|
249
|
+
{ method, socketPath, timeoutMs: CONNECT_TIMEOUT_MS },
|
|
250
|
+
"CLI IPC binary connect timed out",
|
|
251
|
+
);
|
|
252
|
+
finish({
|
|
253
|
+
ok: false,
|
|
254
|
+
error: `Could not connect to the assistant at ${socketPath}.\nRun \`assistant status\` to check, or \`assistant gateway start\` to start it.`,
|
|
255
|
+
});
|
|
231
256
|
}, CONNECT_TIMEOUT_MS);
|
|
232
257
|
|
|
233
258
|
const socket = new Socket();
|
|
@@ -235,7 +260,10 @@ export async function cliIpcCallBinary(
|
|
|
235
260
|
|
|
236
261
|
socket.on("error", (err) => {
|
|
237
262
|
const code = (err as NodeJS.ErrnoException).code;
|
|
238
|
-
log.debug(
|
|
263
|
+
log.debug(
|
|
264
|
+
{ err, code, method, socketPath },
|
|
265
|
+
"CLI IPC binary socket error",
|
|
266
|
+
);
|
|
239
267
|
finish({
|
|
240
268
|
ok: false,
|
|
241
269
|
error:
|
|
@@ -258,23 +286,37 @@ export async function cliIpcCallBinary(
|
|
|
258
286
|
|
|
259
287
|
const reqId = crypto.randomUUID();
|
|
260
288
|
|
|
261
|
-
opts?.signal?.addEventListener(
|
|
262
|
-
|
|
263
|
-
|
|
289
|
+
opts?.signal?.addEventListener(
|
|
290
|
+
"abort",
|
|
291
|
+
() => {
|
|
292
|
+
finish({ ok: false, error: "Request aborted" });
|
|
293
|
+
},
|
|
294
|
+
{ once: true },
|
|
295
|
+
);
|
|
264
296
|
|
|
265
297
|
const reader = new IpcFrameReader(
|
|
266
298
|
(envelope, binary) => {
|
|
267
299
|
if (envelope.id !== reqId) return;
|
|
268
300
|
const msg = envelope as IpcResponse;
|
|
269
301
|
if (msg.error) {
|
|
270
|
-
finish({
|
|
302
|
+
finish({
|
|
303
|
+
ok: false,
|
|
304
|
+
error: msg.error,
|
|
271
305
|
...(msg.statusCode != null && { statusCode: msg.statusCode }),
|
|
272
306
|
...(msg.errorCode != null && { errorCode: msg.errorCode }),
|
|
273
|
-
...(msg.errorDetails != null && { errorDetails: msg.errorDetails })
|
|
307
|
+
...(msg.errorDetails != null && { errorDetails: msg.errorDetails }),
|
|
308
|
+
});
|
|
274
309
|
} else if (binary === undefined) {
|
|
275
|
-
finish({
|
|
310
|
+
finish({
|
|
311
|
+
ok: false,
|
|
312
|
+
error: "Expected binary frame but received JSON-only response",
|
|
313
|
+
});
|
|
276
314
|
} else {
|
|
277
|
-
finish({
|
|
315
|
+
finish({
|
|
316
|
+
ok: true,
|
|
317
|
+
headers: (envelope.headers ?? {}) as Record<string, string>,
|
|
318
|
+
bytes: binary,
|
|
319
|
+
});
|
|
278
320
|
}
|
|
279
321
|
},
|
|
280
322
|
(err) => finish({ ok: false, error: err.message }),
|
|
@@ -285,7 +327,10 @@ export async function cliIpcCallBinary(
|
|
|
285
327
|
writeMessage(socket, { id: reqId, method, params });
|
|
286
328
|
|
|
287
329
|
callTimer = setTimeout(() => {
|
|
288
|
-
log.debug(
|
|
330
|
+
log.debug(
|
|
331
|
+
{ method, socketPath, timeoutMs: callTimeoutMs },
|
|
332
|
+
"CLI IPC binary call timed out",
|
|
333
|
+
);
|
|
289
334
|
finish({ ok: false, error: "Request timed out" });
|
|
290
335
|
}, callTimeoutMs);
|
|
291
336
|
|
|
@@ -323,24 +368,42 @@ export async function cliIpcCallStream(
|
|
|
323
368
|
params?: Record<string, unknown>,
|
|
324
369
|
opts?: { firstByteTimeoutMs?: number; signal?: AbortSignal },
|
|
325
370
|
): Promise<
|
|
326
|
-
| {
|
|
327
|
-
|
|
371
|
+
| {
|
|
372
|
+
ok: true;
|
|
373
|
+
headers: Record<string, string>;
|
|
374
|
+
body: ReadableStream<Uint8Array>;
|
|
375
|
+
abort: () => void;
|
|
376
|
+
}
|
|
377
|
+
| {
|
|
378
|
+
ok: false;
|
|
379
|
+
error: string;
|
|
380
|
+
statusCode?: number;
|
|
381
|
+
errorCode?: string;
|
|
382
|
+
errorDetails?: unknown;
|
|
383
|
+
}
|
|
328
384
|
> {
|
|
329
385
|
if (opts?.signal?.aborted) {
|
|
330
386
|
throw opts.signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
331
387
|
}
|
|
332
388
|
|
|
333
389
|
const socketPath = getAssistantSocketPath();
|
|
334
|
-
const firstByteTimeoutMs =
|
|
390
|
+
const firstByteTimeoutMs =
|
|
391
|
+
opts?.firstByteTimeoutMs ?? DEFAULT_FIRST_BYTE_TIMEOUT_MS;
|
|
335
392
|
|
|
336
393
|
return new Promise((resolve) => {
|
|
337
394
|
let settled = false;
|
|
338
395
|
let firstByteTimer: ReturnType<typeof setTimeout> | undefined;
|
|
339
|
-
let streamController:
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
396
|
+
let streamController:
|
|
397
|
+
| ReadableStreamDefaultController<Uint8Array>
|
|
398
|
+
| undefined;
|
|
399
|
+
|
|
400
|
+
const finishError = (result: {
|
|
401
|
+
ok: false;
|
|
402
|
+
error: string;
|
|
403
|
+
statusCode?: number;
|
|
404
|
+
errorCode?: string;
|
|
405
|
+
errorDetails?: unknown;
|
|
406
|
+
}) => {
|
|
344
407
|
if (settled) return;
|
|
345
408
|
settled = true;
|
|
346
409
|
clearTimeout(connectTimer);
|
|
@@ -366,8 +429,14 @@ export async function cliIpcCallStream(
|
|
|
366
429
|
};
|
|
367
430
|
|
|
368
431
|
const connectTimer = setTimeout(() => {
|
|
369
|
-
log.debug(
|
|
370
|
-
|
|
432
|
+
log.debug(
|
|
433
|
+
{ method, socketPath, timeoutMs: CONNECT_TIMEOUT_MS },
|
|
434
|
+
"CLI IPC stream connect timed out",
|
|
435
|
+
);
|
|
436
|
+
finishError({
|
|
437
|
+
ok: false,
|
|
438
|
+
error: `Could not connect to the assistant at ${socketPath}.\nRun \`assistant status\` to check, or \`assistant gateway start\` to start it.`,
|
|
439
|
+
});
|
|
371
440
|
}, CONNECT_TIMEOUT_MS);
|
|
372
441
|
|
|
373
442
|
const socket = new Socket();
|
|
@@ -375,7 +444,10 @@ export async function cliIpcCallStream(
|
|
|
375
444
|
|
|
376
445
|
socket.on("error", (err) => {
|
|
377
446
|
const code = (err as NodeJS.ErrnoException).code;
|
|
378
|
-
log.debug(
|
|
447
|
+
log.debug(
|
|
448
|
+
{ err, code, method, socketPath },
|
|
449
|
+
"CLI IPC stream socket error",
|
|
450
|
+
);
|
|
379
451
|
if (!settled) {
|
|
380
452
|
finishError({
|
|
381
453
|
ok: false,
|
|
@@ -399,24 +471,35 @@ export async function cliIpcCallStream(
|
|
|
399
471
|
: "Connection closed before response",
|
|
400
472
|
});
|
|
401
473
|
} else if (streamController) {
|
|
402
|
-
streamController.error(
|
|
474
|
+
streamController.error(
|
|
475
|
+
new Error("Connection closed before stream ended"),
|
|
476
|
+
);
|
|
403
477
|
streamController = undefined;
|
|
404
478
|
}
|
|
405
479
|
});
|
|
406
480
|
|
|
407
481
|
const reqId = crypto.randomUUID();
|
|
408
482
|
|
|
409
|
-
opts?.signal?.addEventListener(
|
|
483
|
+
opts?.signal?.addEventListener(
|
|
484
|
+
"abort",
|
|
485
|
+
() => {
|
|
486
|
+
abort();
|
|
487
|
+
},
|
|
488
|
+
{ once: true },
|
|
489
|
+
);
|
|
410
490
|
|
|
411
491
|
const reader = new IpcFrameReader(
|
|
412
492
|
(envelope) => {
|
|
413
493
|
// Non-streaming envelope with error (e.g. method not found, auth failure)
|
|
414
494
|
if (envelope.id !== reqId) return;
|
|
415
495
|
const msg = envelope as IpcResponse;
|
|
416
|
-
finishError({
|
|
496
|
+
finishError({
|
|
497
|
+
ok: false,
|
|
498
|
+
error: msg.error ?? "Unexpected non-streaming response",
|
|
417
499
|
...(msg.statusCode != null && { statusCode: msg.statusCode }),
|
|
418
500
|
...(msg.errorCode != null && { errorCode: msg.errorCode }),
|
|
419
|
-
...(msg.errorDetails != null && { errorDetails: msg.errorDetails })
|
|
501
|
+
...(msg.errorDetails != null && { errorDetails: msg.errorDetails }),
|
|
502
|
+
});
|
|
420
503
|
},
|
|
421
504
|
(err) => finishError({ ok: false, error: err.message }),
|
|
422
505
|
{
|
|
@@ -437,7 +520,12 @@ export async function cliIpcCallStream(
|
|
|
437
520
|
});
|
|
438
521
|
settled = true;
|
|
439
522
|
clearTimeout(connectTimer);
|
|
440
|
-
resolve({
|
|
523
|
+
resolve({
|
|
524
|
+
ok: true,
|
|
525
|
+
headers: (envelope.headers ?? {}) as Record<string, string>,
|
|
526
|
+
body,
|
|
527
|
+
abort,
|
|
528
|
+
});
|
|
441
529
|
},
|
|
442
530
|
onStreamChunk: (chunk) => {
|
|
443
531
|
streamController?.enqueue(chunk);
|
|
@@ -455,8 +543,14 @@ export async function cliIpcCallStream(
|
|
|
455
543
|
writeMessage(socket, { id: reqId, method, params });
|
|
456
544
|
|
|
457
545
|
firstByteTimer = setTimeout(() => {
|
|
458
|
-
log.debug(
|
|
459
|
-
|
|
546
|
+
log.debug(
|
|
547
|
+
{ method, socketPath, timeoutMs: firstByteTimeoutMs },
|
|
548
|
+
"CLI IPC stream first-byte timeout",
|
|
549
|
+
);
|
|
550
|
+
finishError({
|
|
551
|
+
ok: false,
|
|
552
|
+
error: "Stream timed out waiting for first byte",
|
|
553
|
+
});
|
|
460
554
|
}, firstByteTimeoutMs);
|
|
461
555
|
|
|
462
556
|
socket.on("data", (chunk) => {
|
|
@@ -491,13 +585,21 @@ export function exitFromIpcResult(
|
|
|
491
585
|
_cmd?: unknown,
|
|
492
586
|
): never {
|
|
493
587
|
process.stderr.write((r.error ?? "Unknown error") + "\n");
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
588
|
+
process.exit(exitCodeFromIpcResult(r));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Map an IPC error result to its CLI process exit code without exiting.
|
|
593
|
+
*
|
|
594
|
+
* Use this when callers want to emit a structured response (e.g. a JSON
|
|
595
|
+
* error envelope in `--json` mode) before terminating with the same status
|
|
596
|
+
* code that {@link exitFromIpcResult} would produce.
|
|
597
|
+
*
|
|
598
|
+
* Exit code matrix matches {@link exitFromIpcResult}.
|
|
599
|
+
*/
|
|
600
|
+
export function exitCodeFromIpcResult(r: { statusCode?: number }): number {
|
|
601
|
+
if (r.statusCode === undefined) return 10;
|
|
602
|
+
if (r.statusCode >= 500) return 3;
|
|
603
|
+
if (r.statusCode >= 400) return 2;
|
|
604
|
+
return 1;
|
|
503
605
|
}
|
|
@@ -14,7 +14,9 @@ import {
|
|
|
14
14
|
buildExcerpt,
|
|
15
15
|
buildRecallEvidenceExcerpt,
|
|
16
16
|
countConversations,
|
|
17
|
+
getMessageRoleStatsByConversation,
|
|
17
18
|
listConversations,
|
|
19
|
+
listConversationsBySource,
|
|
18
20
|
} from "../conversation-queries.js";
|
|
19
21
|
import { getDb } from "../db-connection.js";
|
|
20
22
|
import { initializeDb } from "../db-init.js";
|
|
@@ -261,3 +263,221 @@ describe("listConversations", () => {
|
|
|
261
263
|
expect(fgList).toHaveLength(0);
|
|
262
264
|
});
|
|
263
265
|
});
|
|
266
|
+
|
|
267
|
+
describe("listConversationsBySource", () => {
|
|
268
|
+
beforeEach(() => {
|
|
269
|
+
resetTables();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
test("returns only conversations whose source matches exactly", () => {
|
|
273
|
+
createConversation({
|
|
274
|
+
title: "consol-1",
|
|
275
|
+
source: "memory_v2_consolidation",
|
|
276
|
+
});
|
|
277
|
+
createConversation({
|
|
278
|
+
title: "consol-2",
|
|
279
|
+
source: "memory_v2_consolidation",
|
|
280
|
+
});
|
|
281
|
+
createConversation({ title: "heartbeat-1", source: "heartbeat" });
|
|
282
|
+
createConversation({ title: "user-1", source: "user" });
|
|
283
|
+
|
|
284
|
+
const results = listConversationsBySource("memory_v2_consolidation");
|
|
285
|
+
|
|
286
|
+
expect(results).toHaveLength(2);
|
|
287
|
+
expect(results.map((r) => r.title).sort()).toEqual([
|
|
288
|
+
"consol-1",
|
|
289
|
+
"consol-2",
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("orders by createdAt descending", () => {
|
|
294
|
+
const a = createConversation({
|
|
295
|
+
title: "a",
|
|
296
|
+
source: "memory_v2_consolidation",
|
|
297
|
+
});
|
|
298
|
+
const b = createConversation({
|
|
299
|
+
title: "b",
|
|
300
|
+
source: "memory_v2_consolidation",
|
|
301
|
+
});
|
|
302
|
+
const c = createConversation({
|
|
303
|
+
title: "c",
|
|
304
|
+
source: "memory_v2_consolidation",
|
|
305
|
+
});
|
|
306
|
+
// Force distinct createdAt regardless of ms-clock granularity.
|
|
307
|
+
rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 1000, a.id);
|
|
308
|
+
rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 3000, b.id);
|
|
309
|
+
rawRun("UPDATE conversations SET created_at = ? WHERE id = ?", 2000, c.id);
|
|
310
|
+
|
|
311
|
+
const results = listConversationsBySource("memory_v2_consolidation");
|
|
312
|
+
|
|
313
|
+
expect(results.map((r) => r.id)).toEqual([b.id, c.id, a.id]);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("honors the limit parameter", () => {
|
|
317
|
+
for (let i = 0; i < 5; i++) {
|
|
318
|
+
createConversation({
|
|
319
|
+
title: `consol-${i}`,
|
|
320
|
+
source: "memory_v2_consolidation",
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const results = listConversationsBySource("memory_v2_consolidation", 3);
|
|
325
|
+
|
|
326
|
+
expect(results).toHaveLength(3);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("includes archived rows by default", () => {
|
|
330
|
+
const conv = createConversation({
|
|
331
|
+
title: "archived",
|
|
332
|
+
source: "memory_v2_consolidation",
|
|
333
|
+
});
|
|
334
|
+
rawRun(
|
|
335
|
+
"UPDATE conversations SET archived_at = ? WHERE id = ?",
|
|
336
|
+
Date.now(),
|
|
337
|
+
conv.id,
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
const results = listConversationsBySource("memory_v2_consolidation");
|
|
341
|
+
|
|
342
|
+
expect(results).toHaveLength(1);
|
|
343
|
+
expect(results[0]!.archivedAt).not.toBeNull();
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("excludes archived rows when includeArchived is false", () => {
|
|
347
|
+
const archived = createConversation({
|
|
348
|
+
title: "archived",
|
|
349
|
+
source: "memory_v2_consolidation",
|
|
350
|
+
});
|
|
351
|
+
createConversation({ title: "live", source: "memory_v2_consolidation" });
|
|
352
|
+
rawRun(
|
|
353
|
+
"UPDATE conversations SET archived_at = ? WHERE id = ?",
|
|
354
|
+
Date.now(),
|
|
355
|
+
archived.id,
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
const results = listConversationsBySource("memory_v2_consolidation", 20, {
|
|
359
|
+
includeArchived: false,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
expect(results).toHaveLength(1);
|
|
363
|
+
expect(results[0]!.title).toBe("live");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("does not apply the subagent-exclusion that listConversations does", () => {
|
|
367
|
+
// The defensive `source != 'subagent'` carve-out in listConversations is
|
|
368
|
+
// a foreground/background bucketing concern. A caller asking for the
|
|
369
|
+
// exact `subagent` source via this query gets exactly that.
|
|
370
|
+
createConversation({ title: "sub-1", source: "subagent" });
|
|
371
|
+
|
|
372
|
+
const results = listConversationsBySource("subagent");
|
|
373
|
+
|
|
374
|
+
expect(results).toHaveLength(1);
|
|
375
|
+
expect(results[0]!.title).toBe("sub-1");
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
describe("getMessageRoleStatsByConversation", () => {
|
|
380
|
+
beforeEach(() => {
|
|
381
|
+
resetTables();
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
function insertMessage(
|
|
385
|
+
conversationId: string,
|
|
386
|
+
role: string,
|
|
387
|
+
createdAt: number,
|
|
388
|
+
): void {
|
|
389
|
+
rawRun(
|
|
390
|
+
"INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES (?, ?, ?, ?, ?)",
|
|
391
|
+
`msg-${conversationId}-${role}-${createdAt}`,
|
|
392
|
+
conversationId,
|
|
393
|
+
role,
|
|
394
|
+
"x",
|
|
395
|
+
createdAt,
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
test("returns empty map for empty input", () => {
|
|
400
|
+
const result = getMessageRoleStatsByConversation([]);
|
|
401
|
+
expect(result.size).toBe(0);
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
test("returns empty map when conversations exist but no matching role", () => {
|
|
405
|
+
const a = createConversation("a");
|
|
406
|
+
insertMessage(a.id, "user", 1000);
|
|
407
|
+
|
|
408
|
+
const result = getMessageRoleStatsByConversation([a.id], "assistant");
|
|
409
|
+
|
|
410
|
+
expect(result.size).toBe(0);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("counts assistant messages and returns max createdAt", () => {
|
|
414
|
+
const a = createConversation("a");
|
|
415
|
+
insertMessage(a.id, "user", 1000);
|
|
416
|
+
insertMessage(a.id, "assistant", 1500);
|
|
417
|
+
insertMessage(a.id, "assistant", 2500);
|
|
418
|
+
insertMessage(a.id, "assistant", 2000);
|
|
419
|
+
|
|
420
|
+
const result = getMessageRoleStatsByConversation([a.id], "assistant");
|
|
421
|
+
|
|
422
|
+
expect(result.size).toBe(1);
|
|
423
|
+
expect(result.get(a.id)).toEqual({ count: 3, lastAt: 2500 });
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("does not count messages from other roles", () => {
|
|
427
|
+
const a = createConversation("a");
|
|
428
|
+
insertMessage(a.id, "user", 1000);
|
|
429
|
+
insertMessage(a.id, "user", 2000);
|
|
430
|
+
insertMessage(a.id, "system", 1500);
|
|
431
|
+
insertMessage(a.id, "assistant", 3000);
|
|
432
|
+
|
|
433
|
+
const result = getMessageRoleStatsByConversation([a.id], "assistant");
|
|
434
|
+
|
|
435
|
+
expect(result.get(a.id)).toEqual({ count: 1, lastAt: 3000 });
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("scopes to the supplied conversation ids", () => {
|
|
439
|
+
const a = createConversation("a");
|
|
440
|
+
const b = createConversation("b");
|
|
441
|
+
insertMessage(a.id, "assistant", 1000);
|
|
442
|
+
insertMessage(b.id, "assistant", 2000);
|
|
443
|
+
|
|
444
|
+
const result = getMessageRoleStatsByConversation([a.id], "assistant");
|
|
445
|
+
|
|
446
|
+
expect(result.size).toBe(1);
|
|
447
|
+
expect(result.has(a.id)).toBe(true);
|
|
448
|
+
expect(result.has(b.id)).toBe(false);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("aggregates per-conversation across many ids in a single query", () => {
|
|
452
|
+
const a = createConversation("a");
|
|
453
|
+
const b = createConversation("b");
|
|
454
|
+
const c = createConversation("c");
|
|
455
|
+
insertMessage(a.id, "assistant", 1000);
|
|
456
|
+
insertMessage(a.id, "assistant", 1500);
|
|
457
|
+
insertMessage(b.id, "assistant", 2000);
|
|
458
|
+
// c has no assistant messages → absent from the result.
|
|
459
|
+
|
|
460
|
+
const result = getMessageRoleStatsByConversation(
|
|
461
|
+
[a.id, b.id, c.id],
|
|
462
|
+
"assistant",
|
|
463
|
+
);
|
|
464
|
+
|
|
465
|
+
expect(result.size).toBe(2);
|
|
466
|
+
expect(result.get(a.id)).toEqual({ count: 2, lastAt: 1500 });
|
|
467
|
+
expect(result.get(b.id)).toEqual({ count: 1, lastAt: 2000 });
|
|
468
|
+
expect(result.has(c.id)).toBe(false);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("role parameter selects the counted role (defaults to assistant)", () => {
|
|
472
|
+
const a = createConversation("a");
|
|
473
|
+
insertMessage(a.id, "user", 1000);
|
|
474
|
+
insertMessage(a.id, "user", 2000);
|
|
475
|
+
insertMessage(a.id, "assistant", 1500);
|
|
476
|
+
|
|
477
|
+
const assistants = getMessageRoleStatsByConversation([a.id]);
|
|
478
|
+
expect(assistants.get(a.id)).toEqual({ count: 1, lastAt: 1500 });
|
|
479
|
+
|
|
480
|
+
const users = getMessageRoleStatsByConversation([a.id], "user");
|
|
481
|
+
expect(users.get(a.id)).toEqual({ count: 2, lastAt: 2000 });
|
|
482
|
+
});
|
|
483
|
+
});
|
|
@@ -11,26 +11,12 @@ mock.module("../../util/logger.js", () => ({
|
|
|
11
11
|
// Mock state — reset between tests.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
13
|
|
|
14
|
-
let flagEnabled = true;
|
|
15
14
|
let sourceTag: string | null = null;
|
|
16
|
-
let getConfigThrows = false;
|
|
17
15
|
const upsertCalls: Array<{
|
|
18
16
|
payload: { conversationId: string };
|
|
19
17
|
runAfter: number;
|
|
20
18
|
}> = [];
|
|
21
19
|
|
|
22
|
-
mock.module("../../config/loader.js", () => ({
|
|
23
|
-
getConfig: () => {
|
|
24
|
-
if (getConfigThrows) throw new Error("boom");
|
|
25
|
-
return {};
|
|
26
|
-
},
|
|
27
|
-
}));
|
|
28
|
-
|
|
29
|
-
mock.module("../../config/assistant-feature-flags.js", () => ({
|
|
30
|
-
isAssistantFeatureFlagEnabled: (_key: string, _config: unknown) =>
|
|
31
|
-
flagEnabled,
|
|
32
|
-
}));
|
|
33
|
-
|
|
34
20
|
mock.module("../conversation-crud.js", () => ({
|
|
35
21
|
getConversationSource: (_id: string) => sourceTag,
|
|
36
22
|
}));
|
|
@@ -59,26 +45,11 @@ import {
|
|
|
59
45
|
|
|
60
46
|
describe("enqueueMemoryRetrospectiveIfEnabled", () => {
|
|
61
47
|
beforeEach(() => {
|
|
62
|
-
flagEnabled = true;
|
|
63
48
|
sourceTag = null;
|
|
64
|
-
getConfigThrows = false;
|
|
65
49
|
upsertCalls.length = 0;
|
|
66
50
|
});
|
|
67
51
|
|
|
68
|
-
test("
|
|
69
|
-
flagEnabled = false;
|
|
70
|
-
for (const trigger of [
|
|
71
|
-
"interval",
|
|
72
|
-
"message_count",
|
|
73
|
-
"compaction",
|
|
74
|
-
"lifecycle",
|
|
75
|
-
] as const) {
|
|
76
|
-
enqueueMemoryRetrospectiveIfEnabled({ conversationId: "c1", trigger });
|
|
77
|
-
}
|
|
78
|
-
expect(upsertCalls).toHaveLength(0);
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("flag on, standard source — interval trigger enqueues with runAfter ≈ now", () => {
|
|
52
|
+
test("standard source — interval trigger enqueues with runAfter ≈ now", () => {
|
|
82
53
|
const before = Date.now();
|
|
83
54
|
enqueueMemoryRetrospectiveIfEnabled({
|
|
84
55
|
conversationId: "c1",
|
|
@@ -113,17 +84,6 @@ describe("enqueueMemoryRetrospectiveIfEnabled", () => {
|
|
|
113
84
|
});
|
|
114
85
|
expect(upsertCalls).toHaveLength(0);
|
|
115
86
|
});
|
|
116
|
-
|
|
117
|
-
test("getConfig throws — no enqueue, no propagation", () => {
|
|
118
|
-
getConfigThrows = true;
|
|
119
|
-
expect(() =>
|
|
120
|
-
enqueueMemoryRetrospectiveIfEnabled({
|
|
121
|
-
conversationId: "c1",
|
|
122
|
-
trigger: "interval",
|
|
123
|
-
}),
|
|
124
|
-
).not.toThrow();
|
|
125
|
-
expect(upsertCalls).toHaveLength(0);
|
|
126
|
-
});
|
|
127
87
|
});
|
|
128
88
|
|
|
129
89
|
describe("isMemoryRetrospectiveConversation", () => {
|
|
@@ -149,9 +109,7 @@ describe("isMemoryRetrospectiveConversation", () => {
|
|
|
149
109
|
|
|
150
110
|
describe("enqueueMemoryRetrospectiveOnCompaction", () => {
|
|
151
111
|
beforeEach(() => {
|
|
152
|
-
flagEnabled = true;
|
|
153
112
|
sourceTag = null;
|
|
154
|
-
getConfigThrows = false;
|
|
155
113
|
upsertCalls.length = 0;
|
|
156
114
|
});
|
|
157
115
|
|
|
@@ -162,16 +120,10 @@ describe("enqueueMemoryRetrospectiveOnCompaction", () => {
|
|
|
162
120
|
expect(upsertCalls).toHaveLength(0);
|
|
163
121
|
});
|
|
164
122
|
|
|
165
|
-
test("guardian trust
|
|
123
|
+
test("guardian trust — enqueues with compaction debounce", () => {
|
|
166
124
|
const before = Date.now();
|
|
167
125
|
enqueueMemoryRetrospectiveOnCompaction("c1", "guardian");
|
|
168
126
|
expect(upsertCalls).toHaveLength(1);
|
|
169
127
|
expect(upsertCalls[0]!.runAfter).toBeGreaterThan(before + 100);
|
|
170
128
|
});
|
|
171
|
-
|
|
172
|
-
test("guardian trust + flag off — no enqueue", () => {
|
|
173
|
-
flagEnabled = false;
|
|
174
|
-
enqueueMemoryRetrospectiveOnCompaction("c1", "guardian");
|
|
175
|
-
expect(upsertCalls).toHaveLength(0);
|
|
176
|
-
});
|
|
177
129
|
});
|