@songsid/agend 2.2.0-alpha.9 → 2.2.0-beta.10

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 (377) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +16 -9
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +21 -9
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +47 -0
  9. package/dist/agent-whats-new.js +147 -0
  10. package/dist/agent-whats-new.js.map +1 -0
  11. package/dist/auth-api.d.ts +2 -2
  12. package/dist/auth-api.js +4 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +3 -0
  15. package/dist/backend/antigravity.js +9 -0
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/claude-carry.d.ts +22 -0
  18. package/dist/backend/claude-carry.js +170 -0
  19. package/dist/backend/claude-carry.js.map +1 -0
  20. package/dist/backend/claude-code.d.ts +65 -2
  21. package/dist/backend/claude-code.js +226 -33
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/codex-metadata.d.ts +33 -0
  24. package/dist/backend/codex-metadata.js +122 -18
  25. package/dist/backend/codex-metadata.js.map +1 -1
  26. package/dist/backend/codex.d.ts +3 -0
  27. package/dist/backend/codex.js +17 -1
  28. package/dist/backend/codex.js.map +1 -1
  29. package/dist/backend/credential-profile.d.ts +40 -3
  30. package/dist/backend/credential-profile.js +113 -5
  31. package/dist/backend/credential-profile.js.map +1 -1
  32. package/dist/backend/effort-metadata.d.ts +1 -1
  33. package/dist/backend/effort-metadata.js +3 -2
  34. package/dist/backend/effort-metadata.js.map +1 -1
  35. package/dist/backend/grok.d.ts +4 -0
  36. package/dist/backend/grok.js +14 -0
  37. package/dist/backend/grok.js.map +1 -1
  38. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  39. package/dist/backend/kiro-engine-ledger.js +20 -0
  40. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  41. package/dist/backend/kiro-identity.d.ts +6 -0
  42. package/dist/backend/kiro-identity.js +19 -5
  43. package/dist/backend/kiro-identity.js.map +1 -1
  44. package/dist/backend/kiro.d.ts +39 -3
  45. package/dist/backend/kiro.js +143 -19
  46. package/dist/backend/kiro.js.map +1 -1
  47. package/dist/backend/launch-effort.d.ts +33 -0
  48. package/dist/backend/launch-effort.js +123 -0
  49. package/dist/backend/launch-effort.js.map +1 -0
  50. package/dist/backend/muse.d.ts +84 -0
  51. package/dist/backend/muse.js +449 -53
  52. package/dist/backend/muse.js.map +1 -1
  53. package/dist/backend/types.d.ts +33 -4
  54. package/dist/backend/types.js +2 -1
  55. package/dist/backend/types.js.map +1 -1
  56. package/dist/backend-command.d.ts +91 -0
  57. package/dist/backend-command.js +122 -0
  58. package/dist/backend-command.js.map +1 -0
  59. package/dist/background-work-ended.d.ts +18 -0
  60. package/dist/background-work-ended.js +30 -0
  61. package/dist/background-work-ended.js.map +1 -0
  62. package/dist/cache-service.d.ts +2 -0
  63. package/dist/cache-service.js +2 -1
  64. package/dist/cache-service.js.map +1 -1
  65. package/dist/channel/adapters/discord.d.ts +26 -1
  66. package/dist/channel/adapters/discord.js +98 -3
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +26 -2
  69. package/dist/channel/adapters/telegram.js +123 -19
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/mcp-server.js +9 -1
  72. package/dist/channel/mcp-server.js.map +1 -1
  73. package/dist/channel/mcp-tools.d.ts +11 -0
  74. package/dist/channel/mcp-tools.js +24 -13
  75. package/dist/channel/mcp-tools.js.map +1 -1
  76. package/dist/channel/message-edit-errors.d.ts +5 -0
  77. package/dist/channel/message-edit-errors.js +24 -0
  78. package/dist/channel/message-edit-errors.js.map +1 -0
  79. package/dist/channel/telegram-markdown.d.ts +6 -0
  80. package/dist/channel/telegram-markdown.js +104 -0
  81. package/dist/channel/telegram-markdown.js.map +1 -0
  82. package/dist/channel/tool-router.js +9 -3
  83. package/dist/channel/tool-router.js.map +1 -1
  84. package/dist/channel/types.d.ts +41 -3
  85. package/dist/cli.js +133 -28
  86. package/dist/cli.js.map +1 -1
  87. package/dist/command-table.d.ts +2 -2
  88. package/dist/command-table.js +6 -3
  89. package/dist/command-table.js.map +1 -1
  90. package/dist/config-validator.js +59 -0
  91. package/dist/config-validator.js.map +1 -1
  92. package/dist/cross-instance-envelope.d.ts +15 -2
  93. package/dist/cross-instance-envelope.js +29 -5
  94. package/dist/cross-instance-envelope.js.map +1 -1
  95. package/dist/daemon.d.ts +156 -1
  96. package/dist/daemon.js +764 -89
  97. package/dist/daemon.js.map +1 -1
  98. package/dist/delivery-outbox.d.ts +62 -7
  99. package/dist/delivery-outbox.js +309 -22
  100. package/dist/delivery-outbox.js.map +1 -1
  101. package/dist/delivery-reconciliation.d.ts +18 -1
  102. package/dist/delivery-reconciliation.js +67 -2
  103. package/dist/delivery-reconciliation.js.map +1 -1
  104. package/dist/fleet-context.d.ts +10 -1
  105. package/dist/fleet-manager.d.ts +352 -10
  106. package/dist/fleet-manager.js +1925 -175
  107. package/dist/fleet-manager.js.map +1 -1
  108. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  111. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  112. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  113. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  114. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  115. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  116. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  117. package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
  118. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  119. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  120. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  121. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  122. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  123. package/dist/general-knowledge/steering/core-rules.md +8 -7
  124. package/dist/host-memory.d.ts +10 -0
  125. package/dist/host-memory.js +33 -1
  126. package/dist/host-memory.js.map +1 -1
  127. package/dist/human-delivery-receipts.d.ts +83 -0
  128. package/dist/human-delivery-receipts.js +194 -0
  129. package/dist/human-delivery-receipts.js.map +1 -0
  130. package/dist/instance-avatar.d.ts +36 -0
  131. package/dist/instance-avatar.js +57 -0
  132. package/dist/instance-avatar.js.map +1 -0
  133. package/dist/instance-config-impact.js +6 -0
  134. package/dist/instance-config-impact.js.map +1 -1
  135. package/dist/instance-lifecycle.d.ts +29 -6
  136. package/dist/instance-lifecycle.js +79 -28
  137. package/dist/instance-lifecycle.js.map +1 -1
  138. package/dist/instructions.d.ts +3 -1
  139. package/dist/instructions.js +11 -8
  140. package/dist/instructions.js.map +1 -1
  141. package/dist/kiro-log-gate.d.ts +42 -0
  142. package/dist/kiro-log-gate.js +39 -0
  143. package/dist/kiro-log-gate.js.map +1 -0
  144. package/dist/locale.js +204 -44
  145. package/dist/locale.js.map +1 -1
  146. package/dist/log-service.d.ts +99 -0
  147. package/dist/log-service.js +243 -0
  148. package/dist/log-service.js.map +1 -0
  149. package/dist/log-steps.d.ts +169 -0
  150. package/dist/log-steps.js +858 -0
  151. package/dist/log-steps.js.map +1 -0
  152. package/dist/login-controller.d.ts +20 -3
  153. package/dist/login-controller.js +71 -19
  154. package/dist/login-controller.js.map +1 -1
  155. package/dist/login-flows.d.ts +5 -2
  156. package/dist/login-flows.js +5 -4
  157. package/dist/login-flows.js.map +1 -1
  158. package/dist/memory-pressure.d.ts +13 -1
  159. package/dist/memory-pressure.js +50 -10
  160. package/dist/memory-pressure.js.map +1 -1
  161. package/dist/needs-you-hub.js +2 -0
  162. package/dist/needs-you-hub.js.map +1 -1
  163. package/dist/needs-you-live.d.ts +6 -0
  164. package/dist/needs-you-live.js +23 -3
  165. package/dist/needs-you-live.js.map +1 -1
  166. package/dist/next-steps.js +1 -1
  167. package/dist/next-steps.js.map +1 -1
  168. package/dist/outbound-handlers.d.ts +100 -0
  169. package/dist/outbound-handlers.js +263 -96
  170. package/dist/outbound-handlers.js.map +1 -1
  171. package/dist/outbound-schemas.d.ts +26 -15
  172. package/dist/outbound-schemas.js +24 -7
  173. package/dist/outbound-schemas.js.map +1 -1
  174. package/dist/public-link-progress.d.ts +1 -1
  175. package/dist/public-link-progress.js +8 -2
  176. package/dist/public-link-progress.js.map +1 -1
  177. package/dist/public-preview.d.ts +101 -0
  178. package/dist/public-preview.js +236 -0
  179. package/dist/public-preview.js.map +1 -0
  180. package/dist/public-web-gateway.js +11 -4
  181. package/dist/public-web-gateway.js.map +1 -1
  182. package/dist/public-web-link.d.ts +24 -1
  183. package/dist/public-web-link.js +63 -9
  184. package/dist/public-web-link.js.map +1 -1
  185. package/dist/reply-buttons-controller.d.ts +3 -0
  186. package/dist/reply-buttons-controller.js +3 -2
  187. package/dist/reply-buttons-controller.js.map +1 -1
  188. package/dist/restart-guard.d.ts +6 -3
  189. package/dist/restart-guard.js +6 -3
  190. package/dist/restart-guard.js.map +1 -1
  191. package/dist/restart-progress.d.ts +33 -0
  192. package/dist/restart-progress.js +30 -0
  193. package/dist/restart-progress.js.map +1 -1
  194. package/dist/service-activation.d.ts +15 -7
  195. package/dist/service-activation.js +46 -29
  196. package/dist/service-activation.js.map +1 -1
  197. package/dist/service-installer.d.ts +31 -0
  198. package/dist/service-installer.js +41 -2
  199. package/dist/service-installer.js.map +1 -1
  200. package/dist/settings-api.js +85 -25
  201. package/dist/settings-api.js.map +1 -1
  202. package/dist/settings-change.js +72 -1
  203. package/dist/settings-change.js.map +1 -1
  204. package/dist/settings-effect.js +28 -4
  205. package/dist/settings-effect.js.map +1 -1
  206. package/dist/settings-http-confirmation.js +7 -0
  207. package/dist/settings-http-confirmation.js.map +1 -1
  208. package/dist/setup-form.d.ts +1 -1
  209. package/dist/setup-form.js +1 -1
  210. package/dist/spawn-gate.js +5 -1
  211. package/dist/spawn-gate.js.map +1 -1
  212. package/dist/spawn-hold-reply.d.ts +12 -0
  213. package/dist/spawn-hold-reply.js +39 -0
  214. package/dist/spawn-hold-reply.js.map +1 -0
  215. package/dist/status-emojis.d.ts +10 -3
  216. package/dist/status-emojis.js +11 -2
  217. package/dist/status-emojis.js.map +1 -1
  218. package/dist/systemd-peer-bus.d.ts +95 -0
  219. package/dist/systemd-peer-bus.js +430 -0
  220. package/dist/systemd-peer-bus.js.map +1 -0
  221. package/dist/systemd-runtime.d.ts +27 -1
  222. package/dist/systemd-runtime.js +69 -7
  223. package/dist/systemd-runtime.js.map +1 -1
  224. package/dist/tips.js +4 -4
  225. package/dist/tips.js.map +1 -1
  226. package/dist/tool-progress.js +3 -2
  227. package/dist/tool-progress.js.map +1 -1
  228. package/dist/topic-commands.d.ts +42 -2
  229. package/dist/topic-commands.js +92 -7
  230. package/dist/topic-commands.js.map +1 -1
  231. package/dist/transcript-delta-reader.d.ts +3 -3
  232. package/dist/transcript-delta-reader.js +8 -8
  233. package/dist/transcript-delta-reader.js.map +1 -1
  234. package/dist/transcript-jsonl.d.ts +80 -3
  235. package/dist/transcript-jsonl.js +147 -10
  236. package/dist/transcript-jsonl.js.map +1 -1
  237. package/dist/transcript-monitor.d.ts +19 -0
  238. package/dist/transcript-monitor.js +87 -15
  239. package/dist/transcript-monitor.js.map +1 -1
  240. package/dist/transcript-sources.d.ts +78 -0
  241. package/dist/transcript-sources.js +375 -64
  242. package/dist/transcript-sources.js.map +1 -1
  243. package/dist/transcript-turns.d.ts +15 -2
  244. package/dist/transcript-turns.js +15 -2
  245. package/dist/transcript-turns.js.map +1 -1
  246. package/dist/tunnel/cloudflared.d.ts +7 -15
  247. package/dist/tunnel/cloudflared.js +36 -1
  248. package/dist/tunnel/cloudflared.js.map +1 -1
  249. package/dist/tunnel/purpose-lane.d.ts +5 -1
  250. package/dist/tunnel/purpose-lane.js +14 -7
  251. package/dist/tunnel/purpose-lane.js.map +1 -1
  252. package/dist/tunnel/types.d.ts +5 -1
  253. package/dist/tunnel/types.js.map +1 -1
  254. package/dist/turn-resume.d.ts +2 -0
  255. package/dist/turn-resume.js +1 -1
  256. package/dist/turn-resume.js.map +1 -1
  257. package/dist/types.d.ts +25 -0
  258. package/dist/ui/chat-commands.js +1 -0
  259. package/dist/ui/chat-emojis.js +33 -0
  260. package/dist/ui/chat-mentions.js +63 -0
  261. package/dist/ui/chat-render.js +42 -2
  262. package/dist/ui/chat-store.js +2 -2
  263. package/dist/ui/chat-strings.js +38 -20
  264. package/dist/ui/chat-thread.js +69 -12
  265. package/dist/ui/chat-time.js +80 -0
  266. package/dist/ui/panel-chat.js +39 -10
  267. package/dist/ui/panel-details.js +2 -0
  268. package/dist/ui/panel-fleet.js +199 -57
  269. package/dist/ui/panel-log.js +428 -0
  270. package/dist/ui/panel-needs.js +6 -2
  271. package/dist/ui/panel-settings.js +137 -32
  272. package/dist/ui/preview-panel.js +55 -15
  273. package/dist/ui/preview-status.js +28 -0
  274. package/dist/ui/preview.js +44 -3
  275. package/dist/ui/settings-dialogs.js +7 -2
  276. package/dist/ui/settings-model.js +2 -1
  277. package/dist/ui/settings-strings.js +46 -4
  278. package/dist/ui/settings-token.js +1 -1
  279. package/dist/ui/shared/app-i18n.js +43 -17
  280. package/dist/ui/shared/app-prefs.js +250 -0
  281. package/dist/ui/shared/app-route.js +7 -2
  282. package/dist/ui/shared/app-shell.js +165 -8
  283. package/dist/ui/shared/app-store.js +5 -1
  284. package/dist/ui/shared/app-stream.js +23 -5
  285. package/dist/ui/shared/app.css +159 -23
  286. package/dist/ui/shared/app.js +11 -1
  287. package/dist/ui/shared/fonts/README.md +1 -1
  288. package/dist/ui/shared/header-tools.js +55 -5
  289. package/dist/ui/shared/instance-nav.js +241 -45
  290. package/dist/ui/shared/instance-switch.js +4 -4
  291. package/dist/ui/shared/panel-view.js +6 -0
  292. package/dist/ui/shared/signin.js +7 -7
  293. package/dist/ui/shared/theme.js +39 -7
  294. package/dist/ui/shared/tokens.css +10 -1
  295. package/dist/ui/shared/ui-icons.js +1 -0
  296. package/dist/ui/shared/usage-dialog.js +23 -5
  297. package/dist/ui/shared/view-strings.js +32 -4
  298. package/dist/ui/signin.html +1 -1
  299. package/dist/unrecognised-prompt.d.ts +23 -0
  300. package/dist/unrecognised-prompt.js +74 -0
  301. package/dist/unrecognised-prompt.js.map +1 -0
  302. package/dist/update-install.d.ts +28 -2
  303. package/dist/update-install.js +22 -4
  304. package/dist/update-install.js.map +1 -1
  305. package/dist/update-marker.d.ts +51 -0
  306. package/dist/update-marker.js +61 -0
  307. package/dist/update-marker.js.map +1 -1
  308. package/dist/update-preflight.d.ts +100 -0
  309. package/dist/update-preflight.js +264 -0
  310. package/dist/update-preflight.js.map +1 -0
  311. package/dist/updater-caller.d.ts +31 -0
  312. package/dist/updater-caller.js +109 -0
  313. package/dist/updater-caller.js.map +1 -0
  314. package/dist/usage/i18n-keys.d.ts +1 -1
  315. package/dist/usage/i18n-keys.js +1 -1
  316. package/dist/usage/i18n-keys.js.map +1 -1
  317. package/dist/usage/providers.d.ts +34 -5
  318. package/dist/usage/providers.js +160 -27
  319. package/dist/usage/providers.js.map +1 -1
  320. package/dist/usage/statusline-usage.d.ts +6 -2
  321. package/dist/usage/statusline-usage.js +10 -4
  322. package/dist/usage/statusline-usage.js.map +1 -1
  323. package/dist/view-api.d.ts +1 -1
  324. package/dist/view-api.js +38 -8
  325. package/dist/view-api.js.map +1 -1
  326. package/dist/wake-coordinator.d.ts +11 -0
  327. package/dist/wake-coordinator.js +38 -4
  328. package/dist/wake-coordinator.js.map +1 -1
  329. package/dist/web-api.d.ts +49 -2
  330. package/dist/web-api.js +421 -31
  331. package/dist/web-api.js.map +1 -1
  332. package/dist/web-auth.d.ts +2 -2
  333. package/dist/web-auth.js +9 -4
  334. package/dist/web-auth.js.map +1 -1
  335. package/dist/web-commands.d.ts +27 -4
  336. package/dist/web-commands.js +19 -4
  337. package/dist/web-commands.js.map +1 -1
  338. package/dist/web-host-guard.d.ts +3 -11
  339. package/dist/web-host-guard.js +3 -12
  340. package/dist/web-host-guard.js.map +1 -1
  341. package/dist/web-images.d.ts +47 -0
  342. package/dist/web-images.js +169 -0
  343. package/dist/web-images.js.map +1 -0
  344. package/dist/web-mentions.d.ts +44 -0
  345. package/dist/web-mentions.js +75 -0
  346. package/dist/web-mentions.js.map +1 -0
  347. package/dist/web-prefs.d.ts +63 -0
  348. package/dist/web-prefs.js +265 -0
  349. package/dist/web-prefs.js.map +1 -0
  350. package/dist/web-preview.d.ts +14 -1
  351. package/dist/web-preview.js +35 -13
  352. package/dist/web-preview.js.map +1 -1
  353. package/dist/web-reply-files.d.ts +45 -0
  354. package/dist/web-reply-files.js +223 -0
  355. package/dist/web-reply-files.js.map +1 -0
  356. package/dist/web-schedules.d.ts +12 -0
  357. package/dist/web-schedules.js +34 -0
  358. package/dist/web-schedules.js.map +1 -0
  359. package/dist/web-server-drain.d.ts +10 -0
  360. package/dist/web-server-drain.js +50 -0
  361. package/dist/web-server-drain.js.map +1 -0
  362. package/dist/web-session.d.ts +36 -1
  363. package/dist/web-session.js +53 -6
  364. package/dist/web-session.js.map +1 -1
  365. package/dist/web-settings-patch.d.ts +23 -0
  366. package/dist/web-settings-patch.js +77 -0
  367. package/dist/web-settings-patch.js.map +1 -0
  368. package/dist/web-shell-routes.d.ts +5 -1
  369. package/dist/web-shell-routes.js +10 -1
  370. package/dist/web-shell-routes.js.map +1 -1
  371. package/dist/web-upload.d.ts +17 -4
  372. package/dist/web-upload.js +64 -13
  373. package/dist/web-upload.js.map +1 -1
  374. package/dist/workflow-templates/default.md +1 -1
  375. package/package.json +1 -1
  376. package/templates/launchd.plist.ejs +3 -1
  377. package/templates/systemd.service.ejs +2 -1
