@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
@@ -1,180 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import { existsSync, readFileSync } from "node:fs";
3
-
4
- import { getConfig } from "../config/loader.js";
5
- import {
6
- getMemoryCheckpoint,
7
- setMemoryCheckpoint,
8
- } from "../memory/checkpoints.js";
9
- import { runBackgroundJob } from "../runtime/background-job-runner.js";
10
- import { hasReceivedUserMessage } from "../runtime/pre-first-message-gate.js";
11
- import { getLogger } from "../util/logger.js";
12
- import {
13
- getWorkspaceDirDisplay,
14
- getWorkspacePromptPath,
15
- } from "../util/platform.js";
16
-
17
- const log = getLogger("update-bulletin-job");
18
-
19
- const HASH_CHECKPOINT_KEY = "updates:last_processed_hash";
20
- const EMPTY_HASH = "empty";
21
- /**
22
- * Hard timeout for the update-bulletin agent turn. The agent reads a small
23
- * markdown file and (usually) deletes it; 10 minutes is generous headroom for
24
- * a slow model + any tool calls (e.g. memory writes).
25
- */
26
- const UPDATE_BULLETIN_TIMEOUT_MS = 10 * 60 * 1000;
27
-
28
- function updateBulletinHint(): string {
29
- const workspace = getWorkspaceDirDisplay();
30
- return `Check ${workspace}/UPDATES.md — new release notes are present. Apply any assistant-facing behavior changes (new tools, deprecations, memory updates). If the user would benefit from knowing about a user-facing change, surface it only when the next topic makes it relevant — do not interrupt them with a proactive message. When you're done processing, delete the file by running \`cd "${workspace}" && rm UPDATES.md\` (the bare-filename \`rm UPDATES.md\` is auto-allowed; path-qualified deletes are not). A silent no-op is preferable to low-signal chatter.`;
31
- }
32
-
33
- type ReadResult =
34
- | { kind: "missing" }
35
- | { kind: "error"; err: unknown }
36
- | { kind: "ok"; content: string };
37
-
38
- function computeHash(content: string): string {
39
- return createHash("sha256").update(content).digest("hex");
40
- }
41
-
42
- function readTrimmedContent(path: string): ReadResult {
43
- if (!existsSync(path)) return { kind: "missing" };
44
- try {
45
- return { kind: "ok", content: readFileSync(path, "utf-8").trim() };
46
- } catch (err) {
47
- return { kind: "error", err };
48
- }
49
- }
50
-
51
- /**
52
- * Fire-and-forget background processor for the release-notes bulletin.
53
- *
54
- * If `<workspace>/UPDATES.md` has new (unprocessed) content, this drives a
55
- * background conversation through `runBackgroundJob` with a hint pointing at
56
- * the file. De-duplication uses a sha256 content hash stored in the
57
- * `updates:last_processed_hash` memory checkpoint — an `"empty"` sentinel
58
- * represents a missing/blank file so the job skips the common no-op case.
59
- *
60
- * The function never throws: any error inside `runBackgroundJob` is captured
61
- * in its structured result (which already emits an `activity.failed`
62
- * notification) and surrounding errors are logged at `warn` and swallowed,
63
- * so callers can safely invoke it in a non-awaited context.
64
- *
65
- * Checkpoint write rules (intentionally conservative — prefer retry over
66
- * poisoning the checkpoint when state is ambiguous):
67
- * - File missing → checkpoint = `EMPTY_HASH`.
68
- * - File present but unreadable → checkpoint UNCHANGED, warn logged.
69
- * - `runBackgroundJob` returned `ok: false` → checkpoint UNCHANGED so the
70
- * next startup retries. The runner has already emitted an
71
- * `activity.failed` notification.
72
- * - Job ran successfully + file deleted/empty → checkpoint = `EMPTY_HASH`.
73
- * - Job ran successfully + file unchanged → checkpoint = current hash
74
- * (agent intentionally left the file).
75
- */
76
- export async function runUpdateBulletinJobIfNeeded(): Promise<void> {
77
- if (getConfig().updates.enabled === false) {
78
- return;
79
- }
80
-
81
- // Warm-pool guard: don't process release notes before a real user has
82
- // interacted with the assistant. Provider credentials are typically not
83
- // registered until the user hatches the image, so this job would fail
84
- // and leave a "Background job failed: update-bulletin" row in the
85
- // sidebar the user inherits at hatch time. The checkpoint is left
86
- // untouched on purpose — once the user sends their first message, the
87
- // job runs on the next daemon start (or after the next UPDATES.md
88
- // change) with normal semantics.
89
- if (!hasReceivedUserMessage()) {
90
- log.info(
91
- "update-bulletin-job: skipped — daemon has not received a first user message yet",
92
- );
93
- return;
94
- }
95
-
96
- try {
97
- const updatesPath = getWorkspacePromptPath("UPDATES.md");
98
- const initial = readTrimmedContent(updatesPath);
99
-
100
- if (initial.kind === "error") {
101
- log.warn(
102
- { err: initial.err, path: updatesPath },
103
- "update-bulletin-job: failed to read UPDATES.md; leaving checkpoint unchanged so next startup retries",
104
- );
105
- return;
106
- }
107
-
108
- if (initial.kind === "missing" || initial.content.length === 0) {
109
- const stored = getMemoryCheckpoint(HASH_CHECKPOINT_KEY);
110
- if (stored !== EMPTY_HASH) {
111
- setMemoryCheckpoint(HASH_CHECKPOINT_KEY, EMPTY_HASH);
112
- }
113
- return;
114
- }
115
-
116
- const currentHash = computeHash(initial.content);
117
- const stored = getMemoryCheckpoint(HASH_CHECKPOINT_KEY);
118
- if (stored === currentHash) {
119
- return;
120
- }
121
-
122
- const result = await runBackgroundJob({
123
- jobName: "update-bulletin",
124
- source: "update-bulletin",
125
- origin: "updates_bulletin",
126
- prompt: updateBulletinHint(),
127
- trustContext: {
128
- sourceChannel: "vellum",
129
- trustClass: "guardian",
130
- },
131
- callSite: "mainAgent",
132
- timeoutMs: UPDATE_BULLETIN_TIMEOUT_MS,
133
- });
134
-
135
- if (!result.ok) {
136
- log.warn(
137
- {
138
- conversationId: result.conversationId,
139
- errorKind: result.errorKind,
140
- err: result.error?.message,
141
- },
142
- "update-bulletin-job: runBackgroundJob returned ok=false; leaving checkpoint unchanged so next startup retries (failure notification already emitted by runner)",
143
- );
144
- return;
145
- }
146
-
147
- // Re-read after the job completed. We need to know whether the file was
148
- // deleted or modified to decide whether to advance the checkpoint.
149
- const after = readTrimmedContent(updatesPath);
150
-
151
- if (after.kind === "error") {
152
- log.warn(
153
- { err: after.err, path: updatesPath },
154
- "update-bulletin-job: failed to re-read UPDATES.md after job; leaving checkpoint unchanged so next startup retries",
155
- );
156
- return;
157
- }
158
-
159
- const fileMissingOrEmpty =
160
- after.kind === "missing" || after.content.length === 0;
161
-
162
- if (fileMissingOrEmpty) {
163
- // The agent (or another process) emptied/removed the file. This is the
164
- // expected happy path — record the empty sentinel.
165
- setMemoryCheckpoint(HASH_CHECKPOINT_KEY, EMPTY_HASH);
166
- return;
167
- }
168
-
169
- // Job succeeded and the file is still present — the agent intentionally
170
- // left it (or modified it). Record the current hash so we don't re-process
171
- // the same content on next startup.
172
- setMemoryCheckpoint(HASH_CHECKPOINT_KEY, computeHash(after.content));
173
- } catch (err) {
174
- log.warn(
175
- { err },
176
- "update-bulletin-job: outer flow threw; swallowing so callers can fire-and-forget",
177
- );
178
- return;
179
- }
180
- }
@@ -1,306 +0,0 @@
1
- /**
2
- * Guardian action follow-up executor.
3
- *
4
- * After the conversation engine classifies the guardian's reply as
5
- * `call_back` and transitions the follow-up state to `dispatching`,
6
- * this module executes the actual action:
7
- *
8
- * - **call_back**: Starts an outbound call to the counterparty with
9
- * context about the guardian's answer.
10
- *
11
- * The executor resolves the counterparty from the original call session,
12
- * dispatches the appropriate action, and returns a result with generated
13
- * reply text for the guardian's confirmation message.
14
- *
15
- * This module is channel-agnostic: both inbound-message-handler (Telegram
16
- * channels) and session-process (desktop channel) use it.
17
- */
18
-
19
- import { startCall } from "../calls/call-domain.js";
20
- import { getCallSession } from "../calls/call-store.js";
21
- import { getOrCreateConversation } from "../memory/conversation-key-store.js";
22
- import {
23
- finalizeFollowup,
24
- type FollowupAction,
25
- getGuardianActionRequest,
26
- type GuardianActionRequest,
27
- } from "../memory/guardian-action-store.js";
28
- import { getLogger } from "../util/logger.js";
29
- import { DAEMON_INTERNAL_ASSISTANT_ID } from "./assistant-scope.js";
30
- import { composeGuardianActionMessageGenerative } from "./guardian-action-message-composer.js";
31
- import type { GuardianActionCopyGenerator } from "./http-types.js";
32
-
33
- const log = getLogger("guardian-action-followup-executor");
34
-
35
- // ---------------------------------------------------------------------------
36
- // Types
37
- // ---------------------------------------------------------------------------
38
-
39
- export interface CounterpartyInfo {
40
- phoneNumber: string;
41
- /** Human-readable identifier (phone number or name if available). */
42
- displayIdentifier: string;
43
- }
44
-
45
- export type FollowupExecutionResult =
46
- | { ok: true; action: FollowupAction; guardianReplyText: string }
47
- | {
48
- ok: false;
49
- action: FollowupAction;
50
- guardianReplyText: string;
51
- error: string;
52
- };
53
-
54
- // ---------------------------------------------------------------------------
55
- // Counterparty resolution
56
- // ---------------------------------------------------------------------------
57
-
58
- /**
59
- * Resolve the counterparty (the external person) from the original call
60
- * session by call direction.
61
- *
62
- * - **Inbound calls** (`initiatedFromConversationId` is null): the external
63
- * caller is `fromNumber`, the assistant's Twilio number is `toNumber`.
64
- * - **Outbound calls** (`initiatedFromConversationId` is set): the assistant
65
- * placed the call so `fromNumber` is the assistant's number and `toNumber`
66
- * is the external callee.
67
- */
68
- export function resolveCounterparty(
69
- callSessionId: string,
70
- ): CounterpartyInfo | null {
71
- const session = getCallSession(callSessionId);
72
- if (!session) {
73
- log.warn(
74
- { callSessionId },
75
- "Cannot resolve counterparty: call session not found",
76
- );
77
- return null;
78
- }
79
-
80
- // Outbound calls (startCall) store the assistant's number in fromNumber
81
- // and the callee in toNumber. They always have initiatedFromConversationId.
82
- const isOutbound = !!session.initiatedFromConversationId;
83
- const phoneNumber = isOutbound ? session.toNumber : session.fromNumber;
84
-
85
- if (!phoneNumber) {
86
- log.warn(
87
- { callSessionId, isOutbound },
88
- "Cannot resolve counterparty: no phone number on call session",
89
- );
90
- return null;
91
- }
92
-
93
- return {
94
- phoneNumber,
95
- displayIdentifier: phoneNumber,
96
- };
97
- }
98
-
99
- // ---------------------------------------------------------------------------
100
- // Action dispatchers
101
- // ---------------------------------------------------------------------------
102
-
103
- /**
104
- * Start an outbound call to the counterparty with context about the
105
- * guardian's answer. Uses the existing call start domain flow.
106
- */
107
- async function executeCallBack(
108
- request: GuardianActionRequest,
109
- counterparty: CounterpartyInfo,
110
- ): Promise<{ ok: true } | { ok: false; error: string }> {
111
- try {
112
- const task = request.lateAnswerText
113
- ? `Call back regarding a question that was asked earlier: "${request.questionText}". The guardian has provided an answer: "${request.lateAnswerText}". Relay this answer to the person.`
114
- : `Call back regarding a question that was asked earlier: "${request.questionText}". The guardian wants to follow up with them.`;
115
-
116
- const callbackContext = [
117
- `This is a follow-up callback. The person called earlier and asked: "${request.questionText}".`,
118
- request.lateAnswerText
119
- ? `The guardian's answer is: "${request.lateAnswerText}".`
120
- : null,
121
- "Relay this information naturally and ask if they need anything else.",
122
- ]
123
- .filter(Boolean)
124
- .join(" ");
125
-
126
- // Create a conversation for the callback call
127
- const convKey = `followup-callback:${request.id}`;
128
- const { conversationId } = getOrCreateConversation(convKey);
129
-
130
- const result = await startCall({
131
- phoneNumber: counterparty.phoneNumber,
132
- task,
133
- context: callbackContext,
134
- conversationId,
135
- assistantId: DAEMON_INTERNAL_ASSISTANT_ID,
136
- });
137
-
138
- if (!result.ok) {
139
- log.warn(
140
- { requestId: request.id, error: result.error },
141
- "Failed to start follow-up callback call",
142
- );
143
- return { ok: false, error: result.error };
144
- }
145
-
146
- log.info(
147
- {
148
- requestId: request.id,
149
- callSessionId: result.session.id,
150
- counterpartyPhone: counterparty.phoneNumber,
151
- },
152
- "Follow-up call_back initiated successfully",
153
- );
154
-
155
- return { ok: true };
156
- } catch (err) {
157
- const message = err instanceof Error ? err.message : String(err);
158
- log.error(
159
- {
160
- err,
161
- requestId: request.id,
162
- counterpartyPhone: counterparty.phoneNumber,
163
- },
164
- "Failed to start follow-up callback call",
165
- );
166
- return { ok: false, error: message };
167
- }
168
- }
169
-
170
- // ---------------------------------------------------------------------------
171
- // Public API
172
- // ---------------------------------------------------------------------------
173
-
174
- /**
175
- * Execute a follow-up action after the conversation engine has classified
176
- * the guardian's intent as call_back and the follow-up
177
- * state has been transitioned to `dispatching`.
178
- *
179
- * On success: finalizes the follow-up to `completed` and returns
180
- * generated confirmation text for the guardian.
181
- *
182
- * On failure: finalizes the follow-up to `failed` and returns
183
- * generated error text for the guardian.
184
- */
185
- export async function executeFollowupAction(
186
- requestId: string,
187
- action: FollowupAction,
188
- generator?: GuardianActionCopyGenerator,
189
- ): Promise<FollowupExecutionResult> {
190
- const request = getGuardianActionRequest(requestId);
191
- if (!request) {
192
- const errorText = await composeGuardianActionMessageGenerative(
193
- {
194
- scenario: "followup_action_failed",
195
- failureReason: "The follow-up request could not be found.",
196
- },
197
- {},
198
- generator,
199
- );
200
- return {
201
- ok: false,
202
- action,
203
- guardianReplyText: errorText,
204
- error: "Request not found",
205
- };
206
- }
207
-
208
- if (request.followupState !== "dispatching") {
209
- const errorText = await composeGuardianActionMessageGenerative(
210
- {
211
- scenario: "followup_action_failed",
212
- failureReason:
213
- "This follow-up is no longer in a valid state for execution.",
214
- },
215
- {},
216
- generator,
217
- );
218
- return {
219
- ok: false,
220
- action,
221
- guardianReplyText: errorText,
222
- error: `Invalid followup state: ${request.followupState}`,
223
- };
224
- }
225
-
226
- // Resolve the counterparty from the original call session
227
- const counterparty = resolveCounterparty(request.callSessionId);
228
- if (!counterparty) {
229
- finalizeFollowup(requestId, "failed");
230
- const errorText = await composeGuardianActionMessageGenerative(
231
- {
232
- scenario: "followup_action_failed",
233
- failureReason: "I couldn't find the caller's contact information.",
234
- },
235
- {},
236
- generator,
237
- );
238
- return {
239
- ok: false,
240
- action,
241
- guardianReplyText: errorText,
242
- error: "Counterparty not found",
243
- };
244
- }
245
-
246
- // Execute the action
247
- let actionResult: { ok: true } | { ok: false; error: string };
248
-
249
- if (action === "call_back") {
250
- actionResult = await executeCallBack(request, counterparty);
251
- } else {
252
- // decline is already handled in M5 — should not reach the executor.
253
- finalizeFollowup(requestId, "failed");
254
- const errorText = await composeGuardianActionMessageGenerative(
255
- {
256
- scenario: "followup_action_failed",
257
- failureReason: "An unexpected action was requested.",
258
- },
259
- {},
260
- generator,
261
- );
262
- return {
263
- ok: false,
264
- action,
265
- guardianReplyText: errorText,
266
- error: `Unsupported action: ${action}`,
267
- };
268
- }
269
-
270
- if (actionResult.ok) {
271
- finalizeFollowup(requestId, "completed");
272
-
273
- const scenario = "followup_call_started" as const;
274
- const confirmText = await composeGuardianActionMessageGenerative(
275
- {
276
- scenario,
277
- counterpartyPhone: counterparty.phoneNumber,
278
- questionText: request.questionText,
279
- lateAnswerText: request.lateAnswerText ?? undefined,
280
- },
281
- {},
282
- generator,
283
- );
284
-
285
- return { ok: true, action, guardianReplyText: confirmText };
286
- }
287
-
288
- // Action failed
289
- finalizeFollowup(requestId, "failed");
290
- const errorText = await composeGuardianActionMessageGenerative(
291
- {
292
- scenario: "followup_action_failed",
293
- failureReason: actionResult.error,
294
- counterpartyPhone: counterparty.phoneNumber,
295
- },
296
- {},
297
- generator,
298
- );
299
-
300
- return {
301
- ok: false,
302
- action,
303
- guardianReplyText: errorText,
304
- error: actionResult.error,
305
- };
306
- }