@vellumai/assistant 0.5.9 → 0.5.11

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.
Files changed (278) hide show
  1. package/AGENTS.md +9 -1
  2. package/ARCHITECTURE.md +48 -48
  3. package/Dockerfile +2 -0
  4. package/README.md +1 -1
  5. package/docs/architecture/integrations.md +6 -13
  6. package/docs/architecture/memory.md +7 -12
  7. package/docs/architecture/security.md +5 -5
  8. package/docs/credential-execution-service.md +9 -9
  9. package/docs/skills.md +1 -1
  10. package/node_modules/@vellumai/credential-storage/src/index.ts +2 -2
  11. package/node_modules/@vellumai/credential-storage/src/static-credentials.ts +1 -1
  12. package/openapi.yaml +7130 -0
  13. package/package.json +2 -1
  14. package/scripts/generate-openapi.ts +562 -0
  15. package/src/__tests__/acp-session.test.ts +239 -44
  16. package/src/__tests__/assistant-feature-flag-guard.test.ts +8 -8
  17. package/src/__tests__/assistant-feature-flag-guardrails.test.ts +5 -86
  18. package/src/__tests__/assistant-feature-flags-integration.test.ts +7 -14
  19. package/src/__tests__/browser-skill-endstate.test.ts +1 -1
  20. package/src/__tests__/btw-routes.test.ts +8 -0
  21. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +10 -10
  22. package/src/__tests__/channel-approvals.test.ts +7 -7
  23. package/src/__tests__/channel-readiness-service.test.ts +41 -0
  24. package/src/__tests__/config-schema.test.ts +10 -2
  25. package/src/__tests__/context-memory-e2e.test.ts +2 -6
  26. package/src/__tests__/conversation-skill-tools.test.ts +1 -3
  27. package/src/__tests__/conversation-title-service.test.ts +2 -15
  28. package/src/__tests__/credential-execution-feature-gates.test.ts +4 -8
  29. package/src/__tests__/credential-execution-managed-contract.test.ts +8 -8
  30. package/src/__tests__/credential-security-e2e.test.ts +4 -4
  31. package/src/__tests__/credential-security-invariants.test.ts +3 -3
  32. package/src/__tests__/credentials-cli.test.ts +3 -3
  33. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +1 -1
  34. package/src/__tests__/gateway-only-guard.test.ts +3 -0
  35. package/src/__tests__/heartbeat-service.test.ts +35 -0
  36. package/src/__tests__/host-shell-tool.test.ts +1 -1
  37. package/src/__tests__/inline-skill-load-permissions.test.ts +3 -3
  38. package/src/__tests__/llm-request-log-turn-query.test.ts +64 -0
  39. package/src/__tests__/log-export-workspace.test.ts +1 -1
  40. package/src/__tests__/mcp-client-auth.test.ts +1 -1
  41. package/src/__tests__/memory-lifecycle-e2e.test.ts +2 -2
  42. package/src/__tests__/memory-recall-log-store.test.ts +182 -0
  43. package/src/__tests__/memory-recall-quality.test.ts +6 -8
  44. package/src/__tests__/memory-regressions.test.ts +53 -42
  45. package/src/__tests__/memory-retrieval.benchmark.test.ts +5 -9
  46. package/src/__tests__/messaging-skill-split.test.ts +2 -17
  47. package/src/__tests__/oauth-cli.test.ts +98 -551
  48. package/src/__tests__/platform-callback-registration.test.ts +119 -0
  49. package/src/__tests__/secret-ingress-channel.test.ts +261 -0
  50. package/src/__tests__/secret-ingress-cli.test.ts +201 -0
  51. package/src/__tests__/secret-ingress-http.test.ts +312 -0
  52. package/src/__tests__/secret-ingress.test.ts +283 -0
  53. package/src/__tests__/secret-onetime-send.test.ts +4 -4
  54. package/src/__tests__/skill-feature-flags-integration.test.ts +4 -4
  55. package/src/__tests__/skill-feature-flags.test.ts +11 -19
  56. package/src/__tests__/skill-load-feature-flag.test.ts +1 -1
  57. package/src/__tests__/skill-load-inline-command.test.ts +3 -3
  58. package/src/__tests__/skill-load-inline-includes.test.ts +2 -2
  59. package/src/__tests__/skill-memory.test.ts +2 -4
  60. package/src/__tests__/skill-projection-feature-flag.test.ts +2 -4
  61. package/src/__tests__/skill-projection.benchmark.test.ts +1 -3
  62. package/src/__tests__/skills.test.ts +16 -2
  63. package/src/__tests__/slack-channel-config.test.ts +1 -1
  64. package/src/__tests__/slack-skill.test.ts +5 -69
  65. package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +1 -1
  66. package/src/__tests__/workspace-migration-015-migrate-credentials-to-keychain.test.ts +5 -238
  67. package/src/__tests__/workspace-migration-016-migrate-credentials-from-keychain.test.ts +5 -206
  68. package/src/__tests__/workspace-migration-018-rekey-compound-credential-keys.test.ts +181 -0
  69. package/src/__tests__/workspace-migrations-runner.test.ts +15 -7
  70. package/src/acp/client-handler.ts +113 -31
  71. package/src/acp/session-manager.ts +29 -27
  72. package/src/approvals/guardian-request-resolvers.ts +1 -1
  73. package/src/cli/AGENTS.md +73 -0
  74. package/src/cli/commands/autonomy.ts +3 -5
  75. package/src/cli/commands/credential-execution.ts +1 -2
  76. package/src/cli/commands/credentials.ts +4 -4
  77. package/src/cli/commands/memory.ts +2 -3
  78. package/src/cli/commands/oauth/__tests__/connect.test.ts +785 -0
  79. package/src/cli/commands/oauth/__tests__/disconnect.test.ts +760 -0
  80. package/src/cli/commands/oauth/__tests__/mode.test.ts +672 -0
  81. package/src/cli/commands/oauth/__tests__/ping.test.ts +690 -0
  82. package/src/cli/commands/oauth/__tests__/status.test.ts +579 -0
  83. package/src/cli/commands/oauth/__tests__/token.test.ts +467 -0
  84. package/src/cli/commands/oauth/apps.ts +29 -11
  85. package/src/cli/commands/oauth/connect.ts +373 -0
  86. package/src/cli/commands/oauth/connections.ts +14 -493
  87. package/src/cli/commands/oauth/disconnect.ts +333 -0
  88. package/src/cli/commands/oauth/index.ts +62 -10
  89. package/src/cli/commands/oauth/mode.ts +263 -0
  90. package/src/cli/commands/oauth/ping.ts +222 -0
  91. package/src/cli/commands/oauth/providers.ts +30 -3
  92. package/src/cli/commands/oauth/request.ts +576 -0
  93. package/src/cli/commands/oauth/shared.ts +132 -0
  94. package/src/cli/commands/oauth/status.ts +202 -0
  95. package/src/cli/commands/oauth/token.ts +159 -0
  96. package/src/cli/commands/platform.ts +20 -14
  97. package/src/cli.ts +82 -17
  98. package/src/config/assistant-feature-flags.ts +74 -11
  99. package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
  100. package/src/config/bundled-skills/app-builder/tools/app-create.ts +1 -1
  101. package/src/config/bundled-skills/messaging/SKILL.md +13 -36
  102. package/src/config/bundled-skills/messaging/TOOLS.json +9 -9
  103. package/src/config/bundled-skills/messaging/tools/messaging-analyze-style.ts +1 -1
  104. package/src/config/bundled-skills/notifications/SKILL.md +1 -1
  105. package/src/config/bundled-skills/schedule/SKILL.md +2 -2
  106. package/src/config/bundled-skills/settings/SKILL.md +5 -3
  107. package/src/config/bundled-skills/settings/TOOLS.json +17 -0
  108. package/src/config/bundled-skills/settings/tools/avatar-get.ts +50 -0
  109. package/src/config/bundled-skills/settings/tools/avatar-remove.ts +7 -0
  110. package/src/config/bundled-skills/settings/tools/avatar-update.ts +6 -1
  111. package/src/config/bundled-skills/settings/tools/identity-avatar.ts +55 -0
  112. package/src/config/bundled-skills/skills-catalog/SKILL.md +3 -3
  113. package/src/config/bundled-skills/slack/SKILL.md +58 -44
  114. package/src/config/bundled-tool-registry.ts +2 -19
  115. package/src/config/env.ts +5 -1
  116. package/src/config/feature-flag-registry.json +57 -41
  117. package/src/config/loader.ts +4 -0
  118. package/src/config/schemas/platform.ts +0 -8
  119. package/src/config/schemas/security.ts +9 -1
  120. package/src/config/schemas/services.ts +1 -1
  121. package/src/config/skill-state.ts +1 -3
  122. package/src/config/skills.ts +2 -4
  123. package/src/credential-execution/feature-gates.ts +9 -16
  124. package/src/credential-execution/process-manager.ts +12 -0
  125. package/src/daemon/config-watcher.ts +4 -0
  126. package/src/daemon/conversation-agent-loop-handlers.ts +10 -0
  127. package/src/daemon/conversation-agent-loop.ts +49 -2
  128. package/src/daemon/conversation-memory.ts +0 -1
  129. package/src/daemon/handlers/config-slack-channel.ts +43 -1
  130. package/src/daemon/handlers/conversations.ts +41 -33
  131. package/src/daemon/lifecycle.ts +28 -5
  132. package/src/daemon/message-types/acp.ts +0 -15
  133. package/src/daemon/message-types/memory.ts +0 -1
  134. package/src/daemon/message-types/messages.ts +9 -1
  135. package/src/daemon/message-types/schedules.ts +9 -0
  136. package/src/daemon/server.ts +19 -7
  137. package/src/email/feature-gate.ts +3 -3
  138. package/src/heartbeat/heartbeat-service.ts +48 -0
  139. package/src/inbound/platform-callback-registration.ts +61 -7
  140. package/src/mcp/mcp-oauth-provider.ts +3 -3
  141. package/src/memory/app-store.ts +3 -3
  142. package/src/memory/conversation-crud.ts +124 -0
  143. package/src/memory/conversation-title-service.ts +7 -17
  144. package/src/memory/db-init.ts +8 -0
  145. package/src/memory/embedding-local.ts +47 -2
  146. package/src/memory/indexer.ts +13 -10
  147. package/src/memory/items-extractor.ts +12 -4
  148. package/src/memory/job-utils.ts +5 -0
  149. package/src/memory/jobs-store.ts +10 -2
  150. package/src/memory/journal-memory.ts +6 -2
  151. package/src/memory/llm-request-log-store.ts +88 -21
  152. package/src/memory/memory-recall-log-store.ts +128 -0
  153. package/src/memory/migrations/194-memory-recall-logs.ts +50 -0
  154. package/src/memory/migrations/195-oauth-providers-ping-config.ts +23 -0
  155. package/src/memory/migrations/index.ts +2 -0
  156. package/src/memory/migrations/validate-migration-state.ts +14 -1
  157. package/src/memory/retriever.test.ts +4 -5
  158. package/src/memory/schema/infrastructure.ts +31 -0
  159. package/src/memory/schema/oauth.ts +3 -0
  160. package/src/messaging/providers/telegram-bot/adapter.ts +1 -1
  161. package/src/oauth/connect-orchestrator.ts +54 -0
  162. package/src/oauth/manual-token-connection.ts +5 -5
  163. package/src/oauth/oauth-store.ts +26 -5
  164. package/src/oauth/seed-providers.ts +10 -1
  165. package/src/permissions/checker.ts +2 -2
  166. package/src/permissions/trust-client.ts +2 -2
  167. package/src/platform/client.ts +2 -2
  168. package/src/prompts/journal-context.ts +6 -1
  169. package/src/providers/anthropic/client.ts +143 -1
  170. package/src/runtime/auth/__tests__/middleware.test.ts +19 -0
  171. package/src/runtime/auth/route-policy.ts +0 -1
  172. package/src/runtime/btw-sidechain.ts +7 -1
  173. package/src/runtime/channel-approvals.ts +2 -2
  174. package/src/runtime/channel-readiness-service.ts +30 -7
  175. package/src/runtime/http-router.ts +31 -0
  176. package/src/runtime/http-server.ts +21 -4
  177. package/src/runtime/http-types.ts +2 -0
  178. package/src/runtime/pending-interactions.ts +21 -3
  179. package/src/runtime/routes/acp-routes.ts +46 -28
  180. package/src/runtime/routes/app-management-routes.ts +123 -0
  181. package/src/runtime/routes/app-routes.ts +31 -0
  182. package/src/runtime/routes/approval-routes.ts +108 -3
  183. package/src/runtime/routes/attachment-routes.ts +45 -0
  184. package/src/runtime/routes/avatar-routes.ts +16 -0
  185. package/src/runtime/routes/brain-graph-routes.ts +18 -0
  186. package/src/runtime/routes/btw-routes.ts +20 -0
  187. package/src/runtime/routes/call-routes.ts +81 -0
  188. package/src/runtime/routes/channel-readiness-routes.ts +48 -7
  189. package/src/runtime/routes/channel-routes.ts +18 -0
  190. package/src/runtime/routes/channel-verification-routes.ts +49 -1
  191. package/src/runtime/routes/contact-routes.ts +77 -0
  192. package/src/runtime/routes/conversation-attention-routes.ts +37 -0
  193. package/src/runtime/routes/conversation-management-routes.ts +94 -0
  194. package/src/runtime/routes/conversation-query-routes.ts +78 -0
  195. package/src/runtime/routes/conversation-routes.ts +115 -38
  196. package/src/runtime/routes/conversation-starter-routes.ts +29 -0
  197. package/src/runtime/routes/debug-routes.ts +23 -0
  198. package/src/runtime/routes/diagnostics-routes.ts +30 -0
  199. package/src/runtime/routes/documents-routes.ts +42 -0
  200. package/src/runtime/routes/events-routes.ts +10 -0
  201. package/src/runtime/routes/global-search-routes.ts +35 -0
  202. package/src/runtime/routes/guardian-action-routes.ts +47 -2
  203. package/src/runtime/routes/guardian-approval-prompt.ts +77 -2
  204. package/src/runtime/routes/heartbeat-routes.ts +278 -0
  205. package/src/runtime/routes/host-bash-routes.ts +16 -1
  206. package/src/runtime/routes/host-cu-routes.ts +23 -1
  207. package/src/runtime/routes/host-file-routes.ts +18 -1
  208. package/src/runtime/routes/identity-routes.ts +35 -0
  209. package/src/runtime/routes/inbound-message-handler.ts +46 -25
  210. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +30 -2
  211. package/src/runtime/routes/inbound-stages/transcribe-audio.ts +1 -2
  212. package/src/runtime/routes/integrations/twilio.ts +32 -22
  213. package/src/runtime/routes/invite-routes.ts +83 -0
  214. package/src/runtime/routes/log-export-routes.ts +14 -0
  215. package/src/runtime/routes/memory-item-routes.ts +99 -1
  216. package/src/runtime/routes/migration-rollback-routes.ts +25 -0
  217. package/src/runtime/routes/migration-routes.ts +40 -0
  218. package/src/runtime/routes/notification-routes.ts +20 -0
  219. package/src/runtime/routes/oauth-apps.ts +11 -3
  220. package/src/runtime/routes/pairing-routes.ts +15 -0
  221. package/src/runtime/routes/recording-routes.ts +72 -0
  222. package/src/runtime/routes/schedule-routes.ts +77 -5
  223. package/src/runtime/routes/secret-routes.ts +63 -1
  224. package/src/runtime/routes/settings-routes.ts +91 -1
  225. package/src/runtime/routes/skills-routes.ts +98 -16
  226. package/src/runtime/routes/subagents-routes.ts +38 -3
  227. package/src/runtime/routes/surface-action-routes.ts +66 -24
  228. package/src/runtime/routes/surface-content-routes.ts +20 -0
  229. package/src/runtime/routes/telemetry-routes.ts +12 -0
  230. package/src/runtime/routes/trace-event-routes.ts +25 -0
  231. package/src/runtime/routes/trust-rules-routes.ts +46 -0
  232. package/src/runtime/routes/tts-routes.ts +15 -4
  233. package/src/runtime/routes/upgrade-broadcast-routes.ts +38 -0
  234. package/src/runtime/routes/usage-routes.ts +59 -0
  235. package/src/runtime/routes/watch-routes.ts +28 -0
  236. package/src/runtime/routes/work-items-routes.ts +59 -0
  237. package/src/runtime/routes/workspace-commit-routes.ts +12 -0
  238. package/src/runtime/routes/workspace-routes.ts +102 -0
  239. package/src/schedule/scheduler.ts +7 -1
  240. package/src/security/AGENTS.md +7 -0
  241. package/src/security/credential-backend.ts +1 -1
  242. package/src/security/encrypted-store.ts +3 -3
  243. package/src/security/oauth2.ts +55 -0
  244. package/src/security/secret-ingress.ts +174 -0
  245. package/src/security/secret-patterns.ts +133 -0
  246. package/src/security/secret-scanner.ts +28 -117
  247. package/src/signals/confirm.ts +12 -8
  248. package/src/signals/user-message.ts +18 -3
  249. package/src/skills/skill-memory.ts +1 -2
  250. package/src/tasks/task-runner.ts +7 -1
  251. package/src/tools/credentials/broker.ts +1 -1
  252. package/src/tools/credentials/metadata-store.ts +1 -1
  253. package/src/tools/credentials/vault.ts +2 -3
  254. package/src/tools/memory/definitions.ts +1 -1
  255. package/src/tools/memory/handlers.test.ts +2 -4
  256. package/src/tools/skills/load.ts +1 -1
  257. package/src/tools/terminal/safe-env.ts +7 -0
  258. package/src/tools/tool-manifest.ts +1 -1
  259. package/src/util/log-redact.ts +9 -34
  260. package/src/workspace/migrations/015-migrate-credentials-to-keychain.ts +13 -148
  261. package/src/workspace/migrations/016-migrate-credentials-from-keychain.ts +7 -145
  262. package/src/workspace/migrations/AGENTS.md +11 -0
  263. package/src/workspace/migrations/runner.ts +16 -6
  264. package/src/workspace/migrations/types.ts +7 -0
  265. package/docs/architecture/keychain-broker.md +0 -69
  266. package/src/__tests__/keychain-broker-client.test.ts +0 -800
  267. package/src/cli/commands/oauth/platform.ts +0 -525
  268. package/src/config/bundled-skills/slack/TOOLS.json +0 -272
  269. package/src/config/bundled-skills/slack/tools/shared.ts +0 -34
  270. package/src/config/bundled-skills/slack/tools/slack-add-reaction.ts +0 -27
  271. package/src/config/bundled-skills/slack/tools/slack-channel-details.ts +0 -38
  272. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +0 -146
  273. package/src/config/bundled-skills/slack/tools/slack-configure-channels.ts +0 -105
  274. package/src/config/bundled-skills/slack/tools/slack-delete-message.ts +0 -26
  275. package/src/config/bundled-skills/slack/tools/slack-edit-message.ts +0 -27
  276. package/src/config/bundled-skills/slack/tools/slack-leave-channel.ts +0 -25
  277. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +0 -372
  278. package/src/security/keychain-broker-client.ts +0 -446