@@ -5,9 +5,23 @@ import { EventEmitter } from "node:events";
5
5
  import { chmodSync, mkdirSync } from "node:fs";
6
6
  import { dirname } from "node:path";
7
7
  import { t } from "./locale.js";
8
+ import { initializeHumanReceipts, createHumanReceipt, getHumanReceipt, bindHumanReceipt, settleHumanReceipt, queryHumanReceipt } from "./human-delivery-receipts.js";
8
9
  export const DURABLE_DELIVERY_MAX_ATTEMPTS = 8;
9
10
  export const DURABLE_DELIVERY_MAX_AGE_MS = 24 * 60 * 60_000;
10
11
  export const DURABLE_DELIVERY_ABORT_BACKOFF_MS = 5_000;
12
+ export const DEFAULT_REPLY_OBLIGATION_TTL_MS = 24 * 60 * 60_000;
13
+ const RESTART_REPLY_DISPLAY_LIMIT = 10;
14
+ /** Keep each preview on one physical line and bound even an arbitrary correlation ID. */
15
+ function replyPreview(value) {
16
+ const escaped = value.replace(/[\u0000-\u001f\u007f]/g, c => JSON.stringify(c).slice(1, -1));
17
+ const bytes = Buffer.from(escaped);
18
+ if (bytes.length <= 256)
19
+ return escaped;
20
+ let end = 253;
21
+ while ((bytes[end] & 0xc0) === 0x80)
22
+ end--;
23
+ return bytes.subarray(0, end).toString("utf8") + "…";
24
+ }
11
25
  /** The one mapping from validated tool/CLI arguments to a selector. */
