@vellumai/assistant 0.8.11 → 0.8.12-staging.2

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 (244) hide show
  1. package/ARCHITECTURE.md +15 -17
  2. package/README.md +0 -6
  3. package/bun.lock +6 -122
  4. package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
  5. package/node_modules/@vellumai/gateway-client/package.json +3 -1
  6. package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
  7. package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
  8. package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
  9. package/openapi.yaml +633 -4
  10. package/package.json +1 -3
  11. package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
  12. package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
  13. package/src/__tests__/anthropic-provider.test.ts +129 -0
  14. package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
  15. package/src/__tests__/btw-routes.test.ts +7 -34
  16. package/src/__tests__/checker.test.ts +6 -12
  17. package/src/__tests__/config-loader-backfill.test.ts +4 -2
  18. package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
  19. package/src/__tests__/config-watcher.test.ts +2 -2
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
  21. package/src/__tests__/conversation-error.test.ts +2 -6
  22. package/src/__tests__/conversation-history-web-search.test.ts +8 -0
  23. package/src/__tests__/conversation-title-service.test.ts +2 -1
  24. package/src/__tests__/credential-security-invariants.test.ts +1 -1
  25. package/src/__tests__/disk-pressure-tools.test.ts +1 -1
  26. package/src/__tests__/exploration-drift-hook.test.ts +692 -0
  27. package/src/__tests__/filing-service.test.ts +8 -3
  28. package/src/__tests__/guardian-action-store.test.ts +0 -167
  29. package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
  30. package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
  31. package/src/__tests__/heartbeat-service.test.ts +5 -2
  32. package/src/__tests__/identity-intro-cache.test.ts +12 -5
  33. package/src/__tests__/identity-routes.test.ts +16 -57
  34. package/src/__tests__/injector-chain.test.ts +8 -3
  35. package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
  36. package/src/__tests__/llm-catalog-parity.test.ts +16 -0
  37. package/src/__tests__/llm-usage-store.test.ts +11 -0
  38. package/src/__tests__/log-export-workspace.test.ts +468 -3
  39. package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
  40. package/src/__tests__/model-intents.test.ts +1 -1
  41. package/src/__tests__/oauth-cli.test.ts +19 -8
  42. package/src/__tests__/openai-provider.test.ts +34 -0
  43. package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
  44. package/src/__tests__/recurrence-engine.test.ts +45 -0
  45. package/src/__tests__/schedule-routes.test.ts +34 -0
  46. package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
  47. package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
  48. package/src/__tests__/secret-fixtures.ts +20 -0
  49. package/src/__tests__/skill-tool-factory.test.ts +49 -0
  50. package/src/__tests__/subagent-role-registry.test.ts +24 -1
  51. package/src/__tests__/subagent-tools.test.ts +1 -0
  52. package/src/__tests__/system-prompt.test.ts +109 -11
  53. package/src/__tests__/tool-approval-handler.test.ts +85 -0
  54. package/src/__tests__/tool-audit-listener.test.ts +86 -0
  55. package/src/__tests__/tool-error-hook.test.ts +1 -0
  56. package/src/__tests__/tool-result-spool.test.ts +337 -0
  57. package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
  58. package/src/__tests__/validate-input.test.ts +95 -1
  59. package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
  60. package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
  61. package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
  62. package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
  63. package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
  64. package/src/acp/__tests__/agent-process.test.ts +315 -2
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
  66. package/src/acp/agent-process.ts +163 -34
  67. package/src/acp/prepare-agent-env.ts +55 -15
  68. package/src/agent/loop.ts +81 -24
  69. package/src/api/events/usage-progress.ts +28 -0
  70. package/src/api/index.ts +6 -0
  71. package/src/background-wake/wake-intent-hooks.test.ts +2 -0
  72. package/src/bundler/app-bundler.ts +25 -42
  73. package/src/bundler/app-compiler.ts +8 -0
  74. package/src/calls/call-controller.ts +1 -1
  75. package/src/cli/commands/plugins.ts +248 -15
  76. package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
  77. package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
  78. package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
  79. package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
  80. package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
  81. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
  82. package/src/cli/lib/inspect-plugin.ts +252 -0
  83. package/src/cli/lib/install-from-github.ts +214 -21
  84. package/src/cli/lib/list-installed-plugins.ts +17 -6
  85. package/src/cli/lib/plugin-artifact.ts +103 -0
  86. package/src/cli/lib/plugin-details.ts +18 -1
  87. package/src/cli/lib/plugin-fingerprint.ts +197 -0
  88. package/src/cli/lib/upgrade-plugin.ts +225 -0
  89. package/src/config/bundled-skills/subagent/SKILL.md +2 -0
  90. package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
  91. package/src/config/call-site-defaults.ts +13 -2
  92. package/src/config/feature-flag-registry.json +8 -16
  93. package/src/config/loader.ts +52 -59
  94. package/src/config/schema.ts +0 -2
  95. package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
  96. package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
  97. package/src/config/schemas/llm.ts +10 -0
  98. package/src/config/schemas/memory-v2.ts +13 -0
  99. package/src/config/schemas/memory-v3.ts +92 -0
  100. package/src/config/seed-inference-profiles.ts +4 -8
  101. package/src/context/post-turn-tool-result-truncation.ts +32 -18
  102. package/src/context/tool-result-spool.ts +104 -0
  103. package/src/credential-execution/feature-gates.ts +0 -1
  104. package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
  105. package/src/daemon/conversation-error.ts +6 -15
  106. package/src/daemon/conversation.ts +9 -0
  107. package/src/daemon/disk-pressure-policy.ts +0 -1
  108. package/src/daemon/lifecycle.ts +1 -20
  109. package/src/daemon/message-types/conversations.ts +2 -15
  110. package/src/daemon/trust-context.ts +1 -1
  111. package/src/events/tool-audit-listener.ts +40 -9
  112. package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
  113. package/src/home/__tests__/home-content-refresh.test.ts +114 -0
  114. package/src/home/__tests__/suggested-prompts.test.ts +86 -5
  115. package/src/home/home-content-refresh.ts +43 -31
  116. package/src/home/home-greeting-cache.ts +8 -1
  117. package/src/home/home-greeting.ts +13 -9
  118. package/src/home/suggested-prompts.ts +77 -24
  119. package/src/ipc/routes/trust-rules.test.ts +66 -72
  120. package/src/media/image-credentials.ts +2 -2
  121. package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
  122. package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
  123. package/src/memory/conversation-attention-store.ts +1 -0
  124. package/src/memory/conversation-bootstrap.ts +18 -9
  125. package/src/memory/conversation-crud.ts +12 -2
  126. package/src/memory/conversation-title-service.ts +53 -9
  127. package/src/memory/delivery-channels.ts +0 -69
  128. package/src/memory/graph/extraction-job.ts +0 -15
  129. package/src/memory/guardian-action-store.ts +1 -376
  130. package/src/memory/llm-usage-store.ts +5 -1
  131. package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
  132. package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
  133. package/src/memory/v2/__tests__/injection.test.ts +70 -0
  134. package/src/memory/v2/__tests__/static-context.test.ts +12 -0
  135. package/src/memory/v2/consolidation-job.ts +93 -9
  136. package/src/memory/v2/injection.ts +53 -0
  137. package/src/memory/v2/prompts/consolidation.ts +1 -0
  138. package/src/memory/v2/static-context.ts +13 -1
  139. package/src/memory/v2/sweep-job.ts +1 -1
  140. package/src/memory/v2/types.ts +5 -0
  141. package/src/plugin-api/types.ts +7 -0
  142. package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
  143. package/src/plugins/defaults/exploration-drift/package.json +15 -0
  144. package/src/plugins/defaults/index.ts +25 -0
  145. package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
  146. package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
  147. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
  148. package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
  149. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
  150. package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
  151. package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
  152. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
  153. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
  154. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
  155. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
  156. package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
  157. package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
  158. package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
  159. package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
  160. package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
  161. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
  162. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
  163. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
  164. package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
  165. package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
  166. package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
  167. package/src/prompts/cache-boundary.ts +17 -0
  168. package/src/prompts/sections.ts +50 -17
  169. package/src/prompts/system-prompt.ts +12 -4
  170. package/src/prompts/templates/system-sections.ts +22 -0
  171. package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
  172. package/src/providers/anthropic/client.ts +74 -28
  173. package/src/providers/gemini/client.ts +5 -1
  174. package/src/providers/minimax/client.ts +9 -0
  175. package/src/providers/model-catalog.ts +28 -0
  176. package/src/providers/model-intents.ts +3 -3
  177. package/src/providers/openai/chat-completions-provider.ts +4 -2
  178. package/src/providers/openai/responses-provider.ts +7 -2
  179. package/src/providers/retry.ts +8 -0
  180. package/src/providers/types.ts +11 -0
  181. package/src/providers/unparseable-tool-args.ts +56 -0
  182. package/src/runtime/AGENTS.md +6 -0
  183. package/src/runtime/__tests__/agent-wake.test.ts +2 -2
  184. package/src/runtime/agent-wake.ts +5 -5
  185. package/src/runtime/background-job-runner.ts +2 -2
  186. package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
  187. package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
  188. package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
  189. package/src/runtime/migrations/vbundle-importer.ts +9 -4
  190. package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
  191. package/src/runtime/pre-first-message-gate.ts +1 -1
  192. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
  193. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
  194. package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
  195. package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
  196. package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
  197. package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
  198. package/src/runtime/routes/btw-routes.ts +0 -14
  199. package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
  200. package/src/runtime/routes/conversation-list-routes.ts +77 -5
  201. package/src/runtime/routes/conversation-management-routes.ts +54 -0
  202. package/src/runtime/routes/conversation-query-routes.ts +79 -4
  203. package/src/runtime/routes/gateway-log-routes.ts +14 -64
  204. package/src/runtime/routes/home-feed-routes.ts +10 -0
  205. package/src/runtime/routes/identity-intro-cache.ts +1 -1
  206. package/src/runtime/routes/identity-routes.ts +76 -20
  207. package/src/runtime/routes/inbound-message-handler.ts +0 -36
  208. package/src/runtime/routes/log-export-routes.ts +143 -96
  209. package/src/runtime/routes/plugins-routes.ts +380 -0
  210. package/src/runtime/routes/redact-staged-export.ts +259 -0
  211. package/src/runtime/routes/schedule-routes.ts +19 -2
  212. package/src/runtime/routes/trust-rules-routes.ts +14 -67
  213. package/src/schedule/recurrence-engine.ts +34 -0
  214. package/src/schedule/scheduler.ts +1 -0
  215. package/src/security/redact-json.ts +61 -0
  216. package/src/skills/validate-input.ts +41 -1
  217. package/src/subagent/types.ts +26 -1
  218. package/src/telemetry/types.ts +15 -1
  219. package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
  220. package/src/telemetry/usage-telemetry-reporter.ts +1 -0
  221. package/src/tools/apps/executors.ts +1 -1
  222. package/src/tools/skills/skill-tool-factory.ts +19 -8
  223. package/src/tools/tool-approval-handler.ts +31 -0
  224. package/src/usage/types.ts +8 -1
  225. package/src/util/platform.ts +16 -0
  226. package/src/watcher/engine.ts +1 -0
  227. package/src/workspace/adaptive-thinking-repair.ts +113 -0
  228. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
  229. package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
  230. package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
  231. package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
  232. package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
  233. package/src/workspace/migrations/registry.ts +8 -0
  234. package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
  235. package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
  236. package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
  237. package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
  238. package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
  239. package/src/__tests__/update-bulletin-job.test.ts +0 -292
  240. package/src/config/schemas/updates.ts +0 -14
  241. package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
  242. package/src/memory/conversation-starters-cadence.ts +0 -78
  243. package/src/prompts/update-bulletin-job.ts +0 -180
  244. package/src/runtime/guardian-action-followup-executor.ts +0 -306