@@ -0,0 +1,128 @@
1
+ import { and, eq, inArray, isNull } from "drizzle-orm";
2
+ import { v4 as uuid } from "uuid";
3
+
4
+ import { getDb } from "./db.js";
5
+ import { memoryRecallLogs } from "./schema.js";
6
+
7
+ export interface RecordMemoryRecallLogParams {
8
+ conversationId: string;
9
+ enabled: boolean;
10
+ degraded: boolean;
11
+ provider?: string;
12
+ model?: string;
13
+ degradationJson?: unknown;
14
+ semanticHits: number;
15
+ mergedCount: number;
16
+ selectedCount: number;
17
+ tier1Count: number;
18
+ tier2Count: number;
19
+ hybridSearchLatencyMs: number;
20
+ sparseVectorUsed: boolean;
21
+ injectedTokens: number;
22
+ latencyMs: number;
23
+ topCandidatesJson: unknown;
24
+ injectedText?: string;
25
+ reason?: string;
26
+ }
27
+
28
+ export function recordMemoryRecallLog(params: RecordMemoryRecallLogParams): void {
29
+ const db = getDb();
30
+ db.insert(memoryRecallLogs)
31
+ .values({
32
+ id: uuid(),
33
+ conversationId: params.conversationId,
34
+ messageId: null,
35
+ enabled: params.enabled ? 1 : 0,
36
+ degraded: params.degraded ? 1 : 0,
37
+ provider: params.provider ?? null,
38
+ model: params.model ?? null,
39
+ degradationJson: params.degradationJson
40
+ ? JSON.stringify(params.degradationJson)
41
+ : null,
42
+ semanticHits: params.semanticHits,
43
+ mergedCount: params.mergedCount,
44
+ selectedCount: params.selectedCount,
45
+ tier1Count: params.tier1Count,
46
+ tier2Count: params.tier2Count,
47
+ hybridSearchLatencyMs: params.hybridSearchLatencyMs,
48
+ sparseVectorUsed: params.sparseVectorUsed ? 1 : 0,
49
+ injectedTokens: params.injectedTokens,
50
+ latencyMs: params.latencyMs,
51
+ topCandidatesJson: JSON.stringify(params.topCandidatesJson),
52
+ injectedText: params.injectedText ?? null,
53
+ reason: params.reason ?? null,
54
+ createdAt: Date.now(),
55
+ })
56
+ .run();
57
+ }
58
+
59
+ export function backfillMemoryRecallLogMessageId(
60
+ conversationId: string,
61
+ messageId: string,
62
+ ): void {
63
+ const db = getDb();
64
+ db.update(memoryRecallLogs)
65
+ .set({ messageId })
66
+ .where(
67
+ and(
68
+ eq(memoryRecallLogs.conversationId, conversationId),
69
+ isNull(memoryRecallLogs.messageId),
70
+ ),
71
+ )
72
+ .run();
73
+ }
74
+
75
+ export interface MemoryRecallLog {
76
+ enabled: boolean;
77
+ degraded: boolean;
78
+ provider: string | null;
79
+ model: string | null;
80
+ degradation: unknown | null;
81
+ semanticHits: number;
82
+ mergedCount: number;
83
+ selectedCount: number;
84
+ tier1Count: number;
85
+ tier2Count: number;
86
+ hybridSearchLatencyMs: number;
87
+ sparseVectorUsed: boolean;
88
+ injectedTokens: number;
89
+ latencyMs: number;
90
+ topCandidates: unknown;
91
+ injectedText: string | null;
92
+ reason: string | null;
93
+ }
94
+
95
+ export function getMemoryRecallLogByMessageIds(
96
+ messageIds: string[],
97
+ ): MemoryRecallLog | null {
98
+ if (messageIds.length === 0) return null;
99
+ const db = getDb();
100
+ const rows = db
101
+ .select()
102
+ .from(memoryRecallLogs)
103
+ .where(inArray(memoryRecallLogs.messageId, messageIds))
104
+ .all();
105
+ if (rows.length === 0) return null;
106
+ const row = rows[0]!;
107
+ return {
108
+ enabled: !!row.enabled,
109
+ degraded: !!row.degraded,
110
+ provider: row.provider,
111
+ model: row.model,
112
+ degradation: row.degradationJson
113
+ ? JSON.parse(row.degradationJson)
114
+ : null,
115
+ semanticHits: row.semanticHits,
116
+ mergedCount: row.mergedCount,
117
+ selectedCount: row.selectedCount,
118
+ tier1Count: row.tier1Count,
119
+ tier2Count: row.tier2Count,
120
+ hybridSearchLatencyMs: row.hybridSearchLatencyMs,
121
+ sparseVectorUsed: !!row.sparseVectorUsed,
122
+ injectedTokens: row.injectedTokens,
123
+ latencyMs: row.latencyMs,
124
+ topCandidates: JSON.parse(row.topCandidatesJson),
125
+ injectedText: row.injectedText,
126
+ reason: row.reason,
127
+ };
128
+ }
@@ -0,0 +1,50 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+ import { withCrashRecovery } from "./validate-migration-state.js";
4
+
5
+ const CHECKPOINT_KEY = "migration_create_memory_recall_logs_v1";
6
+
7
+ /**
8
+ * Create the memory_recall_logs table for the inspector memory tab.
9
+ */
10
+ export function migrateCreateMemoryRecallLogs(database: DrizzleDb): void {
11
+ withCrashRecovery(database, CHECKPOINT_KEY, () => {
12
+ const raw = getSqliteFrom(database);
13
+
14
+ raw.exec(/*sql*/ `
15
+ CREATE TABLE IF NOT EXISTS memory_recall_logs (
16
+ id TEXT PRIMARY KEY,
17
+ conversation_id TEXT NOT NULL,
18
+ message_id TEXT,
19
+ enabled INTEGER NOT NULL,
20
+ degraded INTEGER NOT NULL,
21
+ provider TEXT,
22
+ model TEXT,
23
+ degradation_json TEXT,
24
+ semantic_hits INTEGER NOT NULL,
25
+ merged_count INTEGER NOT NULL,
26
+ selected_count INTEGER NOT NULL,
27
+ tier1_count INTEGER NOT NULL,
28
+ tier2_count INTEGER NOT NULL,
29
+ hybrid_search_latency_ms INTEGER NOT NULL,
30
+ sparse_vector_used INTEGER NOT NULL,
31
+ injected_tokens INTEGER NOT NULL,
32
+ latency_ms INTEGER NOT NULL,
33
+ top_candidates_json TEXT NOT NULL,
34
+ injected_text TEXT,
35
+ reason TEXT,
36
+ created_at INTEGER NOT NULL
37
+ )
38
+ `);
39
+
40
+ raw.exec(/*sql*/ `
41
+ CREATE INDEX IF NOT EXISTS idx_memory_recall_logs_message_id
42
+ ON memory_recall_logs (message_id)
43
+ `);
44
+
45
+ raw.exec(/*sql*/ `
46
+ CREATE INDEX IF NOT EXISTS idx_memory_recall_logs_conversation_id
47
+ ON memory_recall_logs (conversation_id)
48
+ `);
49
+ });
50
+ }
@@ -0,0 +1,23 @@
1
+ import type { DrizzleDb } from "../db-connection.js";
2
+ import { getSqliteFrom } from "../db-connection.js";
3
+
4
+ export function migrateOAuthProvidersPingConfig(database: DrizzleDb): void {
5
+ const raw = getSqliteFrom(database);
6
+ try {
7
+ raw.exec(/*sql*/ `ALTER TABLE oauth_providers ADD COLUMN ping_method TEXT`);
8
+ } catch {
9
+ // Column already exists — nothing to do.
10
+ }
11
+ try {
12
+ raw.exec(
13
+ /*sql*/ `ALTER TABLE oauth_providers ADD COLUMN ping_headers TEXT`,
14
+ );
15
+ } catch {
16
+ // Column already exists — nothing to do.
17
+ }
18
+ try {
19
+ raw.exec(/*sql*/ `ALTER TABLE oauth_providers ADD COLUMN ping_body TEXT`);
20
+ } catch {
21
+ // Column already exists — nothing to do.
22
+ }
23
+ }
@@ -132,6 +132,8 @@ export { migrateCallSessionSkipDisclosure } from "./190-call-session-skip-disclo
132
132
  export { migrateBackfillAudioAttachmentMimeTypes } from "./191-backfill-audio-attachment-mime-types.js";
