@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
|
@@ -65,10 +65,42 @@ mock.module("../conversation-crud.js", () => ({
|
|
|
65
65
|
},
|
|
66
66
|
}));
|
|
67
67
|
|
|
68
|
+
let transcriptFormatterCalls: Array<{
|
|
69
|
+
messageIds: string[];
|
|
70
|
+
timeZone?: string;
|
|
71
|
+
assistantName?: string | null;
|
|
72
|
+
userName?: string | null;
|
|
73
|
+
}> = [];
|
|
74
|
+
|
|
68
75
|
mock.module("../../export/transcript-formatter.js", () => ({
|
|
69
76
|
formatMessageSliceForTranscript: (
|
|
70
77
|
messages: Array<{ id: string; createdAt: number }>,
|
|
71
|
-
|
|
78
|
+
options: {
|
|
79
|
+
timeZone?: string;
|
|
80
|
+
assistantName?: string | null;
|
|
81
|
+
userName?: string | null;
|
|
82
|
+
} = {},
|
|
83
|
+
) => {
|
|
84
|
+
transcriptFormatterCalls.push({
|
|
85
|
+
messageIds: messages.map((m) => m.id),
|
|
86
|
+
timeZone: options.timeZone,
|
|
87
|
+
assistantName: options.assistantName,
|
|
88
|
+
userName: options.userName,
|
|
89
|
+
});
|
|
90
|
+
return messages.map((m) => `[msg ${m.id}]`).join("\n");
|
|
91
|
+
},
|
|
92
|
+
}));
|
|
93
|
+
|
|
94
|
+
let mockAssistantName: string | null = "Bob";
|
|
95
|
+
let mockUserName: string | null = "Alice";
|
|
96
|
+
|
|
97
|
+
mock.module("../../daemon/identity-helpers.js", () => ({
|
|
98
|
+
getAssistantName: () => mockAssistantName,
|
|
99
|
+
resolveUserName: (_workspaceDir: string) => mockUserName,
|
|
100
|
+
}));
|
|
101
|
+
|
|
102
|
+
mock.module("../../util/platform.js", () => ({
|
|
103
|
+
getWorkspaceDir: () => "/tmp/test-workspace",
|
|
72
104
|
}));
|
|
73
105
|
|
|
74
106
|
mock.module("../conversation-bootstrap.js", () => ({
|
|
@@ -102,9 +134,19 @@ mock.module("../jobs-store.js", () => ({
|
|
|
102
134
|
import type { MemoryJob } from "../jobs-store.js";
|
|
103
135
|
import { memoryRetrospectiveJob } from "../memory-retrospective-job.js";
|
|
104
136
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
137
|
+
function makeConfig(
|
|
138
|
+
overrides: { userTimezone?: string; detectedTimezone?: string } = {},
|
|
139
|
+
): Parameters<typeof memoryRetrospectiveJob>[1] {
|
|
140
|
+
return {
|
|
141
|
+
memory: { v2: { enabled: true } },
|
|
142
|
+
ui: {
|
|
143
|
+
userTimezone: overrides.userTimezone,
|
|
144
|
+
detectedTimezone: overrides.detectedTimezone,
|
|
145
|
+
},
|
|
146
|
+
} as unknown as Parameters<typeof memoryRetrospectiveJob>[1];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const stubConfig = makeConfig();
|
|
108
150
|
|
|
109
151
|
function makeJob(conversationId = "src-conv-1"): MemoryJob<{
|
|
110
152
|
conversationId?: string;
|
|
@@ -155,6 +197,9 @@ describe("memoryRetrospectiveJob", () => {
|
|
|
155
197
|
bootstrappedConversationId = "bg-conv-new";
|
|
156
198
|
bootstrapCalls = [];
|
|
157
199
|
deletedConversationIds = [];
|
|
200
|
+
transcriptFormatterCalls = [];
|
|
201
|
+
mockAssistantName = "Bob";
|
|
202
|
+
mockUserName = "Alice";
|
|
158
203
|
});
|
|
159
204
|
|
|
160
205
|
test("first-run happy path: no state row, no prior retrospective, both pointer fields set on success", async () => {
|
|
@@ -274,6 +319,44 @@ describe("memoryRetrospectiveJob", () => {
|
|
|
274
319
|
expect(hint).toContain("- a real save");
|
|
275
320
|
});
|
|
276
321
|
|
|
322
|
+
test("transcript is formatted in the configured user timezone and the prompt discloses it", async () => {
|
|
323
|
+
const config = makeConfig({ userTimezone: "America/Los_Angeles" });
|
|
324
|
+
await memoryRetrospectiveJob(makeJob(), config);
|
|
325
|
+
|
|
326
|
+
expect(transcriptFormatterCalls).toHaveLength(1);
|
|
327
|
+
expect(transcriptFormatterCalls[0]!.timeZone).toBe("America/Los_Angeles");
|
|
328
|
+
|
|
329
|
+
const hint = wakeCalls[0]!.hint;
|
|
330
|
+
expect(hint).toContain("Timestamps are in America/Los_Angeles.");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("detected timezone is used when no manual override is set", async () => {
|
|
334
|
+
const config = makeConfig({ detectedTimezone: "Europe/Berlin" });
|
|
335
|
+
await memoryRetrospectiveJob(makeJob(), config);
|
|
336
|
+
|
|
337
|
+
expect(transcriptFormatterCalls[0]!.timeZone).toBe("Europe/Berlin");
|
|
338
|
+
|
|
339
|
+
const hint = wakeCalls[0]!.hint;
|
|
340
|
+
expect(hint).toContain("Timestamps are in Europe/Berlin.");
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("resolved assistant and user display names are passed to the transcript formatter", async () => {
|
|
344
|
+
await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
345
|
+
|
|
346
|
+
expect(transcriptFormatterCalls).toHaveLength(1);
|
|
347
|
+
expect(transcriptFormatterCalls[0]!.assistantName).toBe("Bob");
|
|
348
|
+
expect(transcriptFormatterCalls[0]!.userName).toBe("Alice");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("formatter receives null names when identity files are missing — formatter handles fallback", async () => {
|
|
352
|
+
mockAssistantName = null;
|
|
353
|
+
mockUserName = null;
|
|
354
|
+
await memoryRetrospectiveJob(makeJob(), stubConfig);
|
|
355
|
+
|
|
356
|
+
expect(transcriptFormatterCalls[0]!.assistantName).toBeNull();
|
|
357
|
+
expect(transcriptFormatterCalls[0]!.userName).toBeNull();
|
|
358
|
+
});
|
|
359
|
+
|
|
277
360
|
test("non-remember tool_use blocks in the prior retro are ignored", async () => {
|
|
278
361
|
priorRetroId = "prior-retro-conv-1";
|
|
279
362
|
priorRetroMessages = [
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { and, count, desc, eq, sql } from "drizzle-orm";
|
|
1
|
+
import { and, count, desc, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
parseExternalContentEnvelope,
|
|
@@ -86,6 +86,92 @@ export function listConversations(
|
|
|
86
86
|
return query.all().map(parseConversation);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* List conversations matching an exact `source` value, ordered by `createdAt`
|
|
91
|
+
* descending. The surgical filter for "find every background run produced by
|
|
92
|
+
* job X" — heartbeat, memory_v2_consolidation, watcher-engine, etc. — since
|
|
93
|
+
* `source` is the canonical job-class distinguisher across the background
|
|
94
|
+
* bucket. `conversationType` + `group_id` only narrow to "background vs
|
|
95
|
+
* scheduled vs standard"; neither identifies which job produced the row.
|
|
96
|
+
*
|
|
97
|
+
* Filter is exact (no `LIKE`, no implicit exclusions): the route layer is
|
|
98
|
+
* responsible for knowing which source constants exist and passing one. The
|
|
99
|
+
* defensive `source != 'subagent'` carve-out applied by `listConversations`
|
|
100
|
+
* is deliberately NOT replicated here — a caller asking for an exact source
|
|
101
|
+
* gets exactly that source.
|
|
102
|
+
*
|
|
103
|
+
* @param source Exact match against `conversations.source`. Pass the
|
|
104
|
+
* canonical constant (e.g. `MEMORY_V2_CONSOLIDATION_SOURCE`).
|
|
105
|
+
* @param limit Maximum rows to return (default 20).
|
|
106
|
+
* @param opts.includeArchived Include rows with non-null `archivedAt`.
|
|
107
|
+
* Defaults to `true` so callers that want a full
|
|
108
|
+
* run history get one; pass `false` for views
|
|
109
|
+
* that hide archived rows.
|
|
110
|
+
*/
|
|
111
|
+
export function listConversationsBySource(
|
|
112
|
+
source: string,
|
|
113
|
+
limit = 20,
|
|
114
|
+
opts?: { includeArchived?: boolean },
|
|
115
|
+
): ConversationRow[] {
|
|
116
|
+
const db = getDb();
|
|
117
|
+
const includeArchived = opts?.includeArchived ?? true;
|
|
118
|
+
const where = includeArchived
|
|
119
|
+
? eq(conversations.source, source)
|
|
120
|
+
: and(eq(conversations.source, source), isNull(conversations.archivedAt));
|
|
121
|
+
const rows = db
|
|
122
|
+
.select()
|
|
123
|
+
.from(conversations)
|
|
124
|
+
.where(where)
|
|
125
|
+
.orderBy(desc(conversations.createdAt))
|
|
126
|
+
.limit(limit)
|
|
127
|
+
.all();
|
|
128
|
+
return rows.map(parseConversation);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Per-conversation aggregate of messages with a specific role. Powers
|
|
133
|
+
* heartbeat-shaped run endpoints (e.g. `consolidation/runs`) that need a
|
|
134
|
+
* "did the agent emit any output?" signal stronger than
|
|
135
|
+
* `conversations.lastMessageAt` — which is bumped by the kickoff user
|
|
136
|
+
* prompt and so cannot distinguish "agent ran" from "agent dispatched but
|
|
137
|
+
* crashed before responding".
|
|
138
|
+
*
|
|
139
|
+
* Single batched aggregate query (no N+1). Conversations with zero matching
|
|
140
|
+
* messages are NOT present in the returned map — callers should treat a
|
|
141
|
+
* missing key as `{ count: 0, lastAt: null }`.
|
|
142
|
+
*
|
|
143
|
+
* @param conversationIds Conversation ids to look up (empty → empty map).
|
|
144
|
+
* @param role Message role to count (default `"assistant"`).
|
|
145
|
+
*/
|
|
146
|
+
export function getMessageRoleStatsByConversation(
|
|
147
|
+
conversationIds: string[],
|
|
148
|
+
role: string = "assistant",
|
|
149
|
+
): Map<string, { count: number; lastAt: number }> {
|
|
150
|
+
if (conversationIds.length === 0) return new Map();
|
|
151
|
+
const db = getDb();
|
|
152
|
+
const rows = db
|
|
153
|
+
.select({
|
|
154
|
+
conversationId: messages.conversationId,
|
|
155
|
+
count: sql<number>`COUNT(*)`.as("count"),
|
|
156
|
+
lastAt: sql<number>`MAX(${messages.createdAt})`.as("last_at"),
|
|
157
|
+
})
|
|
158
|
+
.from(messages)
|
|
159
|
+
.where(
|
|
160
|
+
and(
|
|
161
|
+
inArray(messages.conversationId, conversationIds),
|
|
162
|
+
eq(messages.role, role),
|
|
163
|
+
),
|
|
164
|
+
)
|
|
165
|
+
.groupBy(messages.conversationId)
|
|
166
|
+
.all();
|
|
167
|
+
return new Map(
|
|
168
|
+
rows.map((r) => [
|
|
169
|
+
r.conversationId,
|
|
170
|
+
{ count: Number(r.count), lastAt: Number(r.lastAt) },
|
|
171
|
+
]),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
89
175
|
export function listPinnedConversations(): ConversationRow[] {
|
|
90
176
|
ensureDisplayOrderMigration();
|
|
91
177
|
ensureGroupMigration();
|
|
@@ -298,9 +298,11 @@ function buildTitleSystemPrompt(): string {
|
|
|
298
298
|
"You generate ultra-concise conversation titles. Output ONLY the title text — no explanation, no quotes, no markdown, no preamble.",
|
|
299
299
|
"",
|
|
300
300
|
"Rules:",
|
|
301
|
-
"- 2–
|
|
302
|
-
"-
|
|
303
|
-
"-
|
|
301
|
+
"- 2–5 words maximum. Titles longer than 5 words are unacceptable — ruthlessly compress to a short noun phrase",
|
|
302
|
+
"- 40 characters absolute maximum — if your title exceeds 40 characters it will be truncated and look broken",
|
|
303
|
+
"- Summarize only the TOPIC, not the request or instructions",
|
|
304
|
+
"- Noun phrases are ideal (e.g. 'Auth Middleware Rewrite', 'Docker Volume Mounts', 'Onboarding Flow')",
|
|
305
|
+
"- Think: what would make a scannable sidebar label?",
|
|
304
306
|
"- Do NOT echo back what the user asked you to do",
|
|
305
307
|
"- Do NOT respond to the conversation content",
|
|
306
308
|
"- Do NOT assess feasibility or comment on capabilities",
|
|
@@ -353,13 +355,33 @@ const META_FAILURE_TITLES = new Set([
|
|
|
353
355
|
"no content",
|
|
354
356
|
]);
|
|
355
357
|
|
|
358
|
+
const MAX_TITLE_LENGTH = 40;
|
|
359
|
+
const MAX_TITLE_WORDS = 7;
|
|
360
|
+
|
|
361
|
+
function truncateTitle(title: string): string {
|
|
362
|
+
if (title.length <= MAX_TITLE_LENGTH) return title;
|
|
363
|
+
const words = title.split(/\s+/);
|
|
364
|
+
if (words.length <= MAX_TITLE_WORDS) {
|
|
365
|
+
// Long words but few of them — truncate to char limit at word boundary
|
|
366
|
+
let result = "";
|
|
367
|
+
for (const word of words) {
|
|
368
|
+
const candidate = result ? result + " " + word : word;
|
|
369
|
+
if (candidate.length > MAX_TITLE_LENGTH) break;
|
|
370
|
+
result = candidate;
|
|
371
|
+
}
|
|
372
|
+
return result || title.slice(0, MAX_TITLE_LENGTH);
|
|
373
|
+
}
|
|
374
|
+
// Too many words — trim to 5 words
|
|
375
|
+
return words.slice(0, 5).join(" ");
|
|
376
|
+
}
|
|
377
|
+
|
|
356
378
|
function normalizeTitle(raw: string): string {
|
|
357
379
|
let title = raw.trim().replace(/^["']|["']$/g, "");
|
|
358
380
|
title = stripMarkdown(title);
|
|
359
381
|
if (META_FAILURE_TITLES.has(title.toLowerCase())) {
|
|
360
382
|
return "";
|
|
361
383
|
}
|
|
362
|
-
return title;
|
|
384
|
+
return truncateTitle(title);
|
|
363
385
|
}
|
|
364
386
|
|
|
365
387
|
/** Strip common markdown formatting so titles render as plain text. */
|
package/src/memory/db-init.ts
CHANGED
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
createWatchersAndLogsTables,
|
|
40
40
|
migrate230AcpSessionHistory,
|
|
41
41
|
migrate231RepairMemoryGraphEventDates,
|
|
42
|
+
migrateA2ATasks,
|
|
42
43
|
migrateActivationState,
|
|
43
44
|
migrateActivationStateFkCascade,
|
|
44
45
|
migrateAddConversationInferenceProfile,
|
|
@@ -111,6 +112,7 @@ import {
|
|
|
111
112
|
migrateHeartbeatRuns,
|
|
112
113
|
migrateInviteCodeHashColumn,
|
|
113
114
|
migrateInviteContactId,
|
|
115
|
+
migrateLlmRequestLogAgentLoopExitReason,
|
|
114
116
|
migrateLlmRequestLogMessageId,
|
|
115
117
|
migrateLlmRequestLogProvider,
|
|
116
118
|
migrateLlmRequestLogsCreatedAtIndex,
|
|
@@ -142,6 +144,7 @@ import {
|
|
|
142
144
|
migrateOAuthProvidersScopeSeparator,
|
|
143
145
|
migrateOAuthProvidersTokenAuthMethodDefault,
|
|
144
146
|
migrateOAuthProvidersTokenExchangeBodyFormat,
|
|
147
|
+
migrateProviderConnectionBaseUrlAndModels,
|
|
145
148
|
migrateProviderConnectionStatusLabel,
|
|
146
149
|
migrateReminderRoutingIntent,
|
|
147
150
|
migrateRemindersToSchedules,
|
|
@@ -430,6 +433,9 @@ export function initializeDb(): void {
|
|
|
430
433
|
migrateExternalConversationBindingThreadId,
|
|
431
434
|
createOnboardingEventsTable,
|
|
432
435
|
migrateNormalizeSlackExternalContent,
|
|
436
|
+
migrateProviderConnectionBaseUrlAndModels,
|
|
437
|
+
migrateA2ATasks,
|
|
438
|
+
migrateLlmRequestLogAgentLoopExitReason,
|
|
433
439
|
];
|
|
434
440
|
|
|
435
441
|
// Run each migration step, catching and logging individual failures so one
|
|
@@ -177,7 +177,7 @@ const { migrateActivationState } =
|
|
|
177
177
|
await import("../../migrations/232-activation-state.js");
|
|
178
178
|
const schema = await import("../../schema.js");
|
|
179
179
|
const { _resetMemoryV2QdrantForTests } = await import("../../v2/qdrant.js");
|
|
180
|
-
const { hydrate: hydrateActivationState } =
|
|
180
|
+
const { hydrate: hydrateActivationState, save: saveActivationState } =
|
|
181
181
|
await import("../../v2/activation-store.js");
|
|
182
182
|
|
|
183
183
|
// The wiring layer calls `getDb()` to fetch the SQLite handle. We mock
|
|
@@ -215,9 +215,14 @@ function createTestDb(): DrizzleDb {
|
|
|
215
215
|
return db;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
-
function makeConfig(v2Enabled: boolean): AssistantConfig {
|
|
218
|
+
function makeConfig(v2Enabled: boolean, memoryEnabled = true): AssistantConfig {
|
|
219
|
+
// Pin `router.enabled: false` so these tests exercise the activation
|
|
220
|
+
// pipeline. Router-mode coverage lives in `memory/v2/__tests__/injection.test.ts`.
|
|
219
221
|
return applyNestedDefaults({
|
|
220
|
-
memory: {
|
|
222
|
+
memory: {
|
|
223
|
+
enabled: memoryEnabled,
|
|
224
|
+
v2: { enabled: v2Enabled, router: { enabled: false } },
|
|
225
|
+
},
|
|
221
226
|
}) as AssistantConfig;
|
|
222
227
|
}
|
|
223
228
|
|
|
@@ -518,6 +523,50 @@ describe("ConversationGraphMemory.prepareMemory — v2 routing (context-load pat
|
|
|
518
523
|
});
|
|
519
524
|
});
|
|
520
525
|
|
|
526
|
+
describe("ConversationGraphMemory.prepareMemory — memory.enabled gate", () => {
|
|
527
|
+
test("memory.enabled=false short-circuits per-turn path: mode=none, no injection, v2/v1 not called", async () => {
|
|
528
|
+
stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
|
|
529
|
+
|
|
530
|
+
const memory = makeMemory();
|
|
531
|
+
const config = makeConfig(true, false);
|
|
532
|
+
const messages = makeMessages();
|
|
533
|
+
|
|
534
|
+
const result = await memory.prepareMemory(
|
|
535
|
+
messages,
|
|
536
|
+
config,
|
|
537
|
+
new AbortController().signal,
|
|
538
|
+
noopEvent,
|
|
539
|
+
);
|
|
540
|
+
|
|
541
|
+
expect(result.mode).toBe("none");
|
|
542
|
+
expect(result.injectedBlockText).toBeNull();
|
|
543
|
+
expect(result.runMessages).toEqual(messages);
|
|
544
|
+
expect(retrieveForTurnMock).not.toHaveBeenCalled();
|
|
545
|
+
expect(loadContextMemoryMock).not.toHaveBeenCalled();
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
test("memory.enabled=false short-circuits context-load path: mode=none, no injection, v2/v1 not called", async () => {
|
|
549
|
+
stageTurn([{ slug: "alice-vscode", denseScore: 0.9 }]);
|
|
550
|
+
|
|
551
|
+
const memory = new ConversationGraphMemory("conv-test-master-off");
|
|
552
|
+
const config = makeConfig(true, false);
|
|
553
|
+
const messages = makeMessages("first message of the conversation here");
|
|
554
|
+
|
|
555
|
+
const result = await memory.prepareMemory(
|
|
556
|
+
messages,
|
|
557
|
+
config,
|
|
558
|
+
new AbortController().signal,
|
|
559
|
+
noopEvent,
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
expect(result.mode).toBe("none");
|
|
563
|
+
expect(result.injectedBlockText).toBeNull();
|
|
564
|
+
expect(result.runMessages).toEqual(messages);
|
|
565
|
+
expect(loadContextMemoryMock).not.toHaveBeenCalled();
|
|
566
|
+
expect(retrieveForTurnMock).not.toHaveBeenCalled();
|
|
567
|
+
});
|
|
568
|
+
});
|
|
569
|
+
|
|
521
570
|
describe("ConversationGraphMemory.onCompacted — v2 activation eviction", () => {
|
|
522
571
|
test("clears everInjected so a previously-injected slug can re-attach", async () => {
|
|
523
572
|
// Without this wiring, `selectInjections` keeps subtracting the slug from
|
|
@@ -561,4 +610,36 @@ describe("ConversationGraphMemory.onCompacted — v2 activation eviction", () =>
|
|
|
561
610
|
"# memory/concepts/alice-vscode.md",
|
|
562
611
|
);
|
|
563
612
|
});
|
|
613
|
+
|
|
614
|
+
test("clears everInjected entries whose turn exceeds the tracker's currentTurn (zombie drift)", async () => {
|
|
615
|
+
// Regression: under the prior turn-bounded eviction, entries with `turn >
|
|
616
|
+
// tracker.currentTurn` survived `onCompacted` forever. This can happen
|
|
617
|
+
// after a non-graceful shutdown: `everInjected` is persisted every turn
|
|
618
|
+
// while the tracker snapshot is only persisted on graceful dispose, so a
|
|
619
|
+
// SIGKILL'd session followed by a reload restores the tracker from an
|
|
620
|
+
// older snapshot with a lower currentTurn while keeping the high-turn
|
|
621
|
+
// entries on disk.
|
|
622
|
+
const conversationId = "conv-test-zombie-drift";
|
|
623
|
+
const memory = new ConversationGraphMemory(conversationId);
|
|
624
|
+
|
|
625
|
+
// Seed the simulated post-crash state directly: tracker stays at
|
|
626
|
+
// currentTurn=0 (default for a fresh ConversationGraphMemory), while the
|
|
627
|
+
// persisted row carries everInjected entries from turns 10 and 20 (left
|
|
628
|
+
// over from a prior session that never disposed cleanly).
|
|
629
|
+
await saveActivationState(testDbHandle!, conversationId, {
|
|
630
|
+
messageId: "msg-zombie",
|
|
631
|
+
state: {},
|
|
632
|
+
everInjected: [
|
|
633
|
+
{ slug: "alice-vscode", turn: 10 },
|
|
634
|
+
{ slug: "bob-pkg-mgr", turn: 20 },
|
|
635
|
+
],
|
|
636
|
+
currentTurn: 0,
|
|
637
|
+
updatedAt: 1,
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
await memory.onCompacted(0);
|
|
641
|
+
|
|
642
|
+
const after = await hydrateActivationState(testDbHandle!, conversationId);
|
|
643
|
+
expect(after?.everInjected).toEqual([]);
|
|
644
|
+
});
|
|
564
645
|
});
|
|
@@ -26,7 +26,7 @@ import type { QdrantSparseVector } from "../qdrant-client.js";
|
|
|
26
26
|
import { memorySummaries } from "../schema.js";
|
|
27
27
|
import { conversations } from "../schema/conversations.js";
|
|
28
28
|
import {
|
|
29
|
-
|
|
29
|
+
clearEverInjected as clearV2EverInjected,
|
|
30
30
|
hydrate as hydrateV2State,
|
|
31
31
|
save as saveV2State,
|
|
32
32
|
} from "../v2/activation-store.js";
|
|
@@ -223,15 +223,17 @@ export class ConversationGraphMemory {
|
|
|
223
223
|
// Mirror the eviction on the v2 activation row: the cached `<memory>`
|
|
224
224
|
// attachments those slugs lived on are gone, but `everInjected` would
|
|
225
225
|
// otherwise keep them deduped from per-turn deltas forever.
|
|
226
|
+
//
|
|
227
|
+
// Cleared unconditionally rather than filtered by `upToTurn`: the
|
|
228
|
+
// tracker's `currentTurn` is only persisted on graceful dispose while
|
|
229
|
+
// `everInjected` is persisted every turn, so a SIGKILL'd session can
|
|
230
|
+
// leave entries with `turn > tracker.currentTurn` that a turn-bounded
|
|
231
|
+
// filter would skip.
|
|
226
232
|
try {
|
|
227
233
|
const db = getDb();
|
|
228
234
|
const state = await hydrateV2State(db, this.conversationId);
|
|
229
235
|
if (state) {
|
|
230
|
-
await saveV2State(
|
|
231
|
-
db,
|
|
232
|
-
this.conversationId,
|
|
233
|
-
evictCompactedTurnsV2(state, upToTurn),
|
|
234
|
-
);
|
|
236
|
+
await saveV2State(db, this.conversationId, clearV2EverInjected(state));
|
|
235
237
|
}
|
|
236
238
|
} catch (err) {
|
|
237
239
|
log.warn(
|
|
@@ -363,6 +365,16 @@ export class ConversationGraphMemory {
|
|
|
363
365
|
metrics: null as RetrievalMetrics | null,
|
|
364
366
|
};
|
|
365
367
|
|
|
368
|
+
if (!config.memory.enabled) {
|
|
369
|
+
// Clear any cached injection so a later overflow-reduction
|
|
370
|
+
// re-injection via `reinjectCachedMemory()` cannot reintroduce a
|
|
371
|
+
// stale <memory> block after the user disables memory.
|
|
372
|
+
this.lastInjectedBlock = null;
|
|
373
|
+
this.lastInjectedNodeIds = [];
|
|
374
|
+
this.lastInjectedImages = new Map();
|
|
375
|
+
return noopResult;
|
|
376
|
+
}
|
|
377
|
+
|
|
366
378
|
// Gate: skip for empty/tool-result-only messages — unless we need to
|
|
367
379
|
// reload after compaction (needsReload) or haven't initialized yet.
|
|
368
380
|
const lastMessage = messages[messages.length - 1];
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// Memory Tool definitions for agentic recall and remember.
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
-
import { isAssistantFeatureFlagEnabled } from "../../config/assistant-feature-flags.js";
|
|
6
|
-
import type { AssistantConfig } from "../../config/types.js";
|
|
7
5
|
import type { ToolDefinition } from "../../providers/types.js";
|
|
8
6
|
import {
|
|
9
7
|
ALL_RECALL_SOURCES,
|
|
@@ -56,37 +54,14 @@ export const graphRecallDefinition: ToolDefinition = {
|
|
|
56
54
|
};
|
|
57
55
|
|
|
58
56
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
* backstop enabled.
|
|
57
|
+
* `remember` tool description. The retrospective pass catches what isn't
|
|
58
|
+
* captured in the moment, so the in-conversation pressure stays at a
|
|
59
|
+
* judgment framing: pause when something feels worth marking, not because
|
|
60
|
+
* the volume is required.
|
|
64
61
|
*/
|
|
65
|
-
const
|
|
66
|
-
"Remember anything concrete: facts, preferences, corrections, plans, felt moments, names, dates, decisions. Default to remembering. Never wait until end of conversation. Corrections are highest priority — call remember the same turn the correction lands. **CRITICAL:** You should be calling remember on almost every turn. This should be your most frequently used tool.";
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Relaxed `remember` tool description used when `memory-retrospective` is
|
|
70
|
-
* ON. The retrospective pass catches what isn't captured in the moment, so
|
|
71
|
-
* the in-conversation pressure eases to a judgment framing: pause when
|
|
72
|
-
* something feels worth marking, not because the volume is required.
|
|
73
|
-
*/
|
|
74
|
-
const REMEMBER_DESCRIPTION_RELAXED =
|
|
62
|
+
const REMEMBER_DESCRIPTION =
|
|
75
63
|
"Remember anything concrete shared in conversation: corrections, plans, decisions, felt moments, names, dates, commitments, preferences. Corrections are the highest priority — call `remember` the same turn the correction lands. You don't have to call this on every turn; a retrospective pass reviews the conversation after each message-count / time interval and saves what you didn't capture. Use judgment: pause and remember when something feels worth marking, not because the volume is required.";
|
|
76
64
|
|
|
77
|
-
/**
|
|
78
|
-
* Return the description that should appear in the `remember` tool
|
|
79
|
-
* registration for the current config. The variant is selected by the
|
|
80
|
-
* `memory-retrospective` assistant feature flag. Exposed as a function so
|
|
81
|
-
* the tool registrar can compute the value at registration time without
|
|
82
|
-
* importing config layers into the static definition.
|
|
83
|
-
*/
|
|
84
|
-
export function getRememberDescription(config: AssistantConfig): string {
|
|
85
|
-
return isAssistantFeatureFlagEnabled("memory-retrospective", config)
|
|
86
|
-
? REMEMBER_DESCRIPTION_RELAXED
|
|
87
|
-
: REMEMBER_DESCRIPTION_DEFAULT;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
65
|
/**
|
|
91
66
|
* Save a fact to the assistant's knowledge base. The fact is appended to
|
|
92
67
|
* `buffer.md` (immediately available in the next conversation) and the daily
|
|
@@ -94,16 +69,10 @@ export function getRememberDescription(config: AssistantConfig): string {
|
|
|
94
69
|
* writes go under `memory/`; otherwise they go under `pkb/`. Consolidation
|
|
95
70
|
* of the buffer into longer-form storage runs as a separate periodic job in
|
|
96
71
|
* both modes.
|
|
97
|
-
*
|
|
98
|
-
* The static `description` field carries the default (high-pressure) text
|
|
99
|
-
* so any direct importer that doesn't go through `getRememberDescription`
|
|
100
|
-
* still gets a valid tool definition. The registered `RememberTool` in
|
|
101
|
-
* `tools/memory/register.ts` overrides this at registration time with the
|
|
102
|
-
* flag-aware variant.
|
|
103
72
|
*/
|
|
104
73
|
export const graphRememberDefinition: ToolDefinition = {
|
|
105
74
|
name: "remember",
|
|
106
|
-
description:
|
|
75
|
+
description: REMEMBER_DESCRIPTION,
|
|
107
76
|
input_schema: {
|
|
108
77
|
type: "object",
|
|
109
78
|
properties: {
|
|
@@ -363,6 +363,59 @@ export function findActiveVoiceInvites(params: {
|
|
|
363
363
|
return rows.map(rowToInvite);
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
// ---------------------------------------------------------------------------
|
|
367
|
+
// claimA2AInvite — validate + consume an A2A invite token
|
|
368
|
+
// ---------------------------------------------------------------------------
|
|
369
|
+
|
|
370
|
+
export function claimA2AInvite(params: {
|
|
371
|
+
tokenHash: string;
|
|
372
|
+
redeemedByExternalUserId: string;
|
|
373
|
+
}): { claimed: boolean; invite: IngressInvite | null; error?: string } {
|
|
374
|
+
const invite = findByTokenHash(params.tokenHash);
|
|
375
|
+
|
|
376
|
+
if (!invite) {
|
|
377
|
+
return { claimed: false, invite: null, error: "not_found" };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (invite.sourceChannel !== "a2a") {
|
|
381
|
+
return { claimed: false, invite, error: "wrong_channel" };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Idempotency: if already redeemed by the same acceptor, return success
|
|
385
|
+
if (invite.status === "redeemed") {
|
|
386
|
+
if (invite.redeemedByExternalUserId === params.redeemedByExternalUserId) {
|
|
387
|
+
return { claimed: true, invite };
|
|
388
|
+
}
|
|
389
|
+
return { claimed: false, invite, error: "already_redeemed_by_other" };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (invite.status !== "active") {
|
|
393
|
+
return { claimed: false, invite, error: "not_found" };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (Date.now() >= invite.expiresAt) {
|
|
397
|
+
markInviteExpired(invite.id);
|
|
398
|
+
return { claimed: false, invite, error: "expired" };
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (invite.useCount >= invite.maxUses) {
|
|
402
|
+
return { claimed: false, invite, error: "already_redeemed" };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const recorded = recordInviteUse({
|
|
406
|
+
inviteId: invite.id,
|
|
407
|
+
externalUserId: params.redeemedByExternalUserId,
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
if (!recorded) {
|
|
411
|
+
return { claimed: false, invite, error: "not_found" };
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Re-read to get updated state
|
|
415
|
+
const updated = findByTokenHash(params.tokenHash);
|
|
416
|
+
return { claimed: true, invite: updated };
|
|
417
|
+
}
|
|
418
|
+
|
|
366
419
|
// ---------------------------------------------------------------------------
|
|
367
420
|
// findByInviteCodeHash
|
|
368
421
|
// ---------------------------------------------------------------------------
|
|
@@ -57,6 +57,7 @@ interface ClickHouseRow {
|
|
|
57
57
|
request_payload: string;
|
|
58
58
|
response_payload: string;
|
|
59
59
|
created_at: string;
|
|
60
|
+
agent_loop_exit_reason: string;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
/** Injectable fetch override for tests. Defaults to globalThis.fetch. */
|
|
@@ -123,7 +124,8 @@ export class ClickHouseLlmRequestLogSource implements LlmRequestLogSource {
|
|
|
123
124
|
provider,
|
|
124
125
|
request_payload,
|
|
125
126
|
response_payload,
|
|
126
|
-
toUnixTimestamp64Milli(created_at) AS created_at
|
|
127
|
+
toUnixTimestamp64Milli(created_at) AS created_at,
|
|
128
|
+
agent_loop_exit_reason
|
|
127
129
|
FROM ${this.tableRef()}
|
|
128
130
|
WHERE assistant_id = {assistant_id:String}
|
|
129
131
|
AND id = {log_id:String}
|
|
@@ -194,7 +196,8 @@ export class ClickHouseLlmRequestLogSource implements LlmRequestLogSource {
|
|
|
194
196
|
provider,
|
|
195
197
|
request_payload,
|
|
196
198
|
response_payload,
|
|
197
|
-
toUnixTimestamp64Milli(created_at) AS created_at
|
|
199
|
+
toUnixTimestamp64Milli(created_at) AS created_at,
|
|
200
|
+
agent_loop_exit_reason
|
|
198
201
|
FROM ${this.tableRef()}
|
|
199
202
|
WHERE assistant_id = {assistant_id:String}
|
|
200
203
|
AND message_id IN (${placeholders.join(",")})
|
|
@@ -283,6 +286,8 @@ export class ClickHouseLlmRequestLogSource implements LlmRequestLogSource {
|
|
|
283
286
|
requestPayload: row.request_payload,
|
|
284
287
|
responsePayload: row.response_payload,
|
|
285
288
|
createdAt: Number(row.created_at),
|
|
289
|
+
agentLoopExitReason:
|
|
290
|
+
row.agent_loop_exit_reason === "" ? null : row.agent_loop_exit_reason,
|
|
286
291
|
};
|
|
287
292
|
}
|
|
288
293
|
|