@@ -48,10 +48,6 @@ import {
48
48
  updateMessageContent,
49
49
  updateMessageMetadata,
50
50
  } from "../../memory/conversation-crud.js";
51
- import {
52
- clearPendingVerificationReply,
53
- getPendingVerificationReply,
54
- } from "../../memory/delivery-channels.js";
55
51
  import {
56
52
  clearPayload,
57
53
  findMessageBySourceId,
@@ -629,38 +625,6 @@ export async function handleChannelInbound({
629
625
 
630
626
  const replyCallbackUrl = body.replyCallbackUrl;
631
627
 
632
- // ── Retry pending verification reply on duplicate ──
633
- // If a previous verification delivery failed and stored a pending reply,
634
- // gateway retries (duplicates) re-attempt delivery here. On success the
635
- // pending marker is cleared so further duplicates short-circuit normally.
636
- if (result.duplicate && replyCallbackUrl) {
637
- const pendingReply = getPendingVerificationReply(result.eventId);
638
- if (pendingReply) {
639
- try {
640
- await deliverChannelReply(replyCallbackUrl, {
641
- chatId: pendingReply.chatId,
642
- text: pendingReply.text,
643
- assistantId: pendingReply.assistantId,
644
- });
645
- clearPendingVerificationReply(result.eventId);
646
- log.info(
647
- { eventId: result.eventId },
648
- "Retried pending verification reply: delivered",
649
- );
650
- } catch (retryErr) {
651
- log.error(
652
- { err: retryErr, eventId: result.eventId },
653
- "Retry of pending verification reply failed; will retry on next duplicate",
654
- );
655
- }
656
- return {
657
- accepted: true,
658
- duplicate: true,
659
- eventId: result.eventId,
660
- };
661
- }
662
- }
663
-
664
628
  // external_conversation_bindings is assistant-agnostic. Restrict writes to
665
629
  // self so assistant-scoped legacy routes do not overwrite each other's
666
630
  // channel binding metadata for the same chat.
@@ -41,6 +41,7 @@ import { ACTOR_PRINCIPALS } from "../auth/route-policy.js";
41
41
  import { createTarGz } from "./archive-utils.js";
42
42
  import { InternalError } from "./errors.js";
43
43
  import { collectWorkspaceData } from "./log-export/workspace-allowlist.js";
44
+ import { redactStagedExportFiles } from "./redact-staged-export.js";
44
45
  import type { RouteDefinition, RouteHandlerArgs } from "./types.js";
45
46
 
46
47
  const log = getLogger("log-export-routes");
@@ -48,6 +49,22 @@ const log = getLogger("log-export-routes");
48
49
  /** Maximum total payload size for log file contents (10 MB). */
49
50
  const MAX_LOG_PAYLOAD_BYTES = 10 * 1024 * 1024;
50
51
 
52
+ /**
53
+ * Row caps for the conversation-data table dumps (`messages.json`,
54
+ * `llm-request-logs.json`, `llm-usage-events.json`). Without them, a
55
+ * `full: true` export dumps entire tables — for long-tenured users the only
56
+ * realistic way to blow past the sweep's `MAX_SWEEP_FILE_BYTES` cap, which
57
+ * fails closed by replacing the whole file with an omission note. Bounding
58
+ * at the source keeps the most recent (and most useful) rows instead,
59
+ * mirroring the `auditLimit` pattern: order by createdAt desc, newest rows
60
+ * first. Truncated sections are logged and surfaced in both the export log
61
+ * line and `export-manifest.json` (`truncatedSections`), so a capped bundle
62
+ * is distinguishable from a complete one.
63
+ */
64
+ const MAX_EXPORT_MESSAGE_ROWS = 10_000;
65
+ export const MAX_EXPORT_LLM_REQUEST_LOG_ROWS = 2_000;
66
+ const MAX_EXPORT_LLM_USAGE_EVENT_ROWS = 10_000;
67
+
51
68
  interface ExportRequestBody {
52
69
  auditLimit?: number;
53
70
  conversationId?: string;
@@ -102,23 +119,44 @@ async function handleExport({
102
119
  );
103
120
 
104
121
  // --- Conversation data tables ---
122
+ // Sections truncated by a row cap, surfaced in the export log line and
123
+ // the export manifest.
124
+ const truncatedSections: string[] = [];
125
+ /**
126
+ * Keep the newest `limit` rows of a section dump. Callers fetch
127
+ * `limit + 1` rows so truncation is detectable without a COUNT query.
128
+ */
129
+ const capRows = <T>(rows: T[], limit: number, section: string): T[] => {
130
+ if (rows.length <= limit) return rows;
131
+ truncatedSections.push(section);
132
+ log.warn(
133
+ { section, limit },
134
+ "Export section exceeded its row cap; keeping only the most recent rows",
135
+ );
136
+ return rows.slice(0, limit);
137
+ };
105
138
  if (conversationId || full) {
106
139
  const conversationFilter = conversationId
107
140
  ? [eq(messages.conversationId, conversationId)]
108
141
  : [];
109
142
 
110
- const messageRows = db
111
- .select()
112
- .from(messages)
113
- .where(
114
- and(
115
- ...conversationFilter,
116
- startTime ? gte(messages.createdAt, startTime) : undefined,
117
- endTime ? lte(messages.createdAt, endTime) : undefined,
118
- ),
119
- )
120
- .orderBy(messages.createdAt)
121
- .all();
143
+ const messageRows = capRows(
144
+ db
145
+ .select()
146
+ .from(messages)
147
+ .where(
148
+ and(
149
+ ...conversationFilter,
150
+ startTime ? gte(messages.createdAt, startTime) : undefined,
151
+ endTime ? lte(messages.createdAt, endTime) : undefined,
152
+ ),
153
+ )
154
+ .orderBy(desc(messages.createdAt))
155
+ .limit(MAX_EXPORT_MESSAGE_ROWS + 1)
156
+ .all(),
157
+ MAX_EXPORT_MESSAGE_ROWS,
158
+ "messages",
159
+ );
122
160
  writeFileSync(
123
161
  join(staging, "messages.json"),
124
162
  JSON.stringify(messageRows, null, 2),
@@ -129,18 +167,23 @@ async function handleExport({
129
167
  ? [eq(llmRequestLogs.conversationId, conversationId)]
130
168
  : [];
131
169
 
132
- const llmLogRows = db
133
- .select()
134
- .from(llmRequestLogs)
135
- .where(
136
- and(
137
- ...llmConversationFilter,
138
- startTime ? gte(llmRequestLogs.createdAt, startTime) : undefined,
139
- endTime ? lte(llmRequestLogs.createdAt, endTime) : undefined,
140
- ),
141
- )
142
- .orderBy(llmRequestLogs.createdAt)
143
- .all();
170
+ const llmLogRows = capRows(
171
+ db
172
+ .select()
173
+ .from(llmRequestLogs)
174
+ .where(
175
+ and(
176
+ ...llmConversationFilter,
177
+ startTime ? gte(llmRequestLogs.createdAt, startTime) : undefined,
178
+ endTime ? lte(llmRequestLogs.createdAt, endTime) : undefined,
179
+ ),
180
+ )
181
+ .orderBy(desc(llmRequestLogs.createdAt))
182
+ .limit(MAX_EXPORT_LLM_REQUEST_LOG_ROWS + 1)
183
+ .all(),
184
+ MAX_EXPORT_LLM_REQUEST_LOG_ROWS,
185
+ "llm-request-logs",
186
+ );
144
187
  writeFileSync(
145
188
  join(staging, "llm-request-logs.json"),
146
189
  JSON.stringify(llmLogRows, null, 2),
@@ -151,18 +194,23 @@ async function handleExport({
151
194
  ? [eq(llmUsageEvents.conversationId, conversationId)]
152
195
  : [];
153
196
 
154
- const usageRows = db
155
- .select()
156
- .from(llmUsageEvents)
157
- .where(
158
- and(
159
- ...usageConversationFilter,
160
- startTime ? gte(llmUsageEvents.createdAt, startTime) : undefined,
161
- endTime ? lte(llmUsageEvents.createdAt, endTime) : undefined,
162
- ),
163
- )
164
- .orderBy(llmUsageEvents.createdAt)
165
- .all();
197
+ const usageRows = capRows(
198
+ db
199
+ .select()
200
+ .from(llmUsageEvents)
201
+ .where(
202
+ and(
203
+ ...usageConversationFilter,
204
+ startTime ? gte(llmUsageEvents.createdAt, startTime) : undefined,
205
+ endTime ? lte(llmUsageEvents.createdAt, endTime) : undefined,
206
+ ),
207
+ )
208
+ .orderBy(desc(llmUsageEvents.createdAt))
209
+ .limit(MAX_EXPORT_LLM_USAGE_EVENT_ROWS + 1)
210
+ .all(),
211
+ MAX_EXPORT_LLM_USAGE_EVENT_ROWS,
212
+ "llm-usage-events",
213
+ );
166
214
  writeFileSync(
167
215
  join(staging, "llm-usage-events.json"),
168
216
  JSON.stringify(usageRows, null, 2),
@@ -335,6 +383,7 @@ async function handleExport({
335
383
  commitSha: COMMIT_SHA,
336
384
  ...(startTime !== undefined ? { startTime } : {}),
337
385
  ...(endTime !== undefined ? { endTime } : {}),
386
+ ...(truncatedSections.length > 0 ? { truncatedSections } : {}),
338
387
  exportedAt: new Date().toISOString(),
339
388
  };
340
389
  writeFileSync(
@@ -343,6 +392,13 @@ async function handleExport({
343
392
  "utf-8",
344
393
  );
345
394
 
395
+ // --- Secret-redaction sweep over every staged text file ---
396
+ // Belt-and-suspenders over the structural sanitizers above: catches
397
+ // legacy audit rows persisted with plaintext inputs (written before
398
+ // write-time redaction existed) and secrets sitting in copied workspace
399
+ // conversation files.
400
+ const redactionResult = redactStagedExportFiles(staging);
401
+
346
402
  log.info(
347
403
  {
348
404
  auditCount: auditRows.length,
@@ -353,6 +409,10 @@ async function handleExport({
353
409
  full: full ?? false,
354
410
  workspaceEntries: workspaceResult.entries.length,
355
411
  workspaceBytes: workspaceResult.totalBytes,
412
+ truncatedSections,
413
+ redactionScanned: redactionResult.filesScanned,
414
+ redactionRedacted: redactionResult.filesRedacted,
415
+ redactionOmitted: redactionResult.filesOmitted,
356
416
  },
357
417
  "Export collected, creating tar.gz archive",
358
418
  );
@@ -386,6 +446,25 @@ function redactStringValue(val: unknown): string {
386
446
  return val ? "(set)" : "(empty)";
387
447
  }
388
448
 
449
+ /**
450
+ * Narrow an unknown value to a mutable record, or undefined if not an object.
451
+ * Deliberately looser than `isPlainObject` (`util/object.ts`): arrays are
452
+ * admitted on purpose so sanitization stays fail-closed — a malformed config
453
+ * shape must redact more, never less.
454
+ */
455
+ function asRecord(v: unknown): Record<string, unknown> | undefined {
456
+ return v && typeof v === "object"
457
+ ? (v as Record<string, unknown>)
458
+ : undefined;
459
+ }
460
+
461
+ /** Replace every value in a map with its (set)/(empty) presence flag. */
462
+ function redactValueMap(obj: Record<string, unknown>): Record<string, string> {
463
+ return Object.fromEntries(
464
+ Object.keys(obj).map((k) => [k, redactStringValue(obj[k])]),
465
+ );
466
+ }
467
+
389
468
  function readSanitizedConfig(): Record<string, unknown> | undefined {
390
469
  const configPath = getWorkspaceConfigPath();
391
470
  if (!existsSync(configPath)) return undefined;
@@ -396,71 +475,39 @@ function readSanitizedConfig(): Record<string, unknown> | undefined {
396
475
 
397
476
  delete config.apiKeys;
398
477
 
399
- if (config.ingress && typeof config.ingress === "object") {
400
- const ingress = config.ingress as Record<string, unknown>;
401
- if (ingress.webhook && typeof ingress.webhook === "object") {
402
- const webhook = ingress.webhook as Record<string, unknown>;
403
- webhook.secret = redactStringValue(webhook.secret);
404
- ingress.webhook = webhook;
405
- }
406
- config.ingress = ingress;
407
- }
478
+ const webhook = asRecord(asRecord(config.ingress)?.webhook);
479
+ if (webhook) webhook.secret = redactStringValue(webhook.secret);
408
480
 
409
- if (config.skills && typeof config.skills === "object") {
410
- const skills = config.skills as Record<string, unknown>;
411
- if (skills.entries && typeof skills.entries === "object") {
412
- const entries = skills.entries as Record<string, unknown>;
413
- for (const name of Object.keys(entries)) {
414
- const entry = entries[name];
415
- if (entry && typeof entry === "object") {
416
- const e = entry as Record<string, unknown>;
417
- if ("apiKey" in e) e.apiKey = redactStringValue(e.apiKey);
418
- if (e.env && typeof e.env === "object") {
419
- const env = e.env as Record<string, unknown>;
420
- e.env = Object.fromEntries(
421
- Object.keys(env).map((k) => [k, redactStringValue(env[k])]),
422
- );
423
- }
424
- }
425
- }
426
- }
481
+ const skillEntries = asRecord(asRecord(config.skills)?.entries);
482
+ for (const entry of Object.values(skillEntries ?? {})) {
483
+ const e = asRecord(entry);
484
+ if (!e) continue;
485
+ if ("apiKey" in e) e.apiKey = redactStringValue(e.apiKey);
486
+ const env = asRecord(e.env);
487
+ if (env) e.env = redactValueMap(env);
427
488
  }
428
489
 
429
- if (config.twilio && typeof config.twilio === "object") {
430
- const twilio = config.twilio as Record<string, unknown>;
431
- twilio.accountSid = redactStringValue(twilio.accountSid);
432
- config.twilio = twilio;
490
+ const twilio = asRecord(config.twilio);
491
+ if (twilio) twilio.accountSid = redactStringValue(twilio.accountSid);
492
+
493
+ const acpAgents = asRecord(asRecord(config.acp)?.agents);
494
+ for (const agent of Object.values(acpAgents ?? {})) {
495
+ const a = asRecord(agent);
496
+ if (!a) continue;
497
+ // Agent env is an arbitrary user-supplied map (often API keys);
498
+ // redact every value and keep only the key names.
499
+ const env = asRecord(a.env);
500
+ if (env) a.env = redactValueMap(env);
433
501
  }
434
502
 
435
- if (config.mcp && typeof config.mcp === "object") {
436
- const mcp = config.mcp as Record<string, unknown>;
437
- if (mcp.servers && typeof mcp.servers === "object") {
438
- const servers = mcp.servers as Record<string, unknown>;
439
- for (const name of Object.keys(servers)) {
440
- const server = servers[name];
441
- if (server && typeof server === "object") {
442
- const s = server as Record<string, unknown>;
443
- if (s.transport && typeof s.transport === "object") {
444
- const transport = s.transport as Record<string, unknown>;
445
- if (transport.headers && typeof transport.headers === "object") {
446
- const headers = transport.headers as Record<string, unknown>;
447
- transport.headers = Object.fromEntries(
448
- Object.keys(headers).map((k) => [
449
- k,
450
- redactStringValue(headers[k]),
451
- ]),
452
- );
453
- }
454
- if (transport.env && typeof transport.env === "object") {
455
- const env = transport.env as Record<string, unknown>;
456
- transport.env = Object.fromEntries(
457
- Object.keys(env).map((k) => [k, redactStringValue(env[k])]),
458
- );
459
- }
460
- }
461
- }
462
- }
463
- }
503
+ const mcpServers = asRecord(asRecord(config.mcp)?.servers);
504
+ for (const server of Object.values(mcpServers ?? {})) {
505
+ const transport = asRecord(asRecord(server)?.transport);
506
+ if (!transport) continue;
507
+ const headers = asRecord(transport.headers);
508
+ if (headers) transport.headers = redactValueMap(headers);
509
+ const env = asRecord(transport.env);
510
+ if (env) transport.env = redactValueMap(env);
464
511
  }
465
512
 
466
513
  return config;