133
133
  export { migrateContactsUserFileColumn } from "./192-contacts-user-file-column.js";
134
134
  export { migrateAddSourceTypeColumns } from "./193-add-source-type-columns.js";
135
+ export { migrateCreateMemoryRecallLogs } from "./194-memory-recall-logs.js";
136
+ export { migrateOAuthProvidersPingConfig } from "./195-oauth-providers-ping-config.js";
135
137
  export {
136
138
  MIGRATION_REGISTRY,
137
139
  type MigrationRegistryEntry,
@@ -98,7 +98,20 @@ export function withCrashRecovery(
98
98
  )
99
99
  .run(checkpointKey, Date.now());
100
100
 
101
- migrationFn();
101
+ try {
102
+ migrationFn();
103
+ } catch (error) {
104
+ log.error(
105
+ { checkpointKey, error },
106
+ `Memory migration failed: ${checkpointKey} — marking as failed and continuing`,
107
+ );
108
+ raw
109
+ .query(
110
+ `UPDATE memory_checkpoints SET value = 'failed', updated_at = ? WHERE key = ?`,
111
+ )
112
+ .run(Date.now(), checkpointKey);
113
+ return;
114
+ }
102
115
 
103
116
  raw
104
117
  .query(
@@ -331,8 +331,7 @@ describe("Memory Retriever Pipeline", () => {
331
331
  expect(result.enabled).toBe(true);
332
332
  expect(result.degraded).toBe(false);
333
333
  expect(result.degradation).toBeUndefined();
334
- // With mock Qdrant returning empty results and recency-only candidates
335
- // scoring below tier thresholds, no candidates are selected.
334
+ // With Qdrant mocked empty, no candidates are found.
336
335
  // The pipeline still completes successfully with tier metadata.
337
336
  expect(result.tier1Count).toBeDefined();
338
337
  expect(result.tier2Count).toBeDefined();
@@ -345,7 +344,7 @@ describe("Memory Retriever Pipeline", () => {
345
344
  // Current-conversation segment filtering
346
345
  // -----------------------------------------------------------------------
347
346
 
348
- test("current-conversation segments are filtered from recency results", async () => {
347
+ test("current-conversation segments are filtered from search results", async () => {
349
348
  const db = getDb();
350
349
  const now = Date.now();
351
350
  const activeConv = "conv-active";
@@ -613,7 +612,7 @@ describe("Memory Retriever Pipeline", () => {
613
612
 
614
613
  insertConversation(db, convId, now - MS_PER_DAY * 200);
615
614
 
616
- // Create a message from 200 days ago to serve as recency source
615
+ // Create a message from 200 days ago (staleness test anchor)
617
616
  insertMessage(
618
617
  db,
619
618
  "msg-old",
@@ -679,7 +678,7 @@ describe("Memory Retriever Pipeline", () => {
679
678
  // Degradation: Qdrant circuit breaker open
680
679
  // -----------------------------------------------------------------------
681
680
 
682
- test("Qdrant unavailable: pipeline completes with recency fallback", async () => {
681
+ test("Qdrant unavailable: pipeline completes with empty results", async () => {
683
682
  seedMemory();
684
683
 
685
684
  // Force the Qdrant circuit breaker open
@@ -121,6 +121,37 @@ export const llmRequestLogs = sqliteTable(
121
121
  (table) => [index("idx_llm_request_logs_message_id").on(table.messageId)],
122
122
  );
123
123
 
124
+ export const memoryRecallLogs = sqliteTable(
125
+ "memory_recall_logs",
126
+ {
127
+ id: text("id").primaryKey(),
128
+ conversationId: text("conversation_id").notNull(),
129
+ messageId: text("message_id"),
130
+ enabled: integer("enabled").notNull(),
131
+ degraded: integer("degraded").notNull(),
132
+ provider: text("provider"),
133
+ model: text("model"),
134
+ degradationJson: text("degradation_json"),
135
+ semanticHits: integer("semantic_hits").notNull(),
136
+ mergedCount: integer("merged_count").notNull(),
137
+ selectedCount: integer("selected_count").notNull(),
138
+ tier1Count: integer("tier1_count").notNull(),
139
+ tier2Count: integer("tier2_count").notNull(),
140
+ hybridSearchLatencyMs: integer("hybrid_search_latency_ms").notNull(),
141
+ sparseVectorUsed: integer("sparse_vector_used").notNull(),
142
+ injectedTokens: integer("injected_tokens").notNull(),
143
+ latencyMs: integer("latency_ms").notNull(),
144
+ topCandidatesJson: text("top_candidates_json").notNull(),
145
+ injectedText: text("injected_text"),
146
+ reason: text("reason"),
147
+ createdAt: integer("created_at").notNull(),
148
+ },
149
+ (table) => [
150
+ index("idx_memory_recall_logs_message_id").on(table.messageId),
151
+ index("idx_memory_recall_logs_conversation_id").on(table.conversationId),
152
+ ],
153
+ );
154
+
124
155
  export const llmUsageEvents = sqliteTable(
125
156
  "llm_usage_events",
126
157
  {
@@ -18,6 +18,9 @@ export const oauthProviders = sqliteTable("oauth_providers", {
18
18
  extraParams: text("extra_params"),
19
19
  callbackTransport: text("callback_transport"),
20
20
  pingUrl: text("ping_url"),
21
+ pingMethod: text("ping_method"),
22
+ pingHeaders: text("ping_headers"),
23
+ pingBody: text("ping_body"),
21
24
  managedServiceConfigKey: text("managed_service_config_key"),
22
25
  displayName: text("display_name"),
23
26
  description: text("description"),
@@ -56,7 +56,7 @@ export const telegramBotMessagingProvider: MessagingProvider = {
56
56
 
57
57
  /**
58
58
  * Custom connectivity check using both the oauth_connection record AND
59
- * actual keychain credentials. The connection row alone can become stale
59
+ * actual stored credentials. The connection row alone can become stale
60
60
  * if clearTelegramConfig() returns early on a secure-key deletion error
61
61
  * without removing the row. Checking both ensures we don't report
62
62
  * Telegram as connected when secrets are missing.
@@ -120,6 +120,16 @@ export async function orchestrateOAuthConnect(
120
120
  options: OAuthConnectOptions,
121
121
  ): Promise<OAuthConnectResult> {
122
122
  const resolvedService = resolveService(options.service);
123
+ log.info(
124
+ {
125
+ rawService: options.service,
126
+ resolvedService,
127
+ isInteractive: options.isInteractive,
128
+ hasOpenUrl: !!options.openUrl,
129
+ hasSendToClient: !!options.sendToClient,
130
+ },
131
+ "orchestrateOAuthConnect: starting",
132
+ );
123
133
 
124
134
  // Read provider config from the DB
125
135
  const providerRow = getProvider(resolvedService);
@@ -199,6 +209,20 @@ export async function orchestrateOAuthConnect(
199
209
  };
200
210
  }
201
211
 
212
+ log.info(
213
+ {
214
+ service: resolvedService,
215
+ authUrl,
216
+ tokenUrl,
217
+ scopeCount: finalScopes.length,
218
+ callbackTransport,
219
+ loopbackPort,
220
+ hasClientSecret: !!options.clientSecret,
221
+ clientIdPrefix: options.clientId.substring(0, 12) + "…",
222
+ },
223
+ "orchestrateOAuthConnect: resolved provider config",
224
+ );
225
+
202
226
  const oauthConfig = {
203
227
  authUrl,
204
228
  tokenUrl,
@@ -331,19 +355,31 @@ export async function orchestrateOAuthConnect(
331
355
  // -----------------------------------------------------------------------
332
356
  // Interactive path — open browser, block until completion
333
357
  // -----------------------------------------------------------------------
358
+ log.info(
359
+ { service: resolvedService, callbackTransport, loopbackPort },
360
+ "orchestrateOAuthConnect: entering interactive path",
361
+ );
334
362
  try {
335
363
  const { tokens, grantedScopes, rawTokenResponse } = await startOAuth2Flow(
336
364
  oauthConfig,
337
365
  {
338
366
  openUrl: (url) => {
367
+ log.info(
368
+ { service: resolvedService, urlLength: url.length },
369
+ "orchestrateOAuthConnect: openUrl callback fired, delivering auth URL to client",
370
+ );
339
371
  if (options.openUrl) {
372
+ log.info("orchestrateOAuthConnect: using options.openUrl");
340
373
  options.openUrl(url);
341
374
  } else if (options.sendToClient) {
375
+ log.info("orchestrateOAuthConnect: using sendToClient with open_url event");
342
376
  options.sendToClient({
343
377
  type: "open_url",
344
378
  url,
345
379
  title: `Connect ${resolvedService}`,
346
380
  });
381
+ } else {
382
+ log.warn("orchestrateOAuthConnect: no openUrl or sendToClient available — auth URL will not reach the user");
347
383
  }
348
384
  },
349
385
  },
@@ -354,6 +390,11 @@ export async function orchestrateOAuthConnect(
354
390
  : undefined,
355
391
  );
356
392
 
393
+ log.info(
394
+ { service: resolvedService, grantedScopeCount: grantedScopes.length },
395
+ "orchestrateOAuthConnect: interactive flow completed, exchanged code for tokens",
396
+ );
397
+
357
398
  // Parse account identifier from the provider's identity endpoint.
358
399
  // Best-effort — format varies by provider and may fail.
359
400
  let parsedAccountIdentifier: string | undefined;
@@ -362,6 +403,10 @@ export async function orchestrateOAuthConnect(
362
403
  parsedAccountIdentifier = await behavior.identityVerifier(
363
404
  tokens.accessToken,
364
405
  );
406
+ log.info(
407
+ { service: resolvedService, parsedAccountIdentifier },
408
+ "orchestrateOAuthConnect: identity verified",
409
+ );
365
410
  } catch {
366
411
  // Non-fatal
367
412
  }
@@ -375,6 +420,11 @@ export async function orchestrateOAuthConnect(
375
420
  parsedAccountIdentifier,
376
421
  });
377
422
 
423
+ log.info(
424
+ { service: resolvedService, accountInfo },
425
+ "orchestrateOAuthConnect: tokens stored, connect complete",
426
+ );
427
+
378
428
  return {
379
429
  success: true,
380
430
  deferred: false,
@@ -384,6 +434,10 @@ export async function orchestrateOAuthConnect(
384
434
  } catch (err: unknown) {
385
435
  const message =
386
436
  err instanceof Error ? err.message : "Unknown error during OAuth flow";
437
+ log.error(
438
+ { service: resolvedService, err },
439
+ "orchestrateOAuthConnect: interactive flow failed",
440
+ );
387
441
  return {
388
442
  success: false,
389
443
  error: `Error connecting "${resolvedService}": ${message}`,
@@ -2,9 +2,9 @@
2
2
  * Helpers for managing oauth_connection records for non-OAuth (manual-token)
3
3
  * providers like slack_channel and telegram.
4
4
  *
5
- * These providers store credentials via the keychain (setSecureKeyAsync) but
6
- * also maintain an oauth_connection row so that getConnectionByProvider() can
7
- * be used as the single source of truth for connection status across the
5
+ * These providers store credentials via the credential store (setSecureKeyAsync)
6
+ * but also maintain an oauth_connection row so that getConnectionByProvider()
7
+ * can be used as the single source of truth for connection status across the
8
8
  * codebase.
9
9
  */
10
10
 
@@ -57,7 +57,7 @@ export async function ensureManualTokenConnection(
57
57
  * Remove the oauth_connection row for a manual-token provider.
58
58
  *
59
59
  * Note: This only removes the oauth_connection row. The caller is still
60
- * responsible for deleting the keychain credentials separately.
60
+ * responsible for deleting the stored credentials separately.
61
61
  */
62
62
  export function removeManualTokenConnection(providerKey: string): void {
63
63
  const conn = getConnectionByProvider(providerKey);
@@ -114,7 +114,7 @@ export async function syncManualTokenConnection(
114
114
 
115
115
  /**
116
116
  * Backfill oauth_connection rows for manual-token providers that already
117
- * have valid keychain credentials but are missing connection records.
117
+ * have valid stored credentials but are missing connection records.
118
118
  *
119
119
  * This handles the upgrade path from installations that stored credentials
120
120
  * before the oauth_connection migration. Without this, existing Telegram
@@ -45,11 +45,12 @@ export type OAuthConnectionRow = typeof oauthConnections.$inferSelect;
45
45
  * Seed well-known provider profiles into the database. Uses INSERT … ON
46
46
  * CONFLICT DO UPDATE so that implementation fields (authUrl, tokenUrl,
47
47
  * tokenEndpointAuthMethod, userinfoUrl, extraParams, callbackTransport,
48
- * pingUrl, managedServiceConfigKey) and display metadata (displayName,
49
- * description, dashboardUrl, clientIdPlaceholder, requiresClientSecret)
50
- * propagate to existing installations on every startup, while
51
- * user-customizable fields (defaultScopes, scopePolicy, baseUrl) are
52
- * only written on the initial insert.
48
+ * pingUrl, pingMethod, pingHeaders, pingBody, managedServiceConfigKey)
49
+ * and display metadata (displayName, description, dashboardUrl,
50
+ * clientIdPlaceholder, requiresClientSecret) propagate to existing
51
+ * installations on every startup, while user-customizable fields
52
+ * (defaultScopes, scopePolicy, baseUrl) are only written on the
53
+ * initial insert.
53
54
  */
54
55
  export function seedProviders(
55
56
  profiles: Array<{
@@ -59,6 +60,9 @@ export function seedProviders(
59
60
  tokenEndpointAuthMethod?: string;
60
61
  userinfoUrl?: string;
61
62
  pingUrl?: string;
63
+ pingMethod?: string;
64
+ pingHeaders?: Record<string, string>;
65
+ pingBody?: unknown;
62
66
  baseUrl?: string;
63
67
  defaultScopes: string[];
64
68
  scopePolicy: Record<string, unknown>;
@@ -80,6 +84,10 @@ export function seedProviders(
80
84
  const tokenEndpointAuthMethod = p.tokenEndpointAuthMethod ?? null;
81
85
  const userinfoUrl = p.userinfoUrl ?? null;
82
86
  const pingUrl = p.pingUrl ?? null;
87
+ const pingMethod = p.pingMethod ?? null;
88
+ const pingHeaders = p.pingHeaders ? JSON.stringify(p.pingHeaders) : null;
89
+ const pingBody =
90
+ p.pingBody !== undefined ? JSON.stringify(p.pingBody) : null;
83
91
  const baseUrl = p.baseUrl ?? null;
84
92
  const defaultScopes = JSON.stringify(p.defaultScopes);
85
93
  const scopePolicy = JSON.stringify(p.scopePolicy);
@@ -105,6 +113,9 @@ export function seedProviders(
105
113
  extraParams,
106
114
  callbackTransport,
107
115
  pingUrl,
116
+ pingMethod,
117
+ pingHeaders,
118
+ pingBody,
108
119
  managedServiceConfigKey,
109
120
  displayName,
110
121
  description,
@@ -124,6 +135,9 @@ export function seedProviders(
124
135
  extraParams,
125
136
  callbackTransport,
126
137
  pingUrl,
138
+ pingMethod,
139
+ pingHeaders,
140
+ pingBody,
127
141
  managedServiceConfigKey,
128
142
  displayName,
129
143
  description,
@@ -164,6 +178,9 @@ export function registerProvider(params: {
164
178
  tokenEndpointAuthMethod?: string;
165
179
  userinfoUrl?: string;
166
180
  pingUrl?: string;
181
+ pingMethod?: string;
182
+ pingHeaders?: Record<string, string>;
183
+ pingBody?: unknown;
167
184
  baseUrl?: string;
168
185
  defaultScopes: string[];
169
186
  scopePolicy: Record<string, unknown>;
@@ -196,6 +213,10 @@ export function registerProvider(params: {
196
213
  extraParams: params.extraParams ? JSON.stringify(params.extraParams) : null,
197
214
  callbackTransport: params.callbackTransport ?? null,
198
215
  pingUrl: params.pingUrl ?? null,
216
+ pingMethod: params.pingMethod ?? null,
217
+ pingHeaders: params.pingHeaders ? JSON.stringify(params.pingHeaders) : null,
218
+ pingBody:
219
+ params.pingBody !== undefined ? JSON.stringify(params.pingBody) : null,
199
220
  managedServiceConfigKey: params.managedServiceConfigKey ?? null,
200
221
  displayName: params.displayName ?? null,
201
222
  description: params.description ?? null,
@@ -6,7 +6,8 @@ import { seedProviders } from "./oauth-store.js";
6
6
  * These values are upserted into the `oauth_providers` SQLite table on
7
7
  * every startup. Only Vellum implementation fields (authUrl, tokenUrl,
8
8
  * tokenEndpointAuthMethod, userinfoUrl, extraParams, callbackTransport,
9
- * pingUrl, managedServiceConfigKey) and display metadata (displayName,
9
+ * pingUrl, pingMethod, pingHeaders, pingBody, managedServiceConfigKey)
10
+ * and display metadata (displayName,
10
11
  * description, dashboardUrl, clientIdPlaceholder, requiresClientSecret)
11
12
  * are overwritten on subsequent startups — user-customizable
12
13
  * fields (defaultScopes, scopePolicy, baseUrl) are only
@@ -25,6 +26,9 @@ const PROVIDER_SEED_DATA: Record<
25
26
  tokenEndpointAuthMethod?: string;
26
27
  userinfoUrl?: string;
27
28
  pingUrl?: string;
29
+ pingMethod?: string;
30
+ pingHeaders?: Record<string, string>;
31
+ pingBody?: unknown;
28
32
  baseUrl?: string;
29
33
  defaultScopes: string[];
30
34
  scopePolicy: {
@@ -117,6 +121,7 @@ const PROVIDER_SEED_DATA: Record<
117
121
  authUrl: "https://api.notion.com/v1/oauth/authorize",
118
122
  tokenUrl: "https://api.notion.com/v1/oauth/token",
119
123
  pingUrl: "https://api.notion.com/v1/users/me",
124
+ pingHeaders: { "Notion-Version": "2022-06-28" },
120
125
  baseUrl: "https://api.notion.com",
121
126
  displayName: "Notion",
122
127
  description: "Pages and databases",
@@ -187,6 +192,9 @@ const PROVIDER_SEED_DATA: Record<
187
192
  authUrl: "https://linear.app/oauth/authorize",
188
193
  tokenUrl: "https://api.linear.app/oauth/token",
189
194
  pingUrl: "https://api.linear.app/graphql",
195
+ pingMethod: "POST",
196
+ pingHeaders: { "Content-Type": "application/json" },
197
+ pingBody: { query: "{ viewer { id name email } }" },
190
198
  baseUrl: "https://api.linear.app",
191
199
  displayName: "Linear",
192
200
  description: "Issues and projects",
@@ -280,6 +288,7 @@ const PROVIDER_SEED_DATA: Record<
280
288
  authUrl: "https://www.dropbox.com/oauth2/authorize",
281
289
  tokenUrl: "https://api.dropboxapi.com/oauth2/token",
282
290
  pingUrl: "https://api.dropboxapi.com/2/users/get_current_account",
291
+ pingMethod: "POST",
283
292
  baseUrl: "https://api.dropboxapi.com/2",
284
293
  displayName: "Dropbox",
285
294
  description: "Files and folders",
@@ -444,7 +444,7 @@ async function buildCommandCandidates(
444
444
  // through to the permissive skill_load:* allow rule.
445
445
  const config = getConfig();
446
446
  const inlineEnabled = isAssistantFeatureFlagEnabled(
447
- "feature_flags.inline-skill-commands.enabled",
447
+ "inline-skill-commands",
448
448
  config,
449
449
  );
450
450
 
@@ -1107,7 +1107,7 @@ function skillLoadAllowlistStrategy(
1107
1107
  // Check whether this is a dynamic (inline-command) skill load
1108
1108
  const config = getConfig();
1109
1109
  const inlineEnabled = isAssistantFeatureFlagEnabled(
1110
- "feature_flags.inline-skill-commands.enabled",
1110
+ "inline-skill-commands",
1111
1111
  config,
1112
1112
  );
1113
1113
 
@@ -13,7 +13,7 @@
13
13
  import type { TrustRule } from "@vellumai/ces-contracts";
14
14
 
15
15
  import { getGatewayInternalBaseUrl } from "../config/env.js";
16
- import { mintDaemonDeliveryToken } from "../runtime/auth/token-service.js";
16
+ import { mintEdgeRelayToken } from "../runtime/auth/token-service.js";
17
17
  import { getLogger } from "../util/logger.js";
18
18
 
19
19
  const log = getLogger("trust-client");
@@ -35,7 +35,7 @@ export interface AcceptStarterBundleResult {
35
35
 
36
36
  function authHeaders(): Record<string, string> {
37
37
  return {
38
- Authorization: `Bearer ${mintDaemonDeliveryToken()}`,
38
+ Authorization: `Bearer ${mintEdgeRelayToken()}`,
39
39
  "Content-Type": "application/json",
40
40
  };
41
41
  }
@@ -30,7 +30,7 @@ export class VellumPlatformClient {
30
30
  *
31
31
  * First tries the in-memory managed proxy context (available when the daemon
32
32
  * has rehydrated env overrides). Falls back to reading platform credentials
33
- * directly from the secure keychain so that standalone CLI invocations work
33
+ * directly from the credential store so that standalone CLI invocations work
34
34
  * without the daemon having run its rehydration step.
35
35
  *
36
36
  * Returns `null` when auth prerequisites are missing (not logged in, no API
@@ -44,7 +44,7 @@ export class VellumPlatformClient {
44
44
  let apiKey = ctx.enabled ? ctx.assistantApiKey : "";
45
45
  let assistantId = getPlatformAssistantId();
46
46
 
47
- // Fall back to keychain for values not yet rehydrated (standalone CLI).
47
+ // Fall back to credential store for values not yet rehydrated (standalone CLI).
48
48
  if (!baseUrl) {
49
49
  baseUrl =
50
50
  (await getSecureKeyAsync(
@@ -88,7 +88,9 @@ export function buildJournalContext(
88
88
  const filepath = join(journalDir, f);
89
89
  const stat = statSync(filepath);
90
90
  if (!stat.isFile()) return [];
91
- return [{ filename: f, filepath, birthtimeMs: stat.birthtimeMs }];
91
+ // Fall back to mtimeMs when birthtimeMs is unavailable (returns 0 on Linux ext4, NFS, Docker overlayfs)
92
+ const birthtimeMs = stat.birthtimeMs > 0 ? stat.birthtimeMs : stat.mtimeMs;
93
+ return [{ filename: f, filepath, birthtimeMs }];
92
94
  } catch {
93
95
  return [];
94
96
  }
@@ -129,5 +131,8 @@ export function buildJournalContext(
129
131
  sections.push(header + "\n" + content);
130
132
  }
131
133
 
134
+ // If all readFileSync calls failed, sections only contains the header — return null
135
+ if (sections.length === 1) return null;
136
+
132
137
  return sections.join("\n\n");
133
138
  }