mixdog 0.9.18 → 0.9.20

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +93 -29
  3. package/package.json +5 -3
  4. package/scripts/build-runtime-windows.ps1 +242 -242
  5. package/scripts/build-tui.mjs +6 -0
  6. package/scripts/hook-bus-test.mjs +8 -0
  7. package/scripts/log-writer-guard-smoke.mjs +131 -0
  8. package/scripts/output-style-smoke.mjs +2 -2
  9. package/scripts/reactive-compact-persist-smoke.mjs +22 -6
  10. package/scripts/recall-bench-cases.json +10 -0
  11. package/scripts/recall-bench.mjs +91 -2
  12. package/scripts/recall-quality-cases.json +1 -2
  13. package/scripts/recall-usecase-cases.json +1 -1
  14. package/scripts/session-ingest-compaction-smoke.mjs +241 -0
  15. package/scripts/smoke-runtime-negative.ps1 +106 -106
  16. package/scripts/tool-efficiency-diag.mjs +5 -2
  17. package/scripts/tool-smoke.mjs +251 -72
  18. package/src/agents/debugger/AGENT.md +3 -3
  19. package/src/agents/heavy-worker/AGENT.md +7 -10
  20. package/src/agents/maintainer/AGENT.md +1 -2
  21. package/src/agents/reviewer/AGENT.md +1 -2
  22. package/src/agents/worker/AGENT.md +5 -8
  23. package/src/defaults/agents.json +3 -0
  24. package/src/help.mjs +2 -5
  25. package/src/lib/mixdog-debug.cjs +13 -0
  26. package/src/mixdog-session-runtime.mjs +7 -3311
  27. package/src/rules/agent/00-core.md +4 -3
  28. package/src/rules/agent/30-explorer.md +53 -22
  29. package/src/rules/lead/02-channels.md +3 -3
  30. package/src/rules/lead/lead-tool.md +3 -2
  31. package/src/rules/shared/01-tool.md +24 -29
  32. package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  34. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
  36. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
  37. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
  38. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
  39. package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
  40. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
  41. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
  42. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
  43. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
  44. package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
  45. package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
  46. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
  47. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
  48. package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
  49. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
  50. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
  51. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
  52. package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
  53. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
  54. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
  55. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
  56. package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
  57. package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
  58. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
  59. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
  60. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
  61. package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
  62. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
  63. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
  64. package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
  65. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
  66. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
  67. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
  68. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
  70. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
  71. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
  72. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
  73. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
  74. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
  75. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
  76. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
  77. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
  78. package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
  79. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
  80. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  81. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
  82. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
  83. package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
  84. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
  85. package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
  86. package/src/runtime/channels/backends/discord.mjs +43 -1
  87. package/src/runtime/channels/index.mjs +6 -2096
  88. package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
  89. package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
  90. package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
  91. package/src/runtime/channels/lib/memory-client.mjs +32 -14
  92. package/src/runtime/channels/lib/network-retry.mjs +23 -0
  93. package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
  94. package/src/runtime/channels/lib/owned-runtime.mjs +547 -0
  95. package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
  96. package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
  97. package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
  98. package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
  99. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
  100. package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
  101. package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
  102. package/src/runtime/channels/lib/worker-main.mjs +771 -0
  103. package/src/runtime/memory/index.mjs +73 -1725
  104. package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
  105. package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
  106. package/src/runtime/memory/lib/http-router.mjs +772 -0
  107. package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
  108. package/src/runtime/memory/lib/memory-embed.mjs +28 -7
  109. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
  110. package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
  111. package/src/runtime/memory/lib/query-handlers.mjs +38 -6
  112. package/src/runtime/memory/lib/recall-format.mjs +106 -6
  113. package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
  114. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  115. package/src/runtime/shared/atomic-file.mjs +10 -4
  116. package/src/runtime/shared/background-tasks.mjs +4 -2
  117. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  118. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  119. package/src/runtime/shared/tool-surface.mjs +30 -1
  120. package/src/session-runtime/boot-profile.mjs +36 -0
  121. package/src/session-runtime/channel-config-api.mjs +70 -0
  122. package/src/session-runtime/config-lifecycle.mjs +1 -1
  123. package/src/session-runtime/context-status.mjs +181 -0
  124. package/src/session-runtime/cwd-plugins.mjs +46 -3
  125. package/src/session-runtime/env.mjs +17 -0
  126. package/src/session-runtime/lifecycle-api.mjs +242 -0
  127. package/src/session-runtime/mcp-glue.mjs +24 -3
  128. package/src/session-runtime/model-route-api.mjs +198 -0
  129. package/src/session-runtime/output-styles.mjs +44 -10
  130. package/src/session-runtime/provider-auth-api.mjs +135 -0
  131. package/src/session-runtime/resource-api.mjs +282 -0
  132. package/src/session-runtime/runtime-core.mjs +2046 -0
  133. package/src/session-runtime/session-turn-api.mjs +274 -0
  134. package/src/session-runtime/tool-catalog.mjs +18 -264
  135. package/src/session-runtime/tool-defs.mjs +2 -2
  136. package/src/session-runtime/workflow-agents-api.mjs +238 -0
  137. package/src/session-runtime/workflow.mjs +16 -1
  138. package/src/standalone/agent-tool.mjs +2 -2
  139. package/src/standalone/channel-worker.mjs +78 -5
  140. package/src/standalone/explore-tool.mjs +1 -1
  141. package/src/standalone/memory-runtime-proxy.mjs +56 -6
  142. package/src/tui/App.jsx +88 -97
  143. package/src/tui/app/channel-pickers.mjs +45 -0
  144. package/src/tui/app/core-memory-picker.mjs +1 -1
  145. package/src/tui/app/slash-commands.mjs +0 -1
  146. package/src/tui/app/slash-dispatch.mjs +0 -16
  147. package/src/tui/app/transcript-window.mjs +44 -1
  148. package/src/tui/app/use-mouse-input.mjs +15 -2
  149. package/src/tui/app/use-prompt-handlers.mjs +7 -94
  150. package/src/tui/app/use-transcript-scroll.mjs +82 -4
  151. package/src/tui/app/use-transcript-window.mjs +65 -5
  152. package/src/tui/components/PromptInput.jsx +33 -64
  153. package/src/tui/components/ToolExecution.jsx +2 -2
  154. package/src/tui/dist/index.mjs +7908 -7558
  155. package/src/tui/engine/context-state.mjs +145 -0
  156. package/src/tui/engine/prompt-history.mjs +27 -0
  157. package/src/tui/engine/session-api-ext.mjs +478 -0
  158. package/src/tui/engine/session-api.mjs +545 -0
  159. package/src/tui/engine/session-flow.mjs +485 -0
  160. package/src/tui/engine/turn.mjs +1078 -0
  161. package/src/tui/engine.mjs +69 -2582
  162. package/src/tui/index.jsx +7 -0
  163. package/src/tui/lib/voice-setup.mjs +166 -0
  164. package/src/tui/paste-attachments.mjs +12 -5
  165. package/src/tui/prompt-history-store.mjs +125 -12
  166. package/vendor/ink/build/ink.js +16 -1
  167. package/vendor/ink/build/output.js +30 -4
  168. package/vendor/ink/build/render.js +5 -0
  169. package/scripts/bench/cache-probe-tasks.json +0 -8
  170. package/scripts/bench/lead-review-tasks-r3.json +0 -20
  171. package/scripts/bench/lead-review-tasks.json +0 -20
  172. package/scripts/bench/r4-mixed-tasks.json +0 -20
  173. package/scripts/bench/r5-orchestrated-task.json +0 -7
  174. package/scripts/bench/review-tasks.json +0 -20
  175. package/scripts/bench/round-codex.json +0 -114
  176. package/scripts/bench/round-mixdog-lead-r3.json +0 -269
  177. package/scripts/bench/round-mixdog-lead.json +0 -269
  178. package/scripts/bench/round-mixdog.json +0 -126
  179. package/scripts/bench/round-r10-bigsample.json +0 -679
  180. package/scripts/bench/round-r11-codexalign.json +0 -257
  181. package/scripts/bench/round-r13-clientmeta.json +0 -464
  182. package/scripts/bench/round-r14-betafeatures.json +0 -466
  183. package/scripts/bench/round-r15-fulldefault.json +0 -462
  184. package/scripts/bench/round-r16-sessionid.json +0 -466
  185. package/scripts/bench/round-r17-wirebytes.json +0 -456
  186. package/scripts/bench/round-r18-prewarm.json +0 -468
  187. package/scripts/bench/round-r19-clean.json +0 -472
  188. package/scripts/bench/round-r20-prewarm-clean.json +0 -475
  189. package/scripts/bench/round-r21-delta-retry.json +0 -473
  190. package/scripts/bench/round-r22-full-probe.json +0 -693
  191. package/scripts/bench/round-r23-itemprobe.json +0 -701
  192. package/scripts/bench/round-r24-shapefix.json +0 -677
  193. package/scripts/bench/round-r25-serial.json +0 -464
  194. package/scripts/bench/round-r26-parallel3.json +0 -671
  195. package/scripts/bench/round-r27-parallel10.json +0 -894
  196. package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
  197. package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
  198. package/scripts/bench/round-r30-instid.json +0 -253
  199. package/scripts/bench/round-r31-upgradeprobe.json +0 -256
  200. package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
  201. package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
  202. package/scripts/bench/round-r34-orchestrated.json +0 -120
  203. package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
  204. package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
  205. package/scripts/bench/round-r4-codex.json +0 -114
  206. package/scripts/bench/round-r4-mixed.json +0 -225
  207. package/scripts/bench/round-r5-gpt-lead.json +0 -259
  208. package/scripts/bench/round-r6-codex.json +0 -114
  209. package/scripts/bench/round-r6-solo.json +0 -257
  210. package/scripts/bench/round-r7-full.json +0 -254
  211. package/scripts/bench/round-r8-fulldefault.json +0 -255
  212. package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
  213. package/src/tui/lib/voice-recorder.mjs +0 -469
  214. package/src/workflows/sequential/WORKFLOW.md +0 -51
