mixdog 1.2.6 → 1.3.0

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 (246) hide show
  1. package/LICENSES/simple-icons-CC0-1.0.txt +30 -0
  2. package/NOTICE.md +19 -0
  3. package/package.json +4 -1
  4. package/scripts/check-model-recommendations.mjs +89 -0
  5. package/scripts/gate-local.mjs +7 -0
  6. package/scripts/lib/isolated-test-env.mjs +16 -0
  7. package/src/agents/reviewer/AGENT.md +4 -2
  8. package/src/agents/reviewer/agent.json +1 -1
  9. package/src/defaults/shipped-history.json +7 -0
  10. package/src/defaults/skills/browser-use/SKILL.md +5 -0
  11. package/src/defaults/skills/memory-management/SKILL.md +4 -1
  12. package/src/defaults/skills/setup/references/actions.md +11 -4
  13. package/src/defaults/skills/setup-guide/SKILL.md +175 -0
  14. package/src/defaults/skills/setup-guide/model-recommendations.json +333 -0
  15. package/src/lib/rules-builder.cjs +55 -6
  16. package/src/rules/agent/AGENT.md +4 -0
  17. package/src/rules/agent-mode/AGENT_MODE.md +24 -0
  18. package/src/rules/agent-mode/MIXY.md +21 -0
  19. package/src/rules/agent-mode/ROUTINE.md +6 -0
  20. package/src/rules/lead/LEAD.md +7 -3
  21. package/src/rules/shared/70-delivery.md +3 -0
  22. package/src/runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs +86 -0
  23. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +77 -14
  24. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +182 -13
  25. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +4 -2
  26. package/src/runtime/agent/orchestrator/mcp/agent-connectors.mjs +215 -0
  27. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +5 -4
  28. package/src/runtime/agent/orchestrator/mcp/client-transport.mjs +10 -1
  29. package/src/runtime/agent/orchestrator/mcp/client.mjs +115 -11
  30. package/src/runtime/agent/orchestrator/mcp/oauth-account.mjs +97 -0
  31. package/src/runtime/agent/orchestrator/mcp/oauth-client.mjs +49 -0
  32. package/src/runtime/agent/orchestrator/mcp/oauth-credentials.mjs +59 -9
  33. package/src/runtime/agent/orchestrator/mcp/oauth-login.mjs +32 -7
  34. package/src/runtime/agent/orchestrator/mcp/oauth-provider.mjs +45 -6
  35. package/src/runtime/agent/orchestrator/mcp/oauth.mjs +195 -6
  36. package/src/runtime/agent/orchestrator/mcp/server-connectors.mjs +17 -0
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-client-version.mjs +1 -1
  38. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +1 -0
  39. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +10 -3
  40. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +4 -0
  41. package/src/runtime/agent/orchestrator/providers/cursor-client-version.mjs +1 -1
  42. package/src/runtime/agent/orchestrator/providers/openai-error-contract.mjs +17 -12
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/runtime-core/ask-user-tool-defs.mjs +96 -1
  45. package/src/runtime/agent/orchestrator/runtime-core/context-status-shape.mjs +4 -0
  46. package/src/runtime/agent/orchestrator/runtime-core/extension-scopes.mjs +6 -4
  47. package/src/runtime/agent/orchestrator/runtime-core/orchestration.mjs +12 -0
  48. package/src/runtime/agent/orchestrator/runtime-core/plugin-mcp.mjs +1 -0
  49. package/src/runtime/agent/orchestrator/runtime-core/tool-bridge.mjs +12 -6
  50. package/src/runtime/agent/orchestrator/runtime-core/tool-catalog.mjs +32 -5
  51. package/src/runtime/agent/orchestrator/runtime-core/tool-defs.mjs +10 -4
  52. package/src/runtime/agent/orchestrator/runtime-core/workflow/agents.mjs +4 -1
  53. package/src/runtime/agent/orchestrator/runtime-core/workflow/context.mjs +50 -20
  54. package/src/runtime/agent/orchestrator/runtime-core/workflow-ids.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/agent-home.d.mts +4 -0
  56. package/src/runtime/agent/orchestrator/session/agent-home.mjs +33 -0
  57. package/src/runtime/agent/orchestrator/session/agent-mode.mjs +62 -0
  58. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.d.mts +31 -0
  59. package/src/runtime/agent/orchestrator/session/agent-room-supervisor.mjs +241 -0
  60. package/src/runtime/agent/orchestrator/session/agent-room.d.mts +162 -0
  61. package/src/runtime/agent/orchestrator/session/agent-room.mjs +641 -0
  62. package/src/runtime/agent/orchestrator/session/catalog-db/schema.mjs +7 -1
  63. package/src/runtime/agent/orchestrator/session/compact/user-message-kind.mjs +2 -0
  64. package/src/runtime/agent/orchestrator/session/compaction-archive-capture.mjs +6 -2
  65. package/src/runtime/agent/orchestrator/session/human-gate/gate.mjs +165 -0
  66. package/src/runtime/agent/orchestrator/session/human-gate/held-message.mjs +95 -0
  67. package/src/runtime/agent/orchestrator/session/human-gate/outbound-policy.mjs +221 -0
  68. package/src/runtime/agent/orchestrator/session/human-gate/requests.mjs +195 -0
  69. package/src/runtime/agent/orchestrator/session/human-gate/store.mjs +167 -0
  70. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +3 -1
  71. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +10 -0
  72. package/src/runtime/agent/orchestrator/session/loop/send-phase.mjs +16 -1
  73. package/src/runtime/agent/orchestrator/session/loop/tool-exec/before-hook.mjs +74 -15
  74. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +9 -1
  75. package/src/runtime/agent/orchestrator/session/loop/tool-exposure-migration.mjs +5 -4
  76. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +13 -3
  77. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +7 -0
  78. package/src/runtime/agent/orchestrator/session/manager/ask-turn-execute.mjs +1 -0
  79. package/src/runtime/agent/orchestrator/session/manager/ask-turn-loop-options.mjs +3 -1
  80. package/src/runtime/agent/orchestrator/session/manager/ask-turn-prepare.mjs +6 -0
  81. package/src/runtime/agent/orchestrator/session/manager/ask-turn-settle.mjs +2 -0
  82. package/src/runtime/agent/orchestrator/session/manager/pending-lifecycle-epoch.mjs +0 -8
  83. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +44 -16
  84. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +22 -5
  85. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +39 -4
  86. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +27 -5
  87. package/src/runtime/agent/orchestrator/session/manager/session-prompt-composition.mjs +55 -8
  88. package/src/runtime/agent/orchestrator/session/manager/session-record.mjs +16 -0
  89. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +3 -2
  90. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/workflow-switch-notice.mjs +58 -0
  92. package/src/runtime/agent/orchestrator/session/mixy-name.mjs +24 -0
  93. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +32 -5
  94. package/src/runtime/agent/orchestrator/session/routine-result.d.mts +70 -0
  95. package/src/runtime/agent/orchestrator/session/routine-result.mjs +277 -0
  96. package/src/runtime/agent/orchestrator/session/store/sweep/sweep-row.mjs +8 -0
  97. package/src/runtime/agent/orchestrator/session/store-record-strict-scan.mjs +28 -9
  98. package/src/runtime/agent/orchestrator/session/store-summary-catalog.mjs +16 -2
  99. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +44 -12
  100. package/src/runtime/agent/orchestrator/session/store-summary-last-message.mjs +46 -0
  101. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +0 -4
  102. package/src/runtime/agent/orchestrator/session/transcript-restore/restore.mjs +44 -1
  103. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +12 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +113 -5
  105. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +4 -0
  106. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +25 -1
  107. package/src/runtime/agent/orchestrator/tools/lib/shell-run-background/promote-job.mjs +8 -0
  108. package/src/runtime/agent/orchestrator/tools/lib/shell-run-settle.mjs +6 -0
  109. package/src/runtime/agent/orchestrator/tools/lib/shell-run-state.mjs +3 -0
  110. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +75 -57
  111. package/src/runtime/browser-bridge/client.mjs +24 -0
  112. package/src/runtime/channels/lib/scheduler.mjs +40 -12
  113. package/src/runtime/channels/lib/schedules-db.mjs +71 -12
  114. package/src/runtime/channels/lib/webhook/session-dispatch.mjs +36 -16
  115. package/src/runtime/channels/lib/webhook.mjs +0 -1
  116. package/src/runtime/channels/lib/webhooks-db.mjs +100 -10
  117. package/src/runtime/channels/lib/worker-main.mjs +17 -20
  118. package/src/runtime/computer-bridge/client.mjs +28 -1
  119. package/src/runtime/connectors/gmail-app-password.mjs +690 -0
  120. package/src/runtime/connectors/google-mcp.mjs +446 -0
  121. package/src/runtime/memory/lib/core-memory-access.mjs +69 -0
  122. package/src/runtime/memory/lib/core-memory-file.mjs +16 -8
  123. package/src/runtime/memory/lib/core-memory-index.mjs +15 -9
  124. package/src/runtime/memory/lib/core-memory-management.mjs +10 -4
  125. package/src/runtime/memory/lib/core-memory-store.mjs +53 -30
  126. package/src/runtime/memory/lib/core-memory-uniqueness.mjs +10 -6
  127. package/src/runtime/memory/lib/http-router/session-routes.mjs +3 -2
  128. package/src/runtime/memory/lib/memory-action-handlers/core-actions.mjs +44 -14
  129. package/src/runtime/memory/lib/memory-schema/core-and-meta.mjs +11 -0
  130. package/src/runtime/memory/lib/memory.mjs +7 -1
  131. package/src/runtime/memory/lib/query-core-recall.mjs +9 -1
  132. package/src/runtime/memory/lib/query-hybrid-search.mjs +2 -0
  133. package/src/runtime/memory/lib/tool-call-handler.mjs +2 -0
  134. package/src/runtime/memory/tool-defs.mjs +5 -0
  135. package/src/runtime/shared/agent-avatar-sprites.mjs +445 -0
  136. package/src/runtime/shared/agent-avatar.d.mts +64 -0
  137. package/src/runtime/shared/agent-avatar.mjs +320 -0
  138. package/src/runtime/shared/agent-route-config.d.mts +3 -0
  139. package/src/runtime/shared/agent-route-config.mjs +16 -0
  140. package/src/runtime/shared/atomic-file.mjs +14 -1
  141. package/src/runtime/shared/config.mjs +8 -1
  142. package/src/runtime/shared/connector-accounts.mjs +15 -0
  143. package/src/runtime/shared/connector-catalog.d.mts +52 -0
  144. package/src/runtime/shared/connector-catalog.mjs +230 -0
  145. package/src/runtime/shared/connector-triggers.d.mts +3 -0
  146. package/src/runtime/shared/connector-triggers.mjs +43 -0
  147. package/src/runtime/shared/google-oauth-client.mjs +24 -0
  148. package/src/runtime/shared/held-request-host.d.mts +12 -0
  149. package/src/runtime/shared/held-request-host.mjs +71 -0
  150. package/src/runtime/shared/plain-preview.d.mts +1 -0
  151. package/src/runtime/shared/plain-preview.mjs +28 -0
  152. package/src/runtime/shared/routine-limits.d.mts +11 -0
  153. package/src/runtime/shared/routine-limits.mjs +21 -0
  154. package/src/runtime/shared/routine-trigger.d.mts +10 -0
  155. package/src/runtime/shared/routine-trigger.mjs +44 -0
  156. package/src/runtime/shared/schedule-model-ref.mjs +9 -10
  157. package/src/runtime/shared/slow-span.mjs +4 -12
  158. package/src/runtime/shared/statusline/statusline-segments.mjs +13 -1
  159. package/src/runtime/shared/transcript-metadata.mjs +10 -0
  160. package/src/runtime/shared/turn-changes.mjs +0 -8
  161. package/src/runtime/shared/usage-readout-totals.mjs +56 -0
  162. package/src/runtime/shared/user-data-guard.mjs +114 -36
  163. package/src/session-runtime/agent-home-delivery.mjs +459 -0
  164. package/src/session-runtime/agent-profile-changes.mjs +21 -0
  165. package/src/session-runtime/agent-room-delivery.mjs +92 -0
  166. package/src/session-runtime/agent-task-sessions.mjs +200 -0
  167. package/src/session-runtime/boot/apis.mjs +50 -18
  168. package/src/session-runtime/boot/facade.mjs +5 -8
  169. package/src/session-runtime/boot/infrastructure.mjs +28 -17
  170. package/src/session-runtime/boot/tools.mjs +3 -0
  171. package/src/session-runtime/channel-config-api.mjs +53 -5
  172. package/src/session-runtime/cwd-plugins/core-memory-context.mjs +36 -20
  173. package/src/session-runtime/cwd-plugins/plugin-status.mjs +6 -1
  174. package/src/session-runtime/held-requests.mjs +228 -0
  175. package/src/session-runtime/internal-tool-executor/ask-user-tool.mjs +121 -0
  176. package/src/session-runtime/internal-tool-executor/feature-tools.mjs +5 -1
  177. package/src/session-runtime/internal-tool-executor/knowledge-tools.mjs +31 -2
  178. package/src/session-runtime/internal-tool-executor/workspace-tools.mjs +31 -2
  179. package/src/session-runtime/internal-tool-executor.mjs +2 -0
  180. package/src/session-runtime/lifecycle/session-catalog.mjs +9 -0
  181. package/src/session-runtime/lifecycle/session-switch/resume.mjs +4 -0
  182. package/src/session-runtime/lifecycle/shared.mjs +6 -0
  183. package/src/session-runtime/mcp-glue/server-catalog.mjs +8 -0
  184. package/src/session-runtime/mcp-glue/server-input.mjs +3 -0
  185. package/src/session-runtime/mcp-oauth-login.mjs +112 -15
  186. package/src/session-runtime/model-route/set-route.mjs +14 -3
  187. package/src/session-runtime/owned-automation-run.mjs +229 -0
  188. package/src/session-runtime/resource-api.mjs +5 -0
  189. package/src/session-runtime/resource-mcp-api.mjs +107 -13
  190. package/src/session-runtime/resource-skills-api.mjs +12 -0
  191. package/src/session-runtime/room-member-gate.mjs +48 -0
  192. package/src/session-runtime/routine-home-turn.mjs +156 -0
  193. package/src/session-runtime/routine-owner.mjs +48 -0
  194. package/src/session-runtime/schedule-session-run.mjs +30 -64
  195. package/src/session-runtime/services/agent-tool/helpers.mjs +10 -0
  196. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-plan.mjs +19 -4
  197. package/src/session-runtime/services/agent-tool/spawn-flow/spawn-prep.mjs +2 -0
  198. package/src/session-runtime/services/channel-admin.mjs +93 -24
  199. package/src/session-runtime/services/memory-access.mjs +37 -0
  200. package/src/session-runtime/services/provider-admin.mjs +74 -39
  201. package/src/session-runtime/services/schedule-access.mjs +91 -0
  202. package/src/session-runtime/session-create/session-options.mjs +34 -7
  203. package/src/session-runtime/session-hooks.mjs +16 -8
  204. package/src/session-runtime/setup-tool/executor.mjs +68 -6
  205. package/src/session-runtime/setup-tool/extended-actions.mjs +72 -14
  206. package/src/session-runtime/setup-tool/settings-contract.mjs +8 -6
  207. package/src/session-runtime/setup-tool/tool-defs.mjs +8 -0
  208. package/src/session-runtime/skills-api.mjs +16 -1
  209. package/src/session-runtime/tool-policy-refresh.mjs +238 -29
  210. package/src/session-runtime/turn/session-ops.mjs +8 -1
  211. package/src/session-runtime/turn/turn-run/ask-callbacks.mjs +10 -2
  212. package/src/session-runtime/turn/turn-run/turn-prepare.mjs +3 -0
  213. package/src/session-runtime/webhook-session-run.mjs +49 -67
  214. package/src/session-runtime/workflow-agents-api/agent-connectors.mjs +239 -0
  215. package/src/session-runtime/workflow-agents-api/agent-editor.mjs +556 -22
  216. package/src/session-runtime/workflow-agents-api/workflow-packs.mjs +11 -1
  217. package/src/session-runtime/workflow-agents-api.mjs +4 -1
  218. package/src/standalone/daemon-agent-control.mjs +1 -8
  219. package/src/standalone/daemon-session-control.mjs +11 -2
  220. package/src/standalone/daemon.mjs +18 -4
  221. package/src/standalone/session-protocol.mjs +27 -0
  222. package/src/standalone/session-service/session-calls/session-turns.mjs +9 -0
  223. package/src/standalone/session-service.mjs +19 -0
  224. package/src/tui/app/slash-commands.mjs +20 -0
  225. package/src/tui/dist/index.mjs +164 -86
  226. package/src/tui/session/context-state.mjs +6 -1
  227. package/src/tui/session/notice-surface.mjs +35 -4
  228. package/src/tui/session/queue-helpers.mjs +2 -1
  229. package/src/tui/session/session-api/extensions.mjs +4 -0
  230. package/src/tui/session/session-api/intake/submission.mjs +5 -0
  231. package/src/tui/session/session-api/integrations.mjs +8 -0
  232. package/src/tui/session/session-api/lifecycle/session-reset.mjs +13 -3
  233. package/src/tui/session/session-api/lifecycle.mjs +17 -1
  234. package/src/tui/session/session-api/routes.mjs +48 -3
  235. package/src/tui/session/session-flow/drain.mjs +12 -3
  236. package/src/tui/session/session-flow/queue/entry-shape.mjs +2 -1
  237. package/src/tui/session/session-flow/queue/take-entries.mjs +3 -2
  238. package/src/tui/session/session-flow/steering.mjs +58 -24
  239. package/src/tui/session/tool-approval.mjs +26 -5
  240. package/src/tui/session/transcript-intake.mjs +6 -2
  241. package/src/tui/session/transcript-spill.mjs +5 -1
  242. package/src/tui/session/tui-steering-persist.mjs +65 -42
  243. package/src/tui/session/turn-live-call.mjs +56 -0
  244. package/src/tui/session/turn.mjs +22 -6
  245. package/src/session-runtime/automation-agents.mjs +0 -94
  246. package/src/session-runtime/automation-workflow.mjs +0 -59