12
26
  export function deliveryStatusSelector(args) {
13
27
  const page = { ...(args.limit !== undefined ? { limit: args.limit } : {}), ...(args.cursor ? { cursor: args.cursor } : {}) };
@@ -54,12 +68,21 @@ function payloadMessageId(payload) {
54
68
  const id = meta && typeof meta === "object" ? meta.message_id : undefined;
55
69
  return typeof id === "string" && id.length > 0 ? id : null;
56
70
  }
57
- /** The exact statement `needsAttention` runs; exported so the plan test checks this text and no copy of it. */
71
+ /**
72
+ * The exact statement `needsAttention` runs; exported so the plan test checks this text and no copy of it. A silent
73
+ * schedule's raw paste is never an item (#1647): nobody can act on it, the next run goes ahead anyway, and the fleet's
74
+ * notice to the leader (with the cause) is its whole report. Every human-originated row stays.
75
+ */
58
76
  export const NEEDS_ATTENTION_SQL = `
59
77
  SELECT delivery_id, state, source_instance, target_instance, kind, finished_at, last_error
60
78
  FROM deliveries INDEXED BY idx_delivery_attention
61
79
  WHERE state IN ('uncertain','failed') AND acknowledged_at IS NULL AND finished_at >= ?
80
+ AND NOT (kind = 'raw_paste' AND source_key LIKE 'schedule:%')
62
81
  ORDER BY finished_at DESC LIMIT ?`;
82
+ /** A silent schedule's raw paste (fleet-manager admits it as `raw_paste` under a `schedule:` source key). */
83
+ export function isSilentScheduleDelivery(row) {
84
+ return row.kind === "raw_paste" && (row.sourceKey ?? "").startsWith("schedule:");
85
+ }
63
86
  function mapRow(row) {
64
87
  return {
65
88
  deliveryId: row.delivery_id,
@@ -94,7 +117,9 @@ function safeErrorSummary(error, state) {
94
117
  return t("delivery.error_expired");
95
118
  if (normalized.includes("attempt limit"))
96
119
  return t("delivery.error_attempts");
97
- if (normalized.includes("reconcil") || normalized.includes("uncertain") || normalized.includes("restart") || normalized.includes("unverified") || normalized.includes("unverifiable")) {
120
+ // `<phase>-proof:<proof>` is what a pane or transcript submission proof could not establish (#1647).
121
+ if (normalized.includes("reconcil") || normalized.includes("uncertain") || normalized.includes("restart") || normalized.includes("unverified") || normalized.includes("unverifiable")
122
+ || /-proof:/.test(normalized)) {
98
123
  // Retry guidance comes from the recorded outcome, not a historical diagnostic.
99
124
  if (state && safeToRetry(state))
100
125
  return t("delivery.error_recorded");
@@ -115,8 +140,9 @@ function hasTable(db, table) {
115
140
  function obligationFor(db, correlationId, requester, owner) {
116
141
  if (!correlationId || !hasTable(db, "reply_obligations"))
117
142
  return null;
143
+ const expiredAt = hasColumn(db, "reply_obligations", "expired_at") ? "expired_at" : "NULL AS expired_at";
118
144
  const row = db.prepare(`
119
- SELECT state, opened_at, last_asked_at, nudged_at, overdue_notified_at, answered_at FROM reply_obligations
145
+ SELECT state, opened_at, last_asked_at, nudged_at, overdue_notified_at, answered_at, ${expiredAt} FROM reply_obligations
120
146
  WHERE correlation_id=? AND requester_instance=? AND owner_instance=?
121
147
  `).get(correlationId, requester, owner);
122
148
  return row ?? null;
@@ -124,6 +150,25 @@ function obligationFor(db, correlationId, requester, owner) {
124
150
  function hasColumn(db, table, column) {
125
151
  return db.prepare(`PRAGMA table_info(${table})`).all().some(c => c.name === column);
126
152
  }
153
+ /** The same row ACL protects results, cursor anchors and forward admission. */
154
+ function deliveryVisibility(db, alias, caller) {
155
+ const grants = hasTable(db, "delivery_forward_edges");
156
+ return {
157
+ sql: `(${alias}.source_instance=? OR ${alias}.target_instance=?${grants
158
+ ? ` OR EXISTS (SELECT 1 FROM delivery_forward_edges g WHERE g.reference_kind='delivery' AND g.reference_id=${alias}.delivery_id AND g.receiver_instance=?)`
159
+ : ""})`,
160
+ params: grants ? [caller, caller, caller] : [caller, caller],
161
+ };
162
+ }
163
+ function forwardingEvidence(db, kind, id, caller) {
164
+ if (!hasTable(db, "delivery_forward_edges"))
165
+ return undefined;
166
+ const rows = db.prepare(`SELECT sender_instance AS source_instance, forwarding_message_id AS message_id,
167
+ created_at AS forwarded_at FROM delivery_forward_edges
168
+ WHERE reference_kind=? AND reference_id=? AND receiver_instance=? ORDER BY created_at DESC,forwarding_delivery_id DESC LIMIT 1`)
169
+ .all(kind, id, caller);
170
+ return rows.length ? rows : undefined;
171
+ }
127
172
  /** Derived from the stored payload, so it is the same for pre-#856 rows. */
128
173
  function rowContentEvidence(payloadJson, withContent) {
129
174
  let payload = null;
@@ -139,6 +184,9 @@ function rowContentEvidence(payloadJson, withContent) {
139
184
  };
140
185
  }
141
186
  function queryStatusPage(db, selector, callerInstance) {
187
+ const human = queryHumanReceipt(db, callerInstance, selector);
188
+ if (human)
189
+ return human;
142
190
  const where = [];
143
191
  const params = [];
144
192
  // An operator may read a database written before the column existed.
@@ -161,8 +209,9 @@ function queryStatusPage(db, selector, callerInstance) {
161
209
  params.push(selector.correlationId);
162
210
  }
163
211
  if (callerInstance !== null) {
164
- where.push("(d.source_instance = ? OR d.target_instance = ?)");
165
- params.push(callerInstance, callerInstance);
212
+ const acl = deliveryVisibility(db, "d", callerInstance);
213
+ where.push(acl.sql);
214
+ params.push(...acl.params);
166
215
  }
167
216
  if (selector.cursor) {
168
217
  const cursorScope = ["c.delivery_id = ?"];
@@ -180,8 +229,9 @@ function queryStatusPage(db, selector, callerInstance) {
180
229
  cursorParams.push(selector.correlationId);
181
230
  }
182
231
  if (callerInstance !== null) {
183
- cursorScope.push("(c.source_instance = ? OR c.target_instance = ?)");
184
- cursorParams.push(callerInstance, callerInstance);
232
+ const acl = deliveryVisibility(db, "c", callerInstance);
233
+ cursorScope.push(acl.sql);
234
+ cursorParams.push(...acl.params);
185
235
  }
186
236
  where.push(`d.created_seq > (SELECT c.created_seq FROM deliveries c WHERE ${cursorScope.join(" AND ")} LIMIT 1)`);
187
237
  params.push(...cursorParams);
@@ -199,7 +249,8 @@ function queryStatusPage(db, selector, callerInstance) {
199
249
  CASE WHEN d.state='submission_started' AND d.reconciliation_pending=1
200
250
  THEN 'reconciliation_pending' ELSE d.state END AS state,
201
251
  d.attempt_no,d.created_at,d.updated_at,d.last_error,d.payload_json,
202
- ${consumedColumns}, ${submissionMode} AS submission_mode
252
+ ${consumedColumns}, ${submissionMode} AS submission_mode,
253
+ ${hasColumn(db, "deliveries", "finished_at") ? "d.finished_at" : "NULL"} AS delivered_at
203
254
  FROM deliveries d
204
255
  WHERE ${where.join(" AND ")}
205
256
  ORDER BY d.created_seq
@@ -224,11 +275,14 @@ function queryStatusPage(db, selector, callerInstance) {
224
275
  safe_to_retry: safeToRetry(row.state),
225
276
  delivery_mode: row.kind === "steer" ? "steer" : "idle_queue",
226
277
  submission_mode: row.submission_mode ?? null,
278
+ delivered_at: row.state === "delivered" ? row.delivered_at : null,
227
279
  consumed_at: row.consumed_at ?? null,
228
280
  consumed_via: row.consumed_via ?? null,
229
281
  // Text only for the explicit verification query (#856), and only to the
230
282
  // row's own source/target; every other query keeps #982's redaction.
231
283
  ...rowContentEvidence(row.payload_json, callerInstance !== null && !!selector.messageId),
284
+ ...(callerInstance !== null && selector.messageId
285
+ ? { forwarded_by: forwardingEvidence(db, "delivery", row.delivery_id, callerInstance) } : {}),
232
286
  reply_obligation: obligationFor(db, row.correlation_id, row.source_instance, row.target_instance),
233
287
  })),
234
288
  next_cursor: hasMore ? visible.at(-1)?.delivery_id ?? null : null,
@@ -240,6 +294,7 @@ function mapObligation(row) {
240
294
  requestDeliveryId: row.request_delivery_id, openedAt: row.opened_at, lastAskedAt: row.last_asked_at, state: row.state,
241
295
  nudgedAt: row.nudged_at, overdueNotifiedAt: row.overdue_notified_at, answeredAt: row.answered_at,
242
296
  answeredDeliveryId: row.answered_delivery_id,
297
+ expiredAt: row.expired_at,
243
298
  };
244
299
  }
245
300
  /** Durable admission and fenced state transitions for the fleet delivery queue. */
@@ -346,20 +401,55 @@ export class DeliveryOutbox extends EventEmitter {
346
401
  request_delivery_id TEXT NOT NULL,
347
402
  opened_at TEXT NOT NULL,
348
403
  last_asked_at TEXT NOT NULL,
349
- state TEXT NOT NULL CHECK (state IN ('open','answered')),
404
+ state TEXT NOT NULL CHECK (state IN ('open','answered','expired')),
350
405
  nudged_at TEXT,
351
406
  overdue_notified_at TEXT,
352
407
  answered_at TEXT,
353
408
  answered_delivery_id TEXT,
409
+ expired_at TEXT,
354
410
  PRIMARY KEY (correlation_id, requester_instance, owner_instance)
355
411
  );
356
412
  CREATE INDEX IF NOT EXISTS idx_reply_obligation_owner_state ON reply_obligations(owner_instance, state);
413
+ -- A notice can be pruned while its boot is still alive. Keep the claim
414
+ -- independently: no notice FK/cascade and no request or message content.
415
+ CREATE TABLE IF NOT EXISTS reply_restart_markers (
416
+ owner_instance TEXT NOT NULL,
417
+ restart_id TEXT NOT NULL,
418
+ reminded_at TEXT NOT NULL,
419
+ PRIMARY KEY (owner_instance, restart_id)
420
+ );
357
421
  CREATE TABLE IF NOT EXISTS failure_notices (
358
422
  parent_delivery_id TEXT PRIMARY KEY,
359
423
  notice_delivery_id TEXT NOT NULL UNIQUE,
360
424
  created_at TEXT NOT NULL
361
425
  );
426
+ -- Retain a grant while its reference exists, even if its forwarding
427
+ -- notification is pruned. No message body or inferred authority here.
428
+ CREATE TABLE IF NOT EXISTS delivery_forward_edges (
429
+ reference_kind TEXT NOT NULL CHECK (reference_kind IN ('delivery','human')),
430
+ reference_id TEXT NOT NULL,
431
+ sender_instance TEXT NOT NULL,
432
+ receiver_instance TEXT NOT NULL,
433
+ forwarding_delivery_id TEXT NOT NULL,
434
+ forwarding_message_id TEXT NOT NULL,
435
+ created_at TEXT NOT NULL,
436
+ PRIMARY KEY (reference_kind,reference_id,receiver_instance,forwarding_delivery_id)
437
+ );
438
+ -- Independent of recipient/session/effective mode and notification retention.
439
+ -- A pruned admitted row cannot authorize a retry to create new grants.
440
+ CREATE TABLE IF NOT EXISTS delivery_forward_proposals (
441
+ source_instance TEXT NOT NULL,
442
+ operation_id TEXT NOT NULL,
443
+ proposal_sha256 TEXT NOT NULL,
444
+ delivery_id TEXT NOT NULL,
445
+ PRIMARY KEY (source_instance,operation_id)
446
+ );
447
+ CREATE INDEX IF NOT EXISTS idx_delivery_forward_admission ON delivery_forward_edges(forwarding_delivery_id);
448
+ CREATE INDEX IF NOT EXISTS idx_delivery_forward_latest ON delivery_forward_edges
449
+ (reference_kind,reference_id,receiver_instance,created_at DESC,forwarding_delivery_id DESC);
362
450
  `);
451
+ this.migrateReplyObligationExpiry();
452
+ initializeHumanReceipts(this.db);
363
453
  // Additive migration: existing Phase 1 databases keep their row identities,
364
454
  // foreign keys, and WAL while gaining reconciliation evidence columns.
365
455
  this.ensureColumn("deliveries", "reconciliation_pending", "INTEGER NOT NULL DEFAULT 0");
@@ -411,6 +501,30 @@ export class DeliveryOutbox extends EventEmitter {
411
501
  WHERE state IN ('uncertain','failed') AND acknowledged_at IS NULL`);
412
502
  this.db.pragma("user_version = 5");
413
503
  }
504
+ /** SQLite cannot widen a CHECK with ADD COLUMN; preserve all old rows in one transaction. */
505
+ migrateReplyObligationExpiry() {
506
+ const schema = this.db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='reply_obligations'")
507
+ .get();
508
+ if (!schema.sql.includes("'expired'")) {
509
+ this.db.transaction(() => {
510
+ this.db.exec(`CREATE TABLE reply_obligations_v1694 (
511
+ correlation_id TEXT NOT NULL, requester_instance TEXT NOT NULL, owner_instance TEXT NOT NULL,
512
+ request_delivery_id TEXT NOT NULL, opened_at TEXT NOT NULL, last_asked_at TEXT NOT NULL,
513
+ state TEXT NOT NULL CHECK (state IN ('open','answered','expired')),
514
+ nudged_at TEXT, overdue_notified_at TEXT, answered_at TEXT, answered_delivery_id TEXT, expired_at TEXT,
515
+ PRIMARY KEY (correlation_id, requester_instance, owner_instance)
516
+ );
517
+ INSERT INTO reply_obligations_v1694 SELECT correlation_id,requester_instance,owner_instance,
518
+ request_delivery_id,opened_at,last_asked_at,state,nudged_at,overdue_notified_at,
519
+ answered_at,answered_delivery_id,NULL FROM reply_obligations;`);
520
+ this.db.exec("DROP TABLE reply_obligations");
521
+ this.db.exec("ALTER TABLE reply_obligations_v1694 RENAME TO reply_obligations");
522
+ this.db.exec("CREATE INDEX idx_reply_obligation_owner_state ON reply_obligations(owner_instance,state)");
523
+ })();
524
+ }
525
+ this.ensureColumn("reply_obligations", "expired_at", "TEXT");
526
+ this.db.exec("CREATE INDEX IF NOT EXISTS idx_reply_obligation_state_asked ON reply_obligations(state,last_asked_at)");
527
+ }
414
528
  /**
415
529
  * #926: a delivered `requires_reply` request opens (or, asked again,
416
530
  * re-opens) the owner's obligation. Runs inside the delivered transition.
@@ -442,7 +556,7 @@ export class DeliveryOutbox extends EventEmitter {
442
556
  ON CONFLICT(correlation_id, requester_instance, owner_instance) DO UPDATE SET
443
557
  request_delivery_id=excluded.request_delivery_id, last_asked_at=excluded.last_asked_at,
444
558
  state=excluded.state, nudged_at=NULL, overdue_notified_at=NULL,
445
- answered_at=excluded.answered_at, answered_delivery_id=excluded.answered_delivery_id
559
+ answered_at=excluded.answered_at, answered_delivery_id=excluded.answered_delivery_id, expired_at=NULL
446
560
  `).run({
447
561
  cid: request.correlation_id, requester: request.source_instance, owner: request.target_instance,
448
562
  request: request.delivery_id, now,
@@ -462,8 +576,8 @@ export class DeliveryOutbox extends EventEmitter {
462
576
  /**
463
577
  * #926 safety net 1: remind an owner that has not answered. `turn-ended`
464
578
  * reminds once per ask, after the grace (a reply seconds after the ask is
465
- * normal); `restart` reminds once per restart, because the interrupted turn
466
- * does not resume on its own. Marked in the same transaction as the notice.
579
+ * normal); `restart` batches once per daemon boot, because the interrupted
580
+ * turn does not resume on its own. Marked in the same transaction as the notice.
467
581
  */
468
582
  remindReplyObligations(ownerInstance, opts) {
469
583
  const now = opts.now ?? new Date();
@@ -471,15 +585,42 @@ export class DeliveryOutbox extends EventEmitter {
471
585
  const graceCutoff = new Date(now.getTime() - opts.graceMs).toISOString();
472
586
  const sinceIso = (opts.since ?? now).toISOString();
473
587
  const inserted = this.db.transaction(() => {
474
- const due = this.db.prepare(opts.reason === "turn-ended"
475
- ? "SELECT * FROM reply_obligations WHERE owner_instance=? AND state='open' AND nudged_at IS NULL AND last_asked_at <= ?"
476
- : "SELECT * FROM reply_obligations WHERE owner_instance=? AND state='open' AND (nudged_at IS NULL OR nudged_at < ?)").all(ownerInstance, opts.reason === "turn-ended" ? graceCutoff : sinceIso);
588
+ if (opts.reason === "restart") {
589
+ const restartId = opts.restartId ?? sinceIso;
590
+ const key = `reply-pending:restart:${JSON.stringify([ownerInstance, restartId])}`;
591
+ if (this.db.prepare("SELECT 1 FROM reply_restart_markers WHERE owner_instance=? AND restart_id=?")
592
+ .get(ownerInstance, restartId))
593
+ return 0;
594
+ // Adopt an existing notice from the earlier implementation/rollback
595
+ // without stamping a new ask or producing another notice.
596
+ const prior = this.db.prepare("SELECT created_at FROM deliveries WHERE source_key=?").get(key);
597
+ if (prior) {
598
+ this.recordReplyRestart(ownerInstance, restartId, prior.created_at);
599
+ return 0;
600
+ }
601
+ const total = this.db.prepare("SELECT count(*) n FROM reply_obligations WHERE owner_instance=? AND state='open'")
602
+ .get(ownerInstance).n;
603
+ if (!total)
604
+ return 0;
605
+ const preview = this.db.prepare(`SELECT requester_instance,correlation_id,last_asked_at FROM reply_obligations
606
+ WHERE owner_instance=? AND state='open' ORDER BY last_asked_at,correlation_id,requester_instance LIMIT ?`)
607
+ .all(ownerInstance, RESTART_REPLY_DISPLAY_LIMIT);
608
+ const lines = preview.map(row => `${replyPreview(row.requester_instance)}; correlation_id=${replyPreview(row.correlation_id)}; asked=${replyPreview(row.last_asked_at)}`);
609
+ if (total > preview.length)
610
+ lines.push(t("delivery.reply_restart_more", total - preview.length));
611
+ const content = t("delivery.reply_restart") + "\n" + lines.join("\n");
612
+ if (!this.insertReplyObligationNotice(ownerInstance, null, key, content, nowIso))
613
+ return 0;
614
+ this.recordReplyRestart(ownerInstance, restartId, nowIso);
615
+ this.db.prepare("UPDATE reply_obligations SET nudged_at=? WHERE owner_instance=? AND state='open'").run(nowIso, ownerInstance);
616
+ return 1;
617
+ }
618
+ const due = this.db.prepare("SELECT * FROM reply_obligations WHERE owner_instance=? AND state='open' AND nudged_at IS NULL AND last_asked_at <= ?")
619
+ .all(ownerInstance, graceCutoff);
477
620
  let count = 0;
478
621
  for (const row of due) {
479
- const content = opts.reason === "turn-ended"
480
- ? t("delivery.reply_turn_ended", row.requester_instance, row.correlation_id, row.last_asked_at)
481
- : t("delivery.reply_restart", row.requester_instance, row.correlation_id, row.last_asked_at);
482
- const key = `reply-pending:${opts.reason}:${row.correlation_id}:${row.requester_instance}:${row.owner_instance}:${opts.reason === "turn-ended" ? row.last_asked_at : sinceIso}`;
622
+ const content = t("delivery.reply_turn_ended", row.requester_instance, row.correlation_id, row.last_asked_at);
623
+ const key = `reply-pending:turn-ended:${row.correlation_id}:${row.requester_instance}:${row.owner_instance}:${row.last_asked_at}`;
483
624
  if (this.insertReplyObligationNotice(row.owner_instance, row.correlation_id, key, content, nowIso))
484
625
  count++;
485
626
  this.db.prepare(`
@@ -492,6 +633,37 @@ export class DeliveryOutbox extends EventEmitter {
492
633
  this.emit("admitted");
493
634
  return inserted;
494
635
  }
636
+ recordReplyRestart(owner, restartId, now) {
637
+ this.db.prepare("INSERT INTO reply_restart_markers(owner_instance,restart_id,reminded_at) VALUES (?,?,?)")
638
+ .run(owner, restartId, now);
639
+ }
640
+ /** Expiry is terminal for this ask; a later delivered re-ask can reopen the same tuple. */
641
+ expireReplyObligations(opts = {}) {
642
+ const now = opts.now ?? new Date();
643
+ const ttl = opts.ttlMs !== undefined && Number.isFinite(opts.ttlMs) && opts.ttlMs > 0 ? opts.ttlMs : DEFAULT_REPLY_OBLIGATION_TTL_MS;
644
+ const cutoff = new Date(Math.max(-8.64e15, now.getTime() - ttl)).toISOString();
645
+ const nowIso = now.toISOString();
646
+ const expired = this.db.transaction(() => {
647
+ const due = this.db.prepare("SELECT * FROM reply_obligations WHERE state='open' AND last_asked_at<=? ORDER BY last_asked_at")
648
+ .all(cutoff);
649
+ for (const row of due) {
650
+ if (!row.overdue_notified_at) {
651
+ const key = `reply-overdue:${row.correlation_id}:${row.requester_instance}:${row.owner_instance}:${row.last_asked_at}`;
652
+ this.insertReplyObligationNotice(row.requester_instance, row.correlation_id, key, t("delivery.reply_expired", row.owner_instance, row.correlation_id, row.last_asked_at), nowIso);
653
+ }
654
+ this.db.prepare(`UPDATE reply_obligations SET state='expired',expired_at=?,overdue_notified_at=COALESCE(overdue_notified_at,?)
655
+ WHERE correlation_id=? AND requester_instance=? AND owner_instance=? AND state='open'`)
656
+ .run(nowIso, nowIso, row.correlation_id, row.requester_instance, row.owner_instance);
657
+ row.state = "expired";
658
+ row.expired_at = nowIso;
659
+ row.overdue_notified_at ??= nowIso;
660
+ }
661
+ return due.map(mapObligation);
662
+ })();
663
+ if (expired.length)
664
+ this.emit("admitted");
665
+ return expired;
666
+ }
495
667
  /**
496
668
  * #926 safety net 2 (poll-on-timeout, in code): tell the requester once when
497
669
  * the owner is not working and nothing has come back for `overdueMs` since
@@ -633,6 +805,74 @@ export class DeliveryOutbox extends EventEmitter {
633
805
  `);
634
806
  const select = this.db.prepare("SELECT * FROM deliveries WHERE source_key = ?");
635
807
  const transaction = this.db.transaction(() => {
808
+ const previous = select.get(input.sourceKey);
809
+ const forwards = input.forwards ?? [];
810
+ if (forwards.length > 16 || new Set(forwards).size !== forwards.length
811
+ || forwards.some(id => typeof id !== "string" || id.length === 0 || id.length > 256 || /[\s\u0000-\u001f\u007f]/.test(id))) {
812
+ throw new Error("Forwarded message verification is unavailable");
813
+ }
814
+ const pinned = this.db.prepare("SELECT proposal_sha256,delivery_id FROM delivery_forward_proposals WHERE source_instance=? AND operation_id=?")
815
+ .get(input.sourceInstance, input.operationId);
816
+ const meta = input.payload.meta;
817
+ const proposalHash = deliveryContentDigest(JSON.stringify({
818
+ target: input.targetInstance, session: input.targetSession ?? null,
819
+ requestedKind: input.requestedKind ?? input.kind, content: payloadContent(input.payload), forwards: [...forwards].sort(),
820
+ // Volatile envelope IDs/timestamps and generated correlations are not proposal input.
821
+ requestKind: meta?.request_kind ?? null, requiresReply: meta?.requires_reply ?? null,
822
+ taskSummary: meta?.task_summary ?? null, workingDirectory: meta?.working_directory ?? null, branch: meta?.branch ?? null,
823
+ }));
824
+ if (pinned) {
825
+ if (pinned.proposal_sha256 !== proposalHash)
826
+ throw new Error("Duplicate operation cannot change its forwarded request");
827
+ const admitted = this.db.prepare("SELECT * FROM deliveries WHERE delivery_id=?").get(pinned.delivery_id);
828
+ if (!admitted)
829
+ throw new Error("Previously admitted forwarded operation is unavailable");
830
+ return { delivery: mapRow(admitted), inserted: false }; // Reuse the original effective mode, never recompute it.
831
+ }
832
+ if (forwards.length && this.db.prepare("SELECT 1 FROM deliveries WHERE source_instance=? AND operation_id=? LIMIT 1")
833
+ .get(input.sourceInstance, input.operationId)) {
834
+ throw new Error("Duplicate operation cannot change its forwarded request");
835
+ }
836
+ if (previous) {
837
+ const previousPayload = JSON.parse(previous.payload_json);
838
+ const oldForwards = (typeof previousPayload.meta?.forwards === "string"
839
+ ? JSON.parse(previousPayload.meta.forwards) : []).sort();
840
+ if ((forwards.length || oldForwards.length)
841
+ && (JSON.stringify([...forwards].sort()) !== JSON.stringify(oldForwards)
842
+ || previous.source_instance !== input.sourceInstance || previous.target_instance !== input.targetInstance
843
+ || previous.target_session !== (input.targetSession ?? null) || previous.kind !== input.kind
844
+ || payloadContent(previousPayload) !== payloadContent(input.payload))) {
845
+ throw new Error("Duplicate operation cannot change its forwarded request");
846
+ }
847
+ return { delivery: mapRow(previous), inserted: false };
848
+ }
849
+ // Validate all references before inserting any delivery or edge. The
850
+ // authenticated sender can explicitly disclose a grant it already owns.
851
+ const acl = deliveryVisibility(this.db, "d", input.sourceInstance);
852
+ const references = forwards.map(messageId => {
853
+ if (messageId.startsWith("hmsg-")) {
854
+ const page = queryHumanReceipt(this.db, input.sourceInstance, { messageId });
855
+ if (!page || page.items.length !== 1 || page.items[0].state !== "delivered") {
856
+ throw new Error("Forwarded message verification is unavailable");
857
+ }
858
+ return { kind: "human", id: page.items[0].delivery_id, messageId };
859
+ }
860
+ const rows = this.db.prepare(`SELECT d.delivery_id,d.state FROM deliveries d
861
+ WHERE d.message_id=? AND ${acl.sql} LIMIT 2`).all(messageId, ...acl.params);
862
+ if (rows.length !== 1 || rows[0].state !== "delivered")
863
+ throw new Error("Forwarded message verification is unavailable");
864
+ return { kind: "delivery", id: rows[0].delivery_id, messageId };
865
+ });
866
+ // The typed admission list is authoritative; callers need not mirror it
867
+ // in display metadata, and retries recover its canonical stored set.
868
+ const payload = { ...input.payload };
869
+ if (forwards.length || (payload.meta && typeof payload.meta === "object" && Object.hasOwn(payload.meta, "forwards"))) {
870
+ const meta = { ...(payload.meta ?? {}) };
871
+ delete meta.forwards;
872
+ if (forwards.length)
873
+ meta.forwards = JSON.stringify(forwards);
874
+ payload.meta = meta;
875
+ }
636
876
  const result = insert.run({
637
877
  delivery_id: deliveryId,
638
878
  operation_id: input.operationId,
@@ -643,7 +883,7 @@ export class DeliveryOutbox extends EventEmitter {
643
883
  target_session: input.targetSession ?? null,
644
884
  kind: input.kind,
645
885
  correlation_id: input.correlationId ?? null,
646
- payload_json: JSON.stringify(input.payload),
886
+ payload_json: JSON.stringify(payload),
647
887
  message_id: payloadMessageId(input.payload),
648
888
  content_sha256: (content => content === null ? null : deliveryContentDigest(content))(payloadContent(input.payload)),
649
889
  created_at: now,
@@ -653,6 +893,18 @@ export class DeliveryOutbox extends EventEmitter {
653
893
  const row = select.get(input.sourceKey);
654
894
  if (!row)
655
895
  throw new Error("outbox admission committed without a readable row");
896
+ const forwardingMessageId = row.message_id;
897
+ if (references.length && !forwardingMessageId)
898
+ throw new Error("Forwarded message verification is unavailable");
899
+ const edge = this.db.prepare(`INSERT INTO delivery_forward_edges
900
+ (reference_kind,reference_id,sender_instance,receiver_instance,forwarding_delivery_id,forwarding_message_id,created_at)
901
+ VALUES (?,?,?,?,?,?,?)`);
902
+ for (const reference of references)
903
+ edge.run(reference.kind, reference.id, input.sourceInstance, input.targetInstance, row.delivery_id, forwardingMessageId, now);
904
+ if (references.length)
905
+ this.db.prepare(`INSERT INTO delivery_forward_proposals
906
+ (source_instance,operation_id,proposal_sha256,delivery_id) VALUES (?,?,?,?)`)
907
+ .run(input.sourceInstance, input.operationId, proposalHash, row.delivery_id);
656
908
  if (result.changes === 1)
657
909
  this.answerReplyObligation(row, now);
658
910
  return { delivery: mapRow(row), inserted: result.changes === 1 };
@@ -663,6 +915,20 @@ export class DeliveryOutbox extends EventEmitter {
663
915
  const row = this.db.prepare("SELECT * FROM deliveries WHERE delivery_id = ?").get(deliveryId);
664
916
  return row ? mapRow(row) : undefined;
665
917
  }
918
+ createHumanReceipt(input) {
919
+ return createHumanReceipt(this.db, input);
920
+ }
921
+ markHumanReceiptUncertain(id, target) {
922
+ this.db.prepare("UPDATE human_delivery_receipts SET state='uncertain',updated_at=? WHERE receipt_id=? AND target_instance=? AND state='queued'")
923
+ .run(new Date().toISOString(), id, target);
924
+ }
925
+ getHumanReceipt(id) { return getHumanReceipt(this.db, id); }
926
+ bindHumanReceipt(id, target, ownerKey, wire, managerBootId) {
927
+ return bindHumanReceipt(this.db, id, target, ownerKey, wire, managerBootId);
928
+ }
929
+ settleHumanReceipt(id, target, ownerKey, proof, currentOwnerKey) {
930
+ return settleHumanReceipt(this.db, id, target, ownerKey, proof, currentOwnerKey);
931
+ }
666
932
  getByOperation(sourceInstance, operationId) {
667
933
  const rows = this.db.prepare(`
668
934
  SELECT * FROM deliveries WHERE source_instance = ? AND operation_id = ? ORDER BY created_seq
@@ -1121,6 +1387,8 @@ export class DeliveryOutbox extends EventEmitter {
1121
1387
  recoverForBoot(managerBootId) {
1122
1388
  const now = new Date().toISOString();
1123
1389
  const transaction = this.db.transaction(() => {
1390
+ this.db.prepare("UPDATE human_delivery_receipts SET state='uncertain',updated_at=? WHERE state='queued' AND manager_boot_id<>?")
1391
+ .run(now, managerBootId);
1124
1392
  const queued = this.db.prepare(`
1125
1393
  UPDATE deliveries SET state='queued',manager_boot_id=NULL,target_daemon_boot_id=NULL,next_attempt_at=NULL,updated_at=?
1126
1394
  WHERE manager_boot_id IS NOT NULL AND manager_boot_id<>? AND state IN ('delivering','retry_wait')
@@ -1351,15 +1619,16 @@ export class DeliveryOutbox extends EventEmitter {
1351
1619
  insertPruned.run(row.delivery_id, row.source_instance, row.target_instance, now);
1352
1620
  // Delete child rows first.
1353
1621
  this.db.prepare(`DELETE FROM delivery_attempts WHERE delivery_id IN (${placeholders})`).run(...ids);
1354
- // Only closed reply_obligations (answered): open ones were excluded above.
1622
+ // Only closed reply_obligations (answered/expired): open ones were excluded above.
1355
1623
  if (hasTable(this.db, "reply_obligations")) {
1356
1624
  this.db.prepare(`
1357
1625
  DELETE FROM reply_obligations
1358
- WHERE request_delivery_id IN (${placeholders}) AND state = 'answered'
1626
+ WHERE request_delivery_id IN (${placeholders}) AND state IN ('answered','expired')
1359
1627
  `).run(...ids);
1360
1628
  }
1361
1629
  this.db.prepare(`DELETE FROM outcome_notices WHERE notice_delivery_id IN (${placeholders})`).run(...ids);
1362
1630
  this.db.prepare(`DELETE FROM failure_notices WHERE parent_delivery_id IN (${placeholders}) OR notice_delivery_id IN (${placeholders})`).run(...ids, ...ids);
1631
+ this.db.prepare(`DELETE FROM delivery_forward_edges WHERE reference_kind='delivery' AND reference_id IN (${placeholders})`).run(...ids);
1363
1632
  this.db.prepare(`DELETE FROM deliveries WHERE delivery_id IN (${placeholders})`).run(...ids);
1364
1633
  })();
1365
1634
  total += rows.length;
@@ -1368,6 +1637,24 @@ export class DeliveryOutbox extends EventEmitter {
1368
1637
  // Yield between chunks so the event loop can turn.
1369
1638
  await new Promise(resolve => setImmediate(resolve));
1370
1639
  }
1640
+ // Human rows are evidence only: retention never schedules or retries them.
1641
+ for (;;) {
1642
+ if (!this.db.open)
1643
+ break;
1644
+ const rows = this.db.prepare("SELECT receipt_id FROM human_delivery_receipts WHERE updated_at<? LIMIT 500").all(cutoff);
1645
+ if (!rows.length)
1646
+ break;
1647
+ this.db.transaction(() => {
1648
+ for (const row of rows) {
1649
+ this.db.prepare("DELETE FROM delivery_forward_edges WHERE reference_kind='human' AND reference_id=?").run(row.receipt_id);
1650
+ this.db.prepare("DELETE FROM human_delivery_receipts WHERE receipt_id=?").run(row.receipt_id);
1651
+ }
1652
+ })();
1653
+ total += rows.length;
1654
+ if (rows.length < CHUNK)
1655
+ break;
1656
+ await new Promise(resolve => setImmediate(resolve));
1657
+ }
1371
1658
  // Self-prune pruned_ids after 2× the retention window — chunked (Fix 4).
1372
1659
  if (this.db.open) {
1373
1660
  const oldCutoff = new Date(Date.now() - 2 * days * 24 * 60 * 60_000).toISOString();