@@ -0,0 +1,145 @@
1
+ /**
2
+ * src/tui/engine/context-state.mjs - route/context/agent-status derivations.
3
+ *
4
+ * Extracted from engine.mjs unchanged. These read the live runtime + store
5
+ * snapshot and (for the two sync helpers) mutate state.stats / the display
6
+ * context fields IN PLACE — the exact same object the store owns — so the
7
+ * immutable-emit contract is preserved by their callers, which follow up with
8
+ * a set({ stats: { ...state.stats }, ...routeState() }). getState() must return
9
+ * the live (latest) store object; getPendingSessionReset() gates the stats
10
+ * sync exactly as the old inline `pendingSessionReset` closure did.
11
+ */
12
+ export function createContextState({ runtime, getState, getPendingSessionReset }) {
13
+ const autoClearState = () => runtime.getAutoClear?.() || runtime.autoClear || { enabled: true, idleMs: 60 * 60 * 1000, custom: false, providerDefault: 60 * 60 * 1000, provider: null, minContextPercent: 10 };
14
+ const AGENT_STATUS_CACHE_MS = 250;
15
+ let agentStatusCache = null;
16
+ let agentStatusCacheAt = 0;
17
+ const agentStatusState = ({ force = false } = {}) => {
18
+ const now = Date.now();
19
+ if (!force && agentStatusCache && now - agentStatusCacheAt < AGENT_STATUS_CACHE_MS) return agentStatusCache;
20
+ const status = runtime.agentStatus?.() || {};
21
+ agentStatusCache = {
22
+ agentWorkers: Array.isArray(status.agentWorkers) ? status.agentWorkers : [],
23
+ agentJobs: Array.isArray(status.agentJobs) ? status.agentJobs : [],
24
+ agentScope: status.agentScope || null,
25
+ };
26
+ agentStatusCacheAt = now;
27
+ return agentStatusCache;
28
+ };
29
+ const baseRouteState = () => ({
30
+ sessionId: runtime.id,
31
+ clientHostPid: runtime.clientHostPid || null,
32
+ model: runtime.model,
33
+ provider: runtime.provider,
34
+ effort: runtime.effort,
35
+ effortOptions: runtime.effortOptions,
36
+ fast: runtime.fast,
37
+ fastCapable: runtime.fastCapable,
38
+ contextWindow: runtime.contextWindow,
39
+ rawContextWindow: runtime.rawContextWindow,
40
+ effectiveContextWindowPercent: runtime.effectiveContextWindowPercent,
41
+ cwd: runtime.cwd || process.cwd(),
42
+ systemShell: runtime.systemShell || { source: 'auto', command: '', effective: '' },
43
+ searchRoute: runtime.getSearchRoute?.() || runtime.searchRoute || null,
44
+ autoClear: autoClearState(),
45
+ workflow: runtime.workflow || null,
46
+ remoteEnabled: runtime.isRemoteEnabled?.() === true,
47
+ });
48
+
49
+ const routeState = () => {
50
+ const state = getState();
51
+ return {
52
+ ...baseRouteState(),
53
+ displayContextWindow: state.displayContextWindow || 0,
54
+ compactBoundaryTokens: state.compactBoundaryTokens || 0,
55
+ autoCompactTokenLimit: state.autoCompactTokenLimit || 0,
56
+ };
57
+ };
58
+
59
+ function syncContextDisplayFields(ctx = null) {
60
+ const status = ctx || runtime.contextStatus?.() || null;
61
+ if (!status) return;
62
+ const state = getState();
63
+ const displayWindow = Number(status.contextWindow || 0);
64
+ const compactBoundary = Number(status.compaction?.boundaryTokens || 0);
65
+ // Prefer the resolved trigger (boundary - buffer): the statusline uses it
66
+ // as the display denominator so context % reads 100% exactly when
67
+ // auto-compact fires, instead of stalling at ~90% of the boundary.
68
+ const autoCompact = Number(
69
+ status.compaction?.triggerTokens
70
+ || status.compaction?.autoCompactTokenLimit
71
+ || runtime.session?.autoCompactTokenLimit
72
+ || 0,
73
+ );
74
+ if (displayWindow > 0) state.displayContextWindow = displayWindow;
75
+ if (compactBoundary > 0) state.compactBoundaryTokens = compactBoundary;
76
+ if (autoCompact > 0) state.autoCompactTokenLimit = autoCompact;
77
+ }
78
+
79
+ const syncContextStats = ({ allowEstimated = false } = {}) => {
80
+ if (getPendingSessionReset()) return null;
81
+ const ctx = runtime.contextStatus?.() || null;
82
+ if (!ctx) return null;
83
+ syncContextDisplayFields(ctx);
84
+ const state = getState();
85
+ const hasProviderUsage = Number(state.stats.latestPromptTokens || state.stats.latestInputTokens || state.stats.inputTokens || 0) > 0;
86
+ const hasApiContextUsage = Number(ctx?.lastApiRequestTokens ?? ctx?.usage?.lastContextTokens ?? 0) > 0;
87
+ const hasTurnActivity = state.busy === true
88
+ || state.spinner != null
89
+ || state.thinking != null;
90
+ const isFreshSession = !hasProviderUsage && !hasApiContextUsage && !hasTurnActivity;
91
+ if (isFreshSession) {
92
+ state.stats.currentEstimatedContextTokens = 0;
93
+ state.stats.currentContextTokens = 0;
94
+ state.stats.currentContextSource = null;
95
+ state.stats.currentContextUpdatedAt = Date.now();
96
+ return ctx;
97
+ }
98
+ const estimatedTokens = Math.max(0, Number(ctx.currentEstimatedTokens ?? ctx.usedTokens ?? 0));
99
+ const usedTokens = Math.max(0, Number(ctx.usedTokens ?? estimatedTokens ?? 0));
100
+ const usedSource = String(ctx.usedSource || '').toLowerCase();
101
+ const shouldPublishEstimate = allowEstimated && (
102
+ usedSource === 'estimated'
103
+ || Number(ctx.currentEstimatedTokens) > 0
104
+ || usedTokens > 0
105
+ );
106
+ if (!allowEstimated && !hasProviderUsage && usedSource !== 'last_api_request') return ctx;
107
+ if (shouldPublishEstimate) {
108
+ state.stats.currentEstimatedContextTokens = estimatedTokens;
109
+ state.stats.currentContextSource = 'estimated';
110
+ state.stats.currentContextTokens = 0;
111
+ } else if (allowEstimated && (hasProviderUsage || hasApiContextUsage || hasTurnActivity)) {
112
+ state.stats.currentEstimatedContextTokens = estimatedTokens;
113
+ state.stats.currentContextSource = usedSource || (estimatedTokens > 0 ? 'estimated' : null);
114
+ const publishedSource = String(state.stats.currentContextSource || '').toLowerCase();
115
+ if (publishedSource === 'last_api_request') {
116
+ const apiUsed = Math.max(0, Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0));
117
+ state.stats.currentContextTokens = apiUsed;
118
+ } else if (publishedSource === 'estimated') {
119
+ state.stats.currentContextTokens = 0;
120
+ } else {
121
+ state.stats.currentContextTokens = usedTokens > 0 ? usedTokens : 0;
122
+ }
123
+ } else {
124
+ state.stats.currentEstimatedContextTokens = 0;
125
+ if (usedSource === 'last_api_request' && Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0) > 0) {
126
+ state.stats.currentContextTokens = Math.max(0, Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0));
127
+ state.stats.currentContextSource = 'last_api_request';
128
+ } else {
129
+ state.stats.currentContextTokens = 0;
130
+ state.stats.currentContextSource = null;
131
+ }
132
+ }
133
+ state.stats.currentContextUpdatedAt = Date.now();
134
+ return ctx;
135
+ };
136
+
137
+ return {
138
+ autoClearState,
139
+ agentStatusState,
140
+ baseRouteState,
141
+ routeState,
142
+ syncContextDisplayFields,
143
+ syncContextStats,
144
+ };
145
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * src/tui/engine/prompt-history.mjs - pure prompt-history derivation.
3
+ *
4
+ * Extracted from engine.mjs unchanged: the newest-first, deduped user-prompt
5
+ * history the engine publishes on state.promptHistoryList. Pure (input items
6
+ * → array); callers decide when/whether to publish so the store's
7
+ * immutable-emit contract is preserved.
8
+ */
9
+ export const PROMPT_HISTORY_LIMIT = 50;
10
+
11
+ export const promptHistoryKey = (value) => String(value || '').trim().replace(/\s+/g, ' ');
12
+
13
+ export function recomputePromptHistory(sourceItems, limit = PROMPT_HISTORY_LIMIT) {
14
+ const items = Array.isArray(sourceItems) ? sourceItems : [];
15
+ const seen = new Set();
16
+ const history = [];
17
+ for (let i = items.length - 1; i >= 0 && history.length < limit; i -= 1) {
18
+ const item = items[i];
19
+ if (item?.kind !== 'user') continue;
20
+ const text = String(item.text || '').trim();
21
+ const key = promptHistoryKey(text);
22
+ if (!key || seen.has(key)) continue;
23
+ seen.add(key);
24
+ history.push(text);
25
+ }
26
+ return history;
27
+ }
@@ -0,0 +1,478 @@
1
+ /**
2
+ * src/tui/engine/session-api-ext.mjs - part of the public engine session object.
3
+ */
4
+ import { listThemes, getThemeSetting, setThemeSetting } from '../theme.mjs';
5
+ import { resetAllStreamingMarkdownStablePrefixes } from '../markdown/streaming-markdown.mjs';
6
+ import { toolResultText } from './tool-result-text.mjs';
7
+ import { parseSyntheticAgentMessage } from './agent-envelope.mjs';
8
+ import { flushTuiSteeringPersist } from './tui-steering-persist.mjs';
9
+
10
+ export function createEngineApiB(bag) {
11
+ const {
12
+ runtime, nextId, flags, lifecycle, listeners, getState, set, replaceItems, pushNotice, removeNotice, setProgressHint, clearToastTimers, routeState, syncContextStats, finishToolApproval, denyAllToolApprovals, restoreLeadSteeringFromDisk, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, resetStatsAndSyncContext,
13
+ } = bag;
14
+ return {
15
+ resolveToolApproval: (id, decision = {}) => {
16
+ const approved = decision === true || decision?.approved === true;
17
+ return finishToolApproval(id, approved, decision?.reason || (approved ? 'approved by user' : 'denied by user'));
18
+ },
19
+ listPresets: () => {
20
+ return runtime.listPresets();
21
+ },
22
+ listProviderModels: (options = {}) => {
23
+ return runtime.listProviderModels(options);
24
+ },
25
+ getSearchRoute: () => {
26
+ return runtime.getSearchRoute?.() || runtime.searchRoute || null;
27
+ },
28
+ listSearchModels: (options = {}) => {
29
+ return runtime.listSearchModels?.(options) || [];
30
+ },
31
+ setSearchRoute: async (opts) => {
32
+ if (getState().commandBusy) return null;
33
+ const beforeRouteState = routeState();
34
+ const optimisticSearchRoute = opts?.provider && opts?.model
35
+ ? {
36
+ provider: String(opts.provider).trim(),
37
+ model: String(opts.model).trim(),
38
+ ...(opts.effort ? { effort: opts.effort } : {}),
39
+ ...(opts.fast === true ? { fast: true } : {}),
40
+ ...(opts.toolType ? { toolType: opts.toolType } : {}),
41
+ }
42
+ : null;
43
+ set({ commandBusy: true });
44
+ try {
45
+ if (optimisticSearchRoute?.provider && optimisticSearchRoute.model) {
46
+ set({ searchRoute: optimisticSearchRoute });
47
+ }
48
+ const result = await runtime.setSearchRoute?.(opts);
49
+ set({ ...routeState(), stats: { ...getState().stats } });
50
+ return result;
51
+ } catch (e) {
52
+ set({ searchRoute: beforeRouteState.searchRoute || null });
53
+ throw e;
54
+ } finally {
55
+ set({ commandBusy: false });
56
+ }
57
+ },
58
+ listAgents: () => {
59
+ return runtime.listAgents?.() || [];
60
+ },
61
+ listWorkflows: () => {
62
+ return runtime.listWorkflows?.() || [];
63
+ },
64
+ getOutputStyle: () => {
65
+ return runtime.getOutputStyle?.() || runtime.listOutputStyles?.() || null;
66
+ },
67
+ listOutputStyles: () => {
68
+ return runtime.listOutputStyles?.() || runtime.getOutputStyle?.() || { styles: [], current: null, configured: 'default' };
69
+ },
70
+ setOutputStyle: async (styleId) => {
71
+ if (getState().commandBusy) return null;
72
+ set({ commandBusy: true });
73
+ try {
74
+ const result = await runtime.setOutputStyle?.(styleId);
75
+ resetStats();
76
+ set({ ...routeState(), stats: { ...getState().stats } });
77
+ // Defer the context recompute (transcript scan) off this tick so
78
+ // the style change repaints immediately; stats settle right after.
79
+ setTimeout(() => {
80
+ syncContextStats({ allowEstimated: true });
81
+ set({ stats: { ...getState().stats } });
82
+ }, 0);
83
+ return result;
84
+ } finally {
85
+ set({ commandBusy: false });
86
+ }
87
+ },
88
+ setWorkflow: async (workflowId) => {
89
+ if (getState().commandBusy) return null;
90
+ set({ commandBusy: true });
91
+ try {
92
+ const result = await runtime.setWorkflow?.(workflowId);
93
+ set({ ...routeState(), stats: { ...getState().stats } });
94
+ return result;
95
+ } finally {
96
+ set({ commandBusy: false });
97
+ }
98
+ },
99
+ // Toggle Discord remote mode for this session. Flips the runtime's
100
+ // remoteEnabled flag (booting/stopping the channel worker) and returns the
101
+ // NEW enabled getState() so the caller can render an ON/OFF notice.
102
+ toggleRemote: () => {
103
+ const enabled = runtime.isRemoteEnabled?.() === true;
104
+ if (enabled) runtime.stopRemote?.();
105
+ else runtime.startRemote?.();
106
+ const next = runtime.isRemoteEnabled?.() === true;
107
+ set({ remoteEnabled: next });
108
+ return next;
109
+ },
110
+ // Force-claim remote for this session (single-holder, last-wins). Always
111
+ // turns remote ON here and steals the bridge seat; the previous holder is
112
+ // superseded and flips itself OFF via onRemoteStateChange. Used by the
113
+ // `/remote` slash command — repeated /remote just re-claims (idempotent).
114
+ claimRemote: () => {
115
+ runtime.startRemote?.();
116
+ const next = runtime.isRemoteEnabled?.() === true;
117
+ set({ remoteEnabled: next });
118
+ return next;
119
+ },
120
+ isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
121
+ // Theme is a TUI-local concern (no runtime round-trip). listThemes returns
122
+ // picker metadata; getTheme reports the active id; setTheme applies the
123
+ // palette in-place + persists ui.theme and bumps a themeEpoch so the React
124
+ // tree re-renders (markdown/status/spinner colorizers re-resolve).
125
+ listThemes: () => listThemes(),
126
+ getTheme: () => getThemeSetting(),
127
+ setTheme: (id, options = {}) => {
128
+ const applied = setThemeSetting(id, options);
129
+ set({ themeEpoch: (getState().themeEpoch || 0) + 1 });
130
+ return applied;
131
+ },
132
+ setAgentRoute: async (agentId, opts) => {
133
+ return await runtime.setAgentRoute?.(agentId, opts);
134
+ },
135
+ setDefaultProvider: async (provider) => {
136
+ return await runtime.setDefaultProvider?.(provider);
137
+ },
138
+ listProviders: () => {
139
+ return runtime.listProviders();
140
+ },
141
+ getProviderSetup: () => {
142
+ return runtime.getProviderSetup();
143
+ },
144
+ getUsageDashboard: async (options = {}) => {
145
+ return await runtime.getUsageDashboard?.(options);
146
+ },
147
+ getOnboardingStatus: () => {
148
+ return runtime.getOnboardingStatus?.() || { completed: true, workflowRoutes: {} };
149
+ },
150
+ skipOnboarding: () => {
151
+ // Completed-marking only; no route/agent/provider writes.
152
+ return runtime.skipOnboarding?.() || null;
153
+ },
154
+ completeOnboarding: async (payload = {}) => {
155
+ if (getState().commandBusy) return null;
156
+ set({ commandBusy: true });
157
+ try {
158
+ const result = await runtime.completeOnboarding?.(payload);
159
+ resetStatsAndSyncContext();
160
+ set({ ...routeState(), stats: { ...getState().stats } });
161
+ pushNotice('first-run setup saved', 'info');
162
+ return result;
163
+ } finally {
164
+ set({ commandBusy: false });
165
+ }
166
+ },
167
+ loginOAuthProvider: async (provider) => {
168
+ if (getState().commandBusy) return false;
169
+ set({ commandBusy: true });
170
+ try {
171
+ const result = await runtime.loginOAuthProvider(provider);
172
+ pushNotice(`provider oauth ok: ${result.provider}`, 'info');
173
+ return true;
174
+ } finally {
175
+ set({ commandBusy: false });
176
+ }
177
+ },
178
+ beginOAuthProviderLogin: async (provider) => {
179
+ if (getState().commandBusy) throw new Error('command busy');
180
+ set({ commandBusy: true });
181
+ try {
182
+ const result = await runtime.beginOAuthProviderLogin(provider);
183
+ pushNotice(`provider oauth started: ${result.provider}`, 'info');
184
+ return result;
185
+ } finally {
186
+ set({ commandBusy: false });
187
+ }
188
+ },
189
+ saveProviderApiKey: (provider, secret) => {
190
+ const result = runtime.saveProviderApiKey(provider, secret);
191
+ pushNotice(`provider api key saved: ${result.provider}`, 'info');
192
+ return true;
193
+ },
194
+ saveOpenCodeGoUsageAuth: (opts) => {
195
+ const result = runtime.saveOpenCodeGoUsageAuth(opts);
196
+ pushNotice(result.workspaceId
197
+ ? `OpenCode Go usage auth saved: ${result.workspaceId}`
198
+ : 'OpenCode Go usage auth saved',
199
+ 'info');
200
+ return true;
201
+ },
202
+ loginOpenCodeGoUsage: async () => {
203
+ if (getState().commandBusy) throw new Error('command busy');
204
+ set({ commandBusy: true });
205
+ try {
206
+ return await runtime.loginOpenCodeGoUsage();
207
+ } finally {
208
+ set({ commandBusy: false });
209
+ }
210
+ },
211
+ saveOpenAIUsageSessionKey: (secret) => {
212
+ runtime.saveOpenAIUsageSessionKey(secret);
213
+ pushNotice('OpenAI usage auth saved', 'info');
214
+ return true;
215
+ },
216
+ setLocalProvider: (provider, opts) => {
217
+ const result = runtime.setLocalProvider(provider, opts);
218
+ pushNotice(`local provider ${result.enabled ? 'enabled' : 'disabled'}: ${result.provider}`, 'info');
219
+ return true;
220
+ },
221
+ authenticateProvider: async (provider, secret) => {
222
+ if (getState().commandBusy) return false;
223
+ set({ commandBusy: true });
224
+ try {
225
+ const result = await runtime.authenticateProvider(provider, secret);
226
+ pushNotice(`provider auth ok: ${result.provider} (${result.type})`, 'info');
227
+ return true;
228
+ } finally {
229
+ set({ commandBusy: false });
230
+ }
231
+ },
232
+ forgetProviderAuth: (provider) => {
233
+ const result = runtime.forgetProviderAuth(provider);
234
+ pushNotice(`provider auth forgotten: ${result.provider}`, 'info');
235
+ return true;
236
+ },
237
+ getChannelSetup: () => {
238
+ return runtime.getChannelSetup();
239
+ },
240
+ getChannelWorkerStatus: () => runtime.getChannelWorkerStatus?.(),
241
+ setBackend: (name) => runtime.setBackend?.(name),
242
+ saveDiscordToken: (token) => {
243
+ const result = runtime.saveDiscordToken(token);
244
+ pushNotice('discord token saved', 'info');
245
+ return result;
246
+ },
247
+ forgetDiscordToken: () => {
248
+ const result = runtime.forgetDiscordToken();
249
+ pushNotice('discord token forgotten', 'info');
250
+ return result;
251
+ },
252
+ saveTelegramToken: (token) => {
253
+ const result = runtime.saveTelegramToken?.(token);
254
+ pushNotice('telegram token saved', 'info');
255
+ return result;
256
+ },
257
+ forgetTelegramToken: () => {
258
+ const result = runtime.forgetTelegramToken?.();
259
+ pushNotice('telegram token forgotten', 'info');
260
+ return result;
261
+ },
262
+ saveWebhookAuthtoken: (token) => {
263
+ const result = runtime.saveWebhookAuthtoken(token);
264
+ pushNotice('webhook/ngrok authtoken saved', 'info');
265
+ return result;
266
+ },
267
+ forgetWebhookAuthtoken: () => {
268
+ const result = runtime.forgetWebhookAuthtoken();
269
+ pushNotice('webhook/ngrok authtoken forgotten', 'info');
270
+ return result;
271
+ },
272
+ setChannel: (entry) => {
273
+ const result = runtime.setChannel(entry);
274
+ pushNotice('channel saved', 'info');
275
+ return result;
276
+ },
277
+ setWebhookConfig: (patch) => {
278
+ const result = runtime.setWebhookConfig(patch);
279
+ pushNotice('webhook config updated', 'info');
280
+ return result;
281
+ },
282
+ saveSchedule: (entry) => {
283
+ const result = runtime.saveSchedule(entry);
284
+ pushNotice(`schedule saved: ${result.name}`, 'info');
285
+ return result;
286
+ },
287
+ deleteSchedule: (name) => {
288
+ const result = runtime.deleteSchedule(name);
289
+ pushNotice(`schedule deleted: ${name}`, 'info');
290
+ return result;
291
+ },
292
+ setScheduleEnabled: (name, enabled) => {
293
+ const result = runtime.setScheduleEnabled(name, enabled);
294
+ pushNotice(`schedule ${enabled ? 'enabled' : 'disabled'}: ${name}`, 'info');
295
+ return result;
296
+ },
297
+ saveWebhook: (entry) => {
298
+ const result = runtime.saveWebhook(entry);
299
+ pushNotice(`webhook saved: ${result.name}`, 'info');
300
+ return result;
301
+ },
302
+ deleteWebhook: (name) => {
303
+ const result = runtime.deleteWebhook(name);
304
+ pushNotice(`webhook deleted: ${name}`, 'info');
305
+ return result;
306
+ },
307
+ setWebhookEnabled: (name, enabled) => {
308
+ const result = runtime.setWebhookEnabled(name, enabled);
309
+ pushNotice(`webhook ${enabled ? 'enabled' : 'disabled'}: ${name}`, 'info');
310
+ return result;
311
+ },
312
+ setRoute: async (opts) => {
313
+ if (getState().commandBusy) return false;
314
+ set({ commandBusy: true });
315
+ try {
316
+ const routeOpts = opts && typeof opts === 'object' ? opts : {};
317
+ // Default: apply to the NEXT session only. Only an explicit
318
+ // `applyToCurrentSession: true` rewrites the live session in place.
319
+ const applyToCurrentSession = routeOpts.applyToCurrentSession === true;
320
+ const { applyToCurrentSession: _drop, ...nextRoute } = routeOpts;
321
+ await runtime.setRoute(nextRoute, { applyToCurrentSession });
322
+ if (applyToCurrentSession) syncContextStats({ allowEstimated: true });
323
+ set({ ...routeState(), stats: { ...getState().stats } });
324
+ return true;
325
+ } finally {
326
+ set({ commandBusy: false });
327
+ }
328
+ },
329
+ pushNotice,
330
+ removeNotice,
331
+ setProgressHint,
332
+ clear: async () => {
333
+ if (getState().commandBusy) return false;
334
+ set({ commandBusy: true });
335
+ clearToastTimers();
336
+ resetAllStreamingMarkdownStablePrefixes();
337
+ const rollbackSnapshot = snapshotTuiBeforeSessionReset();
338
+ resetTuiForPendingSessionReset();
339
+ set({
340
+ items: getState().items,
341
+ toasts: getState().toasts,
342
+ queued: getState().queued,
343
+ thinking: null,
344
+ spinner: null,
345
+ lastTurn: null,
346
+ sessionId: null,
347
+ stats: { ...getState().stats },
348
+ });
349
+ try {
350
+ await runtime.clear({ recoverAgent: true });
351
+ clearUiActivityBeforeContextSync();
352
+ flags.pendingSessionReset = false;
353
+ resetStatsAndSyncContext();
354
+ set({ items: replaceItems([]), toasts: [], queued: [], thinking: null, spinner: null, lastTurn: null, ...routeState(), stats: { ...getState().stats } });
355
+ flags.lastUserActivityAt = Date.now();
356
+ return true;
357
+ } catch (error) {
358
+ restoreTuiAfterFailedSessionReset(rollbackSnapshot);
359
+ throw error;
360
+ } finally {
361
+ flags.pendingSessionReset = false;
362
+ set({ commandBusy: false });
363
+ }
364
+ },
365
+ listSessions: () => {
366
+ return runtime.listSessions();
367
+ },
368
+ newSession: async () => {
369
+ if (getState().commandBusy) return false;
370
+ set({ commandBusy: true });
371
+ clearToastTimers();
372
+ resetAllStreamingMarkdownStablePrefixes();
373
+ const rollbackSnapshot = snapshotTuiBeforeSessionReset();
374
+ resetTuiForPendingSessionReset();
375
+ set({
376
+ items: getState().items,
377
+ toasts: getState().toasts,
378
+ queued: getState().queued,
379
+ thinking: null,
380
+ spinner: null,
381
+ lastTurn: null,
382
+ sessionId: null,
383
+ stats: { ...getState().stats },
384
+ });
385
+ try {
386
+ await runtime.newSession();
387
+ clearUiActivityBeforeContextSync();
388
+ flags.pendingSessionReset = false;
389
+ resetStatsAndSyncContext();
390
+ set({ items: replaceItems([]), toasts: [], queued: [], thinking: null, spinner: null, lastTurn: null, ...routeState(), stats: { ...getState().stats } });
391
+ return true;
392
+ } catch (error) {
393
+ restoreTuiAfterFailedSessionReset(rollbackSnapshot);
394
+ throw error;
395
+ } finally {
396
+ flags.pendingSessionReset = false;
397
+ set({ commandBusy: false });
398
+ }
399
+ },
400
+ resume: async (id) => {
401
+ if (getState().commandBusy) return false;
402
+ set({ commandBusy: true, commandStatus: { active: true, verb: 'Resuming conversation', startedAt: Date.now(), mode: 'resuming' } });
403
+ clearToastTimers();
404
+ try {
405
+ const r = await runtime.resume(id);
406
+ if (!r) return false;
407
+ resetStatsAndSyncContext();
408
+ const items = [];
409
+ for (const m of r.messages || []) {
410
+ if (m.role === 'user') {
411
+ // content may be a string OR an array of parts (text/tool-call
412
+ // interleaving) — toolResultText coerces both to readable text so
413
+ // array-content messages aren't silently dropped.
414
+ const text = (typeof m.content === 'string' ? m.content : toolResultText(m.content)).trim();
415
+ if (text) {
416
+ const synthetic = parseSyntheticAgentMessage(text);
417
+ if (synthetic) {
418
+ const label = synthetic.label || 'notification';
419
+ items.push({
420
+ kind: 'tool',
421
+ id: nextId(),
422
+ name: synthetic.name || 'agent',
423
+ args: synthetic.args || {
424
+ type: label,
425
+ task_id: synthetic.taskId || undefined,
426
+ description: synthetic.summary || 'agent notification',
427
+ },
428
+ result: synthetic.result,
429
+ rawResult: synthetic.rawResult ?? text,
430
+ isError: synthetic.isError ?? /^(failed|error|killed|cancelled)$/i.test(label),
431
+ expanded: false,
432
+ count: 1,
433
+ completedCount: 1,
434
+ startedAt: Date.now(),
435
+ completedAt: Date.now(),
436
+ });
437
+ } else {
438
+ items.push({ kind: 'user', id: nextId(), text });
439
+ }
440
+ }
441
+ } else if (m.role === 'assistant') {
442
+ const text = (typeof m.content === 'string' ? m.content : toolResultText(m.content)).trim();
443
+ if (text) items.push({ kind: 'assistant', id: nextId(), text });
444
+ }
445
+ }
446
+ set({
447
+ items: replaceItems(items),
448
+ toasts: [],
449
+ queued: [],
450
+ thinking: null,
451
+ spinner: null,
452
+ lastTurn: null,
453
+ ...routeState(),
454
+ stats: { ...getState().stats },
455
+ });
456
+ void restoreLeadSteeringFromDisk();
457
+ return true;
458
+ } finally {
459
+ set({ commandBusy: false, commandStatus: null });
460
+ }
461
+ },
462
+
463
+ dispose: async (reason = 'cli-react-exit', options = {}) => {
464
+ if (flags.disposed) return;
465
+ flags.disposed = true;
466
+ clearToastTimers();
467
+ try { clearInterval(lifecycle.runtimePulseTimer); } catch {}
468
+ try { lifecycle.unsubscribeRuntimeNotifications?.(); } catch {}
469
+ lifecycle.unsubscribeRuntimeNotifications = null;
470
+ try { lifecycle.unsubscribeRemoteState?.(); } catch {}
471
+ lifecycle.unsubscribeRemoteState = null;
472
+ denyAllToolApprovals('runtime closing');
473
+ await flushTuiSteeringPersist();
474
+ await runtime.close(reason, options);
475
+ listeners.clear();
476
+ },
477
+ };
478
+ }