@@ -31,12 +31,43 @@ export function createNoticeSurface({ getState, set, isDisposed, pushItem, repla
31
31
  return id;
32
32
  };
33
33
  const pushNotice = (text, tone = 'info', options = {}) => {
34
- const value = polishNoticeText(text);
35
- if (!value) return null;
34
+ // A routine result is a finished sentence, never a raw error to polish;
35
+ // a group-room row is someone's words, kept verbatim (a pass has none).
36
+ const value =
37
+ options.routine || options.room || options.heldRequest ? String(text ?? '').trim() : polishNoticeText(text);
38
+ if (!value && !options.room) return null;
36
39
  const forceTranscript = options.transcript === true;
37
40
  if (!forceTranscript) return pushToast(value, tone, options.ttlMs, options);
38
- const id = nextId();
39
- pushItem({ kind: 'notice', id, text: value, tone });
41
+ const userRow = options.room?.kind === 'user';
42
+ // A room user row keeps its message id (the sender's submission id), as a
43
+ // turn's own prompt row keeps its submission id.
44
+ const id = (userRow && options.messageId) || nextId();
45
+ // One row per messageId: a routine's live-only run row (running, delivery
46
+ // failed) gives way to the row that follows it (routine-result.mjs).
47
+ if (options.messageId) {
48
+ const current = getState().items;
49
+ const items = current.filter((it) => !(it?.kind === 'notice' && it?.messageId === options.messageId));
50
+ if (items.length !== current.length) set({ items: replaceItems(items, { preserveStreamingTail: true }) });
51
+ }
52
+ pushItem({
53
+ // A group-room row (agent-room.mjs): the user's own words are their
54
+ // bubble; every member row is a notice the app renders from `room`.
55
+ kind: userRow ? 'user' : 'notice',
56
+ id,
57
+ text: value,
58
+ ...(userRow ? {} : { tone }),
59
+ // A schedule result posted into a home thread (routine-result.mjs).
60
+ ...(options.routine ? { routine: options.routine } : {}),
61
+ ...(options.room ? { room: options.room } : {}),
62
+ // A room user row's files, in the fields a prompt row draws them from.
63
+ ...(userRow && Array.isArray(options.attachments) && options.attachments.length
64
+ ? { attachments: options.attachments }
65
+ : {}),
66
+ ...(userRow && Array.isArray(options.images) && options.images.length ? { images: options.images } : {}),
67
+ // A held request of an unattended run (human-gate/held-message.mjs).
68
+ ...(options.heldRequest ? { heldRequest: options.heldRequest } : {}),
69
+ ...(options.messageId ? { messageId: options.messageId } : {}),
70
+ });
40
71
  return id;
41
72
  };
42
73
  // Remove a transcript notice previously created via pushNotice(...,
@@ -5,6 +5,7 @@
5
5
  */
6
6
  import { agentJobResultText, parseAgentJob, parseSyntheticAgentMessage } from './agent-envelope.mjs';
7
7
  import { attachmentTextForPart, isAttachmentReference } from '../../runtime/attachments/store.mjs';
8
+ import { recognisedSlashToken } from '../app/slash-commands.mjs';
8
9
 
9
10
  const QUEUE_PRIORITY = { now: 0, next: 1, later: 2 };
10
11
 
@@ -52,7 +53,7 @@ export function isQueuedEntryVisible(entry) {
52
53
  export function isSlashQueuedEntry(entry) {
53
54
  if (entry?.skipSlashCommands) return false;
54
55
  const text = promptContentText(entry?.content ?? entry?.text ?? '');
55
- return text.trim().startsWith('/');
56
+ return recognisedSlashToken(text) !== null;
56
57
  }
57
58
 
58
59
  function firstQueueLine(text) {
@@ -121,6 +121,10 @@ export function createSessionExtensionsApi(bag) {
121
121
  (input) => runtime.saveSkill?.(input),
122
122
  (result, input) => [`skill saved: ${result?.skill?.name || input?.name || 'skill'}`]
123
123
  ),
124
+ removeSkill: lockedChange(
125
+ (input) => runtime.removeSkill?.(input),
126
+ (result, input) => [`skill removed: ${result?.skill?.name || input?.name || 'skill'}`]
127
+ ),
124
128
  reloadSkills: lockedChange(
125
129
  () => runtime.reloadSkills?.(),
126
130
  (status) => [`skills reload: ${status?.count || 0} available`]
@@ -14,6 +14,11 @@ export function createSubmissionIntake(bag) {
14
14
  text,
15
15
  queueOptions: {
16
16
  ...options,
17
+ // A kickoff (a new agent's greeting turn) reaches the model only: no
18
+ // user bubble, no queue row, never restored into the composer.
19
+ ...(options.kickoff === true
20
+ ? { isMeta: true, suppressDisplay: true, skipSlashCommands: true, restorable: false }
21
+ : {}),
17
22
  // Always mint a submission id so daemon retries / dual-path intake
18
23
  // can dedupe instead of booking the same prompt twice.
19
24
  id: String(options.id || '').trim() || nextId(),
@@ -191,6 +191,14 @@ export function createSessionIntegrationsApi(bag, { oauthFlows }) {
191
191
  (name) => runtime.runScheduleNow(name),
192
192
  (_result, name) => `schedule ran: ${name}`
193
193
  ),
194
+ runWebhookNow: announced(
195
+ (name) => runtime.runWebhookNow(name),
196
+ (_result, name) => `webhook ran: ${name}`
197
+ ),
198
+ retryRoutineDelivery: announced(
199
+ (kind, name) => runtime.retryRoutineDelivery(kind, name),
200
+ (_result, kind, name) => `${kind} result delivered: ${name}`
201
+ ),
194
202
  saveWebhook: announced(
195
203
  (entry) => runtime.saveWebhook(entry),
196
204
  (result) => `webhook saved: ${result.name}`
@@ -78,18 +78,28 @@ export function createSessionResetActions(bag) {
78
78
  };
79
79
 
80
80
  return {
81
- clear: () =>
81
+ // `archiveAs`: the id the outgoing transcript is kept under (an agent
82
+ // home's fresh start); the clear fails rather than wipe without it.
83
+ // `rebuildPrompt` / `fork: false`: an agent chat's reset (session-ops clear).
84
+ clear: (options = {}) =>
82
85
  withResetLock(async () => {
86
+ const archiveAs = typeof options?.archiveAs === 'string' ? options.archiveAs : '';
87
+ const reset = {
88
+ ...(options?.rebuildPrompt === true ? { rebuildPrompt: true } : {}),
89
+ ...(options?.fork === false ? { fork: false } : {}),
90
+ };
83
91
  const clearedId = getState().sessionId;
84
92
  // A cleared session has no failed turn left to resume.
85
93
  bag.supersedeQuotaResume?.(clearedId);
86
94
  const rollbackSnapshot = beginTuiReset();
87
95
  set(blankSessionPatch());
88
96
  try {
89
- await runtime.clear({ recoverAgent: true });
97
+ const result = await runtime.clear({ recoverAgent: true, ...(archiveAs ? { archiveAs } : {}), ...reset });
98
+ if (result === false && (archiveAs || reset.rebuildPrompt))
99
+ throw new Error('The conversation could not be cleared.');
90
100
  commitEmptySession(rollbackSnapshot);
91
101
  flags.lastUserActivityAt = Date.now();
92
- return true;
102
+ return reset.rebuildPrompt ? result : true;
93
103
  } catch (error) {
94
104
  restoreTuiAfterFailedSessionReset(rollbackSnapshot);
95
105
  throw error;
@@ -9,6 +9,17 @@ import { createInheritanceActions } from './lifecycle/inheritance.mjs';
9
9
  import { createResumeAction } from './lifecycle/resume.mjs';
10
10
  import { createDisposeAction } from './lifecycle/dispose.mjs';
11
11
  import { publishModelSwitch } from './routes.mjs';
12
+ import { resolveHeldRequest } from '../../../runtime/shared/held-request-host.mjs';
13
+
14
+ /** The human-response fields of a decision (never a value), or null. */
15
+ function humanDecisionFields(decision) {
16
+ if (!decision || typeof decision !== 'object') return null;
17
+ const fields = {};
18
+ for (const key of ['response', 'editedArgs', 'remember', 'scope']) {
19
+ if (decision[key] !== undefined) fields[key] = decision[key];
20
+ }
21
+ return Object.keys(fields).length ? fields : null;
22
+ }
12
23
 
13
24
  export function createSessionLifecycleApi(bag, { restoreTranscriptItems, oauthFlows }) {
14
25
  const { runtime, pushNotice, removeNotice, setProgressHint, finishToolApproval } = bag;
@@ -52,14 +63,19 @@ export function createSessionLifecycleApi(bag, { restoreTranscriptItems, oauthFl
52
63
  return {
53
64
  isSessionTerminal: () => bag.flags?.admission === 'terminal' || runtime.isSessionTerminal?.() === true,
54
65
  resolveToolApproval: (id, decision = {}) => {
66
+ // A held request of an unattended run is answered from its owner's home
67
+ // thread, wherever the run itself lives (session-runtime/held-requests.mjs).
68
+ if (resolveHeldRequest(id, decision)) return true;
55
69
  const approved = decision === true || decision?.approved === true;
70
+ const extra = humanDecisionFields(decision);
56
71
  return finishToolApproval(
57
72
  id,
58
73
  approved,
59
74
  decision?.reason || (approved ? 'approved by user' : 'denied by user'),
60
75
  typeof decision?.device === 'string' ? decision.device : '',
61
76
  decision?.answers,
62
- typeof decision?.reply === 'string' ? decision.reply : undefined
77
+ typeof decision?.reply === 'string' ? decision.reply : undefined,
78
+ ...(extra ? [extra] : [])
63
79
  );
64
80
  },
65
81
  pushNotice,
@@ -3,7 +3,10 @@
3
3
  * the serialized optimistic setRoute, web-search route, workflows, agent
4
4
  * definitions, output style and orchestration mode.
5
5
  */
6
- import { modelSwitchItem } from '../../../runtime/agent/orchestrator/session/transcript-restore/restore.mjs';
6
+ import {
7
+ modelSwitchItem,
8
+ workflowSwitchItem,
9
+ } from '../../../runtime/agent/orchestrator/session/transcript-restore/restore.mjs';
7
10
  import { selectedModel } from '../../../session-runtime/model-route/model-switch-target.mjs';
8
11
  import { createApiHelpers } from './shared.mjs';
9
12
 
@@ -272,8 +275,48 @@ export function createSessionRouteApi(bag) {
272
275
  createWorkflow: async (payload) => runtime.createWorkflow?.(payload) ?? null,
273
276
  deleteWorkflow: async (workflowId) => runtime.deleteWorkflow?.(workflowId) ?? null,
274
277
  getAgentDefinition: (agentId) => runtime.getAgentDefinition?.(agentId) ?? null,
278
+ getAgentAvatarImage: (agentId) => runtime.getAgentAvatarImage?.(agentId) ?? null,
279
+ getAgentConnectors: (agentId) => runtime.getAgentConnectors?.(agentId) ?? [],
280
+ listConnectors: () => runtime.listConnectors?.() ?? [],
281
+ listConnectorTools: (connectorId) => runtime.listConnectorTools?.(connectorId) ?? [],
282
+ setAgentConnectors: async (agentId, connectors) => runtime.setAgentConnectors?.(agentId, connectors) ?? null,
283
+ setAgentConnectorEnabled: async (agentId, connectorId, enabled) =>
284
+ runtime.setAgentConnectorEnabled?.(agentId, connectorId, enabled) ?? [],
285
+ getProjectConnectors: (projectPath) => runtime.getProjectConnectors?.(projectPath) ?? [],
286
+ setProjectConnectorEnabled: async (projectPath, connectorId, enabled) =>
287
+ runtime.setProjectConnectorEnabled?.(projectPath, connectorId, enabled) ?? [],
288
+ setAgentConnectorToolPermission: async (agentId, connectorId, toolName, permission) =>
289
+ runtime.setAgentConnectorToolPermission?.(agentId, connectorId, toolName, permission) ?? null,
290
+ // Per-agent action-kind permissions (ask / allow / block) for the profile.
291
+ getAgentPermissions: async (agentId) =>
292
+ (await import('../../../runtime/agent/orchestrator/session/human-gate/store.mjs')).agentPermissions(agentId),
293
+ setAgentPermission: async (agentId, action, mode) =>
294
+ (await import('../../../runtime/agent/orchestrator/session/human-gate/store.mjs')).setAgentPermission(
295
+ agentId,
296
+ action,
297
+ mode
298
+ ),
299
+ getLeadPersona: () => runtime.getLeadPersona?.() ?? '',
300
+ saveLeadPersona: async (text) => runtime.saveLeadPersona?.(text) ?? '',
301
+ getLeadAgentBody: () => runtime.getLeadAgentBody?.() ?? '',
302
+ saveLeadAgentBody: async (text) => runtime.saveLeadAgentBody?.(text) ?? '',
303
+ getLeadStatus: () => runtime.getLeadStatus?.() ?? { created: null, name: '', avatar: null },
304
+ saveLeadIdentity: async (payload) => runtime.saveLeadIdentity?.(payload) ?? null,
305
+ setLeadCreated: async (created, description) => runtime.setLeadCreated?.(created, description) ?? null,
275
306
  saveAgentDefinition: async (payload) => runtime.saveAgentDefinition?.(payload) ?? null,
276
307
  deleteAgentDefinition: async (agentId) => runtime.deleteAgentDefinition?.(agentId) ?? null,
308
+ agentDefinitionPresence: (agentIds) => runtime.agentDefinitionPresence?.(agentIds) ?? null,
309
+ restoreAgentDefinition: async (agentId) => runtime.restoreAgentDefinition?.(agentId) ?? null,
310
+ purgeAgentDefinition: async (agentId) => runtime.purgeAgentDefinition?.(agentId) ?? null,
311
+ archivedAgentIdentities: (agentIds) => runtime.archivedAgentIdentities?.(agentIds) ?? null,
312
+ // Group-room rows posted by the desktop host into a room's log session.
313
+ appendRoomRows: async (sessionId, rows) =>
314
+ (await import('../../../session-runtime/agent-room-delivery.mjs')).appendRoomRows(sessionId, rows),
315
+ // One group-room supervisor step on the Maintainer route.
316
+ decideRoomTurn: async (input) =>
317
+ (await import('../../../runtime/agent/orchestrator/agent-runtime/room-supervisor-completion.mjs')).decideRoomTurn(
318
+ input
319
+ ),
277
320
  getOutputStyle: () => {
278
321
  return runtime.getOutputStyle?.() || runtime.listOutputStyles?.() || null;
279
322
  },
@@ -291,8 +334,10 @@ export function createSessionRouteApi(bag) {
291
334
  deferStatsRefresh();
292
335
  return result;
293
336
  }),
294
- setWorkflow: withCommandLock(async (workflowId) => {
295
- const result = await runtime.setWorkflow?.(workflowId);
337
+ setWorkflow: withCommandLock(async (workflowId, options) => {
338
+ const result = await runtime.setWorkflow?.(workflowId, options);
339
+ // A switch of this conversation leaves a divider (restored from message meta).
340
+ if (result?.workflowSwitch) bag.pushItem(workflowSwitchItem(bag.nextId(), result.workflowSwitch));
296
341
  publishRoute();
297
342
  return result;
298
343
  }),
@@ -52,6 +52,13 @@ function renderBatchItems(batch, pushUserOrSyntheticItem) {
52
52
  }
53
53
  continue;
54
54
  }
55
+ // A routine fire paints as the routine's marker row, not a user bubble.
56
+ if (entry.mode === 'routine') {
57
+ pushUserOrSyntheticItem(entry.text, entry.id, 'routine', {
58
+ routineTrigger: entry.transcriptMeta?.routineTrigger,
59
+ });
60
+ continue;
61
+ }
55
62
  const sender = String(entry.transcriptMeta?.sender || '')
56
63
  .trim()
57
64
  .toLowerCase();
@@ -106,6 +113,10 @@ export function turnOptionsForBatch(batch, steering) {
106
113
  Array.isArray(entry?.resumeCompletionKeys) ? entry.resumeCompletionKeys : []
107
114
  ),
108
115
  transcriptMeta: batch[0]?.transcriptMeta || null,
116
+ // Only a routine turn names its selection text (never its payload).
117
+ ...(batch[0]?.mode === 'routine' && typeof batch[0].skillSelection === 'string'
118
+ ? { skillSelection: batch[0].skillSelection }
119
+ : {}),
109
120
  context:
110
121
  batch
111
122
  .map((entry) => String(entry.context || '').trim())
@@ -191,9 +202,7 @@ export function createDrainLoop(
191
202
  clearBlockedDrainRetry();
192
203
  if (entries.length === 0) return;
193
204
  queue.removeQueuedEntries(entries);
194
- const mirrored = entries.filter(
195
- (entry) => steering.shouldMirrorSteeringEntry(entry) && !entry.steeringPersistRestored
196
- );
205
+ const mirrored = entries.filter((entry) => steering.shouldMirrorSteeringEntry(entry) && entry.steeringPersistId);
197
206
  if (mirrored.length > 0) {
198
207
  void Promise.resolve(dropTuiSteeringPersist(steering.leadSessionId(), mirrored)).catch(() => {});
199
208
  }
@@ -43,8 +43,9 @@ export function makeQueueEntry(text, options = {}, nextId) {
43
43
  ? options.resumeCompletionKeys.filter((key) => key != null && String(key).trim())
44
44
  : [],
45
45
  steeringPersistId: options.steeringPersistId || null,
46
- steeringPersistRestored: options.steeringPersistRestored === true,
47
46
  isMeta: options.isMeta === true,
47
+ // A routine turn's trusted skill-selection text (its instructions only).
48
+ skillSelection: typeof options.skillSelection === 'string' ? options.skillSelection : null,
48
49
  goalId: options.goalId || null,
49
50
  // Retry of a failed turn: the transcript and the session rewind the
50
51
  // failed turn's unanswered prompt before this entry runs.
@@ -43,7 +43,8 @@ export function createTakeEntriesOps({ pending, pendingNotificationKeys, removeQ
43
43
  batch.push(entry);
44
44
  pending.splice(i, 1);
45
45
  if (entry.mode === 'task-notification' && entry.key) pendingNotificationKeys.delete(entry.key);
46
- if (entry.retryTurnId || batch.length >= limit) break;
46
+ // A routine fire is a turn of its own, never merged with another.
47
+ if (entry.retryTurnId || entry.mode === 'routine' || batch.length >= limit) break;
47
48
  } else {
48
49
  i += 1;
49
50
  }
@@ -73,7 +74,7 @@ export function createTakeEntriesOps({ pending, pendingNotificationKeys, removeQ
73
74
  // A reclaimed prompt goes back to the draft, so its durable steering
74
75
  // mirror goes with it; otherwise the next runtime restore re-queues a
75
76
  // prompt the user already took back.
76
- const mirrored = queued.filter((entry) => entry.steeringPersistId && !entry.steeringPersistRestored);
77
+ const mirrored = queued.filter((entry) => entry.steeringPersistId);
77
78
  if (mirrored.length > 0) void dropTuiSteeringPersist(leadSessionId(), mirrored);
78
79
  const queuedText = queued
79
80
  .map((item) => item.text)
@@ -12,10 +12,13 @@ import {
12
12
  callCommitCallbacks,
13
13
  STEERING_SUPPRESSED_DISPLAY,
14
14
  } from '../queue-helpers.mjs';
15
- import { dropTuiSteeringPersist, drainTuiSteeringPersist } from '../tui-steering-persist.mjs';
15
+ import { dropTuiSteeringPersist, readTuiSteeringPersist } from '../tui-steering-persist.mjs';
16
16
  import { getBackgroundTask } from '../../../runtime/shared/background-tasks.mjs';
17
17
 
18
18
  const RESTORE_DEDUP_WINDOW = 80;
19
+ // An older build's process-local item counter: a later process issues the
20
+ // same ids again, so such an id proves nothing about delivery.
21
+ const PROCESS_LOCAL_ITEM_ID = /^it_\d+$/;
19
22
 
20
23
  // Mid-chain drain converts queued prompt/task notification entries into
21
24
  // model-visible "queued_command" style steering only at provider continuation
@@ -65,8 +68,28 @@ function steeringMessageFromEntry(entry) {
65
68
  return String(content ?? '').trim().length > 0 ? { ...meta, text: displayText, content } : null;
66
69
  }
67
70
 
68
- // Recent user message bodies define "already delivered" for restore dedup;
69
- // substring (not equality) because steering delivery may wrap the raw text.
71
+ // The submissions the transcript holds: a turn's prompt (`checkpointId`, plus
72
+ // `submissionIds` for a merged batch) and a steered message's `submissionIds`.
73
+ function deliveredSubmissionIds(session) {
74
+ const out = new Set();
75
+ for (const message of Array.isArray(session?.messages) ? session.messages : []) {
76
+ if (message?.role !== 'user') continue;
77
+ const meta = message.meta || {};
78
+ const transcript = meta.transcript || {};
79
+ for (const id of [
80
+ transcript.checkpointId,
81
+ ...(Array.isArray(transcript.submissionIds) ? transcript.submissionIds : []),
82
+ ...(Array.isArray(meta.submissionIds) ? meta.submissionIds : []),
83
+ ]) {
84
+ if (typeof id === 'string' && id) out.add(id);
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+
90
+ // Rows of an older build carry no stable submission id; only their text can
91
+ // tell them delivered: substring (not equality) because steering delivery may
92
+ // wrap the raw text.
70
93
  function recentUserTexts(session, window) {
71
94
  const messages = Array.isArray(session?.messages) ? session.messages : [];
72
95
  const out = [];
@@ -107,7 +130,7 @@ export function createSteeringOps(bag, { queue, submissions }) {
107
130
  function commitSteeringQueueEntries(entries) {
108
131
  callCommitCallbacks(entries);
109
132
  const mirrored = (Array.isArray(entries) ? entries : []).filter(
110
- (entry) => shouldMirrorSteeringEntry(entry) && !entry.steeringPersistRestored
133
+ (entry) => shouldMirrorSteeringEntry(entry) && entry.steeringPersistId
111
134
  );
112
135
  if (mirrored.length > 0) dropTuiSteeringPersist(leadSessionId(), mirrored);
113
136
  }
@@ -205,46 +228,57 @@ export function createSteeringOps(bag, { queue, submissions }) {
205
228
  async function restoreLeadSteeringFromDisk() {
206
229
  // A closed/closing conversation never gets queue work restored into it.
207
230
  if (!admissionOpen()) return;
208
- const rows = await drainTuiSteeringPersist(leadSessionId());
231
+ const rows = await readTuiSteeringPersist(leadSessionId());
209
232
  if (!rows.length) return;
210
233
  const livePersistIds = new Set(pending.map((entry) => entry?.steeringPersistId).filter(Boolean));
211
- // Crash-consumed dedup: a row whose text already landed in the session
212
- // transcript was consumed before the restart — only its disk drop was
213
- // lost (drop is an async fire-and-forget write). Re-queuing it silently
214
- // re-injects a stale prompt into the next turn (user report: surprise
215
- // self-injection after a process restart).
216
- const delivered = recentUserTexts(runtime.session, RESTORE_DEDUP_WINDOW);
217
- const alreadyDelivered = (text) => delivered.some((body) => body.includes(text));
234
+ // Crash-consumed dedup: a row whose submission already landed in the
235
+ // session transcript was consumed before the restart — only its disk drop
236
+ // was lost (drop is an async fire-and-forget write). Re-queuing it
237
+ // silently re-injects a stale prompt into the next turn (user report:
238
+ // surprise self-injection after a process restart). Identity, never text:
239
+ // the same words can be a new submission.
240
+ const delivered = deliveredSubmissionIds(runtime.session);
241
+ let legacyTexts = null;
242
+ const alreadyDelivered = (row, text) => {
243
+ if (row.submissionId && !PROCESS_LOCAL_ITEM_ID.test(row.submissionId)) return delivered.has(row.submissionId);
244
+ legacyTexts ??= recentUserTexts(runtime.session, RESTORE_DEDUP_WINDOW);
245
+ return legacyTexts.some((body) => body.includes(text));
246
+ };
218
247
  const restored = [];
219
- let droppedDelivered = 0;
248
+ const consumed = [];
220
249
  for (const row of rows) {
221
250
  const text = String(row?.text || '').trim();
222
251
  if (!text) continue;
223
252
  // Live enqueue already holds this row (id assigned before the async
224
253
  // disk write). Replaying it is the classic double-booked queue.
225
254
  if (row.steeringPersistId && livePersistIds.has(row.steeringPersistId)) continue;
226
- if (alreadyDelivered(text)) {
227
- droppedDelivered += 1;
255
+ if (alreadyDelivered(row, text)) {
256
+ consumed.push(row);
228
257
  continue;
229
258
  }
230
- const entry = queue.makeQueueEntry(row.text, {
231
- id: row.submissionId || undefined,
232
- submittedAt: row.submittedAt || undefined,
233
- steeringPersistRestored: true,
234
- steeringPersistId: row.steeringPersistId || undefined,
259
+ // The row keeps its executable content and attachment links; a row
260
+ // of an older build has only its text.
261
+ const { content, text: displayText, submissionId, submittedAt, ...options } = row;
262
+ const entry = queue.makeQueueEntry(content ?? displayText, {
263
+ ...options,
264
+ id: submissionId || undefined,
265
+ submittedAt: submittedAt || undefined,
266
+ displayText,
235
267
  });
236
268
  pending.push(entry);
237
- if (row.submissionId) submissions.remember(row.submissionId);
238
- if (entry.steeringPersistId) livePersistIds.add(entry.steeringPersistId);
269
+ if (submissionId) submissions.remember(submissionId);
270
+ livePersistIds.add(entry.steeringPersistId);
239
271
  if (isQueuedEntryVisible(entry)) restored.push(entry);
240
272
  }
241
273
  if (restored.length > 0) set({ queued: [...getState().queued, ...restored] });
274
+ // Reading keeps rows durable; a delivered row's lost drop is completed here.
275
+ if (consumed.length > 0) await dropTuiSteeringPersist(leadSessionId(), consumed);
242
276
  // The conversation closed while the rows were being read: settle them
243
277
  // explicitly (rejected + durable mirror released), never leave them parked.
244
278
  if (bag.flags?.admission === 'terminal') bag.settleClosedQueue?.();
245
- if (droppedDelivered > 0) {
279
+ if (consumed.length > 0) {
246
280
  try {
247
- process.stderr.write(`[tui] skipped ${droppedDelivered} already-delivered steering row(s) on restore\n`);
281
+ process.stderr.write(`[tui] skipped ${consumed.length} already-delivered steering row(s) on restore\n`);
248
282
  } catch {
249
283
  /* best effort */
250
284
  }
@@ -6,6 +6,11 @@
6
6
  * state, so the closure identifiers (state/set/nextId/disposed/timeout) are
7
7
  * threaded through the factory argument via getters — never stale snapshots.
8
8
  */
9
+ import {
10
+ humanRequestFields,
11
+ normalizeHumanResponse,
12
+ } from '../../runtime/agent/orchestrator/session/human-gate/requests.mjs';
13
+
9
14
  export function createToolApproval({ getState, set, nextId, getDisposed, timeoutMs }) {
10
15
  const toolApprovalQueue = [];
11
16
  let activeToolApproval = null;
@@ -32,6 +37,8 @@ export function createToolApproval({ getState, set, nextId, getDisposed, timeout
32
37
  requestedAt: now,
33
38
  timeoutMs: requestTimeoutMs,
34
39
  expiresAt: requestTimeoutMs === null ? null : now + requestTimeoutMs,
40
+ // outbound / secret / takeover / permission fields (human-gate/requests.mjs).
41
+ ...humanRequestFields(input),
35
42
  };
36
43
  }
37
44
  // Every request expires from ITS OWN requestedAt, queued or active. Arming
@@ -41,18 +48,24 @@ export function createToolApproval({ getState, set, nextId, getDisposed, timeout
41
48
  if (!entry || entry.timer || entry.request?.expiresAt === null) return;
42
49
  const remainingMs = Math.max(0, Number(entry.request?.expiresAt || 0) - Date.now());
43
50
  entry.timer = setTimeout(() => {
44
- finishToolApproval(entry.id, false, 'approval timed out');
51
+ finishToolApproval(entry.id, false, 'approval timed out', '', undefined, undefined, { timedOut: true });
45
52
  }, remainingMs);
46
53
  entry.timer.unref?.();
47
54
  }
48
55
  // Settle one entry: drop its timeout, then hand the waiter its answer. The
49
56
  // resolve stays guarded — a waiter whose caller already went away must never
50
57
  // take the rest of the queue down with it.
51
- function settleToolApproval(entry, approved, reason, answers, reply) {
58
+ function settleToolApproval(entry, approved, reason, answers, reply, extra, device = '') {
52
59
  if (entry?.timer) clearTimeout(entry.timer);
53
60
  const result = { approved, reason };
54
61
  if (answers !== undefined) result.answers = answers;
55
62
  if (reply !== undefined) result.reply = reply;
63
+ // A human-kind request answers with its canonical response (never a value).
64
+ if (entry?.request?.remember) {
65
+ Object.assign(result, normalizeHumanResponse(entry.request, approved, extra));
66
+ if (device) result.device = device;
67
+ }
68
+ if (extra?.timedOut === true) result.timedOut = true;
56
69
  try {
57
70
  entry.resolve(result);
58
71
  } catch {}
@@ -68,7 +81,15 @@ export function createToolApproval({ getState, set, nextId, getDisposed, timeout
68
81
  armToolApprovalTimeout(entry);
69
82
  set({ toolApproval: entry.request });
70
83
  }
71
- function finishToolApproval(id, approved, reason = '', device = '', answers = undefined, reply = undefined) {
84
+ function finishToolApproval(
85
+ id,
86
+ approved,
87
+ reason = '',
88
+ device = '',
89
+ answers = undefined,
90
+ reply = undefined,
91
+ extra = undefined
92
+ ) {
72
93
  const targetId = String(id || '');
73
94
  if (activeToolApproval && activeToolApproval.id === targetId) {
74
95
  const entry = activeToolApproval;
@@ -79,14 +100,14 @@ export function createToolApproval({ getState, set, nextId, getDisposed, timeout
79
100
  ? { id: targetId, approved: approved === true, device: String(device), at: Date.now() }
80
101
  : null;
81
102
  set({ toolApproval: null, toolApprovalResult });
82
- settleToolApproval(entry, approved === true, String(reason || ''), answers, reply);
103
+ settleToolApproval(entry, approved === true, String(reason || ''), answers, reply, extra, String(device || ''));
83
104
  presentNextToolApproval();
84
105
  return true;
85
106
  }
86
107
  const index = toolApprovalQueue.findIndex((entry) => entry.id === targetId);
87
108
  if (index >= 0) {
88
109
  const [entry] = toolApprovalQueue.splice(index, 1);
89
- settleToolApproval(entry, approved === true, String(reason || ''), answers, reply);
110
+ settleToolApproval(entry, approved === true, String(reason || ''), answers, reply, extra, String(device || ''));
90
111
  return true;
91
112
  }
92
113
  return false;
@@ -12,6 +12,7 @@ import {
12
12
  import { isLateToolAnnouncement } from '../../runtime/agent/orchestrator/runtime-core/session-text.mjs';
13
13
  import { appendPromptHistory } from '../prompt-history-store.mjs';
14
14
  import { newTranscriptMessageId } from '../../runtime/shared/transcript-metadata.mjs';
15
+ import { routineTriggerField } from '../../runtime/shared/routine-trigger.mjs';
15
16
  import {
16
17
  buildExecutionResponseToolItem,
17
18
  completionCardFromExecution,
@@ -107,16 +108,19 @@ export function createTranscriptIntake({ getState, flags, pushItem, patchItem, t
107
108
  // message reports it) or, for a typed prompt, a new one that goes with
108
109
  // the prompt into the model transcript. An injected row with no message
109
110
  // id of its own carries none.
111
+ // A routine fire's row is the prompt the turn persists, like a typed one.
112
+ const ownsPrompt = origin === 'user' || origin === 'routine';
110
113
  let messageId = '';
111
114
  if (typeof extra?.messageId === 'string' && extra.messageId) messageId = extra.messageId;
112
- else if (origin === 'user') messageId = newTranscriptMessageId();
115
+ else if (ownsPrompt) messageId = newTranscriptMessageId();
113
116
  const transcriptMeta = { ...transcriptRouteMetadata(), ...(messageId ? { messageId } : {}) };
114
- if (origin === 'user') flags.pendingTranscriptMeta = transcriptMeta;
117
+ if (ownsPrompt) flags.pendingTranscriptMeta = transcriptMeta;
115
118
  pushItem({
116
119
  kind: 'user',
117
120
  id,
118
121
  text,
119
122
  ...transcriptMeta,
123
+ ...routineTriggerField(extra),
120
124
  ...(extra && typeof extra.sender === 'string' && extra.sender ? { sender: extra.sender } : {}),
121
125
  ...(extra && typeof extra.device === 'string' && extra.device ? { device: extra.device } : {}),
122
126
  // Byte-free attachment metadata (name/mime/size) from the queue entry —
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Transcript spill storage, history paging, and session item-state mutations.
3
3
  */
4
+ import { randomBytes } from 'node:crypto';
4
5
  import { readFileSync } from 'node:fs';
5
6
  import { join } from 'node:path';
6
7
  import { Worker } from 'node:worker_threads';
@@ -19,8 +20,11 @@ export const tuiDebug = (msg) => {
19
20
  } catch {}
20
21
  };
21
22
 
23
+ // Item ids outlive the process (queued-input mirror, turn change records), so
24
+ // a restarted process never issues an id again.
25
+ const ID_PREFIX = `it_${randomBytes(4).toString('hex')}_`;
22
26
  let _idSeq = 0;
23
- export const nextId = () => `it_${++_idSeq}`;
27
+ export const nextId = () => `${ID_PREFIX}${++_idSeq}`;
24
28
 
25
29
  export const TRANSCRIPT_LIVE_ITEM_CAP = 512;
26
30
  export const TRANSCRIPT_SPILL_CHUNK_ITEMS = 128;