mixdog 0.9.95 → 0.9.96

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 (178) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/package.json +12 -9
  6. package/scripts/.tmp-coverage-report.mjs +96 -0
  7. package/scripts/build-tui.mjs +17 -1
  8. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  9. package/src/defaults/mixdog-config.template.json +5 -8
  10. package/src/rules/agent/30-explorer.md +7 -4
  11. package/src/rules/shared/01-tool.md +20 -29
  12. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  13. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  14. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  16. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  17. package/src/runtime/agent/orchestrator/mcp/client.mjs +26 -11
  18. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  19. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  20. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  21. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  22. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  23. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  25. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  26. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  28. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  29. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  30. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  31. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  32. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  33. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -4
  34. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -33
  35. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  36. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  37. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  38. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
  39. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
  40. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +5 -0
  41. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  42. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  44. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  45. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  46. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  48. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
  49. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -33
  50. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  51. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  54. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +118 -18
  55. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  56. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  57. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
  62. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
  63. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +23 -9
  64. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  66. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  70. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  72. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +87 -15
  73. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
  74. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  76. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  77. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +29 -1
  78. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  79. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  80. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +10 -10
  81. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -8
  82. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  83. package/src/runtime/channels/lib/config.mjs +2 -2
  84. package/src/runtime/channels/lib/inbound-handler.mjs +1 -1
  85. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  86. package/src/runtime/memory/index.mjs +5 -38
  87. package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
  88. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  89. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  90. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
  91. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  92. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  93. package/src/runtime/memory/lib/http-router.mjs +8 -0
  94. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  95. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  96. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  97. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  98. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  99. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  100. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  101. package/src/runtime/memory/tool-defs.mjs +1 -5
  102. package/src/runtime/search/tool-defs.mjs +2 -2
  103. package/src/runtime/shared/agent-route-config.mjs +119 -0
  104. package/src/runtime/shared/atomic-file.mjs +80 -4
  105. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  106. package/src/runtime/shared/child-guardian.mjs +204 -108
  107. package/src/runtime/shared/child-spawn-gate.mjs +60 -31
  108. package/src/runtime/shared/config.mjs +130 -44
  109. package/src/runtime/shared/resource-admission.mjs +146 -97
  110. package/src/runtime/shared/stream-progress.mjs +6 -0
  111. package/src/runtime/shared/tool-surface.mjs +0 -5
  112. package/src/runtime/shared/turn-snapshot.mjs +31 -5
  113. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  114. package/src/session-runtime/config-helpers.mjs +39 -37
  115. package/src/session-runtime/model-route-api.mjs +7 -5
  116. package/src/session-runtime/provider-auth-api.mjs +0 -7
  117. package/src/session-runtime/runtime-core.mjs +11 -11
  118. package/src/session-runtime/runtime-tunables.mjs +4 -4
  119. package/src/session-runtime/session-lifecycle.mjs +9 -2
  120. package/src/session-runtime/session-turn-api.mjs +41 -9
  121. package/src/session-runtime/settings-api.mjs +5 -17
  122. package/src/session-runtime/tool-defs.mjs +3 -3
  123. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  124. package/src/session-runtime/workflow.mjs +14 -24
  125. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  126. package/src/standalone/agent-tool/helpers.mjs +2 -2
  127. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  128. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  129. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  130. package/src/standalone/backend-daemon.mjs +97 -36
  131. package/src/standalone/channel-admin.mjs +12 -12
  132. package/src/standalone/channel-daemon-client.mjs +17 -1
  133. package/src/standalone/channel-daemon-transport.mjs +90 -8
  134. package/src/standalone/engine-daemon-client.mjs +380 -205
  135. package/src/standalone/engine-daemon-protocol.mjs +3 -4
  136. package/src/standalone/engine-daemon-service.mjs +378 -134
  137. package/src/standalone/engine-daemon-transport.mjs +268 -77
  138. package/src/standalone/explore-tool.mjs +187 -70
  139. package/src/standalone/fair-call-scheduler.mjs +264 -0
  140. package/src/standalone/session-protocol.mjs +188 -0
  141. package/src/tui/App.jsx +30 -17
  142. package/src/tui/app/app-view.jsx +5 -2
  143. package/src/tui/app/core-memory-picker.mjs +19 -19
  144. package/src/tui/app/doctor.mjs +5 -11
  145. package/src/tui/app/message-selector.mjs +103 -0
  146. package/src/tui/app/onboarding-steps.mjs +1 -2
  147. package/src/tui/app/project-picker.mjs +80 -56
  148. package/src/tui/app/prompt-submit.mjs +46 -41
  149. package/src/tui/app/resume-picker.mjs +2 -2
  150. package/src/tui/app/route-pickers.mjs +3 -1
  151. package/src/tui/app/settings-picker.mjs +6 -8
  152. package/src/tui/app/transcript-window.mjs +38 -0
  153. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  154. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  155. package/src/tui/app/use-prompt-hint.mjs +3 -2
  156. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  157. package/src/tui/app/use-transcript-scroll.mjs +9 -2
  158. package/src/tui/app/use-transcript-window.mjs +50 -17
  159. package/src/tui/components/PromptInput.jsx +71 -38
  160. package/src/tui/components/Spinner.jsx +1 -1
  161. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  162. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  163. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  164. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  165. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  166. package/src/tui/dist/index.mjs +920 -1592
  167. package/src/tui/engine/live-share.mjs +34 -0
  168. package/src/tui/engine/session-api-ext.mjs +1 -4
  169. package/src/tui/engine/session-api.mjs +103 -33
  170. package/src/tui/engine/session-flow.mjs +7 -1
  171. package/src/tui/engine/tool-card-results.mjs +4 -2
  172. package/src/tui/engine/tool-result-text.mjs +10 -0
  173. package/src/tui/engine/turn.mjs +9 -69
  174. package/src/tui/engine-local-session.mjs +47 -31
  175. package/src/tui/index.jsx +1 -2
  176. package/src/tui/spinner-meta.mjs +4 -4
  177. package/src/tui/spinner-verbs.mjs +51 -3
  178. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
@@ -0,0 +1,264 @@
1
+ function positiveInt(value, fallback) {
2
+ const parsed = Math.floor(Number(value));
3
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
4
+ }
5
+
6
+ function activeLimit(value, fallback) {
7
+ if (value === null || value === Infinity) return Infinity;
8
+ return positiveInt(value, fallback);
9
+ }
10
+
11
+ function schedulerError(message, statusCode = 503) {
12
+ const error = new Error(message);
13
+ error.statusCode = statusCode;
14
+ return error;
15
+ }
16
+
17
+ function abortError(signal, fallback = 'scheduled call canceled') {
18
+ return signal?.reason instanceof Error
19
+ ? signal.reason
20
+ : schedulerError(String(signal?.reason || fallback), 499);
21
+ }
22
+
23
+ /**
24
+ * Owner-fair async dispatcher with an optional active limit.
25
+ *
26
+ * One owner may borrow the entire queue while it is alone. When another owner
27
+ * appears, new admissions are divided by weight and a completely full queue
28
+ * evicts one tail item from the largest borrower so the newcomer always gets a
29
+ * seat. Running work is never revoked.
30
+ */
31
+ export function createFairCallScheduler({
32
+ name = 'daemon call',
33
+ activeMax = 32,
34
+ queueMax = 256,
35
+ minOwnerQueue = 8,
36
+ dispatchBurst = 8,
37
+ yieldUnbounded = false,
38
+ schedule = setImmediate,
39
+ now = Date.now,
40
+ } = {}) {
41
+ const maxActive = activeLimit(activeMax, 32);
42
+ const maxQueued = positiveInt(queueMax, 256);
43
+ const ownerFloor = Math.max(1, Math.min(maxQueued, positiveInt(minOwnerQueue, 8)));
44
+ const maxBurst = positiveInt(dispatchBurst, 8);
45
+ const groups = new Map();
46
+ let active = 0;
47
+ let queued = 0;
48
+ let scheduled = false;
49
+ let closed = false;
50
+
51
+ function ownerId(value) {
52
+ const clean = String(value || '').trim();
53
+ return clean ? clean.slice(0, 240) : 'anonymous';
54
+ }
55
+
56
+ function groupFor(owner, weight = 1) {
57
+ const id = ownerId(owner);
58
+ let group = groups.get(id);
59
+ if (!group) {
60
+ group = {
61
+ owner: id,
62
+ weight: positiveInt(weight, 1),
63
+ current: 0,
64
+ active: 0,
65
+ queue: [],
66
+ };
67
+ groups.set(id, group);
68
+ } else if (weight !== undefined) {
69
+ group.weight = positiveInt(weight, group.weight);
70
+ }
71
+ return group;
72
+ }
73
+
74
+ function maybeDeleteGroup(group) {
75
+ if (group && group.active === 0 && group.queue.length === 0) groups.delete(group.owner);
76
+ }
77
+
78
+ function detach(item) {
79
+ if (!item?.onAbort || !item.signal) return;
80
+ try { item.signal.removeEventListener('abort', item.onAbort); } catch {}
81
+ item.onAbort = null;
82
+ }
83
+
84
+ function queuedGroups(extra = null) {
85
+ const out = [];
86
+ for (const group of groups.values()) {
87
+ if (group.queue.length > 0 || group === extra) out.push(group);
88
+ }
89
+ if (extra && !out.includes(extra)) out.push(extra);
90
+ return out;
91
+ }
92
+
93
+ function rejectBorrowedTail(incoming) {
94
+ let borrower = null;
95
+ for (const group of groups.values()) {
96
+ if (group === incoming || group.queue.length <= ownerFloor) continue;
97
+ if (!borrower || group.queue.length > borrower.queue.length) borrower = group;
98
+ }
99
+ if (!borrower) return false;
100
+ const displaced = borrower.queue.pop();
101
+ queued = Math.max(0, queued - 1);
102
+ detach(displaced);
103
+ displaced.reject(schedulerError(
104
+ `${name} queue rebalanced for another client; retry after running work completes`,
105
+ 503,
106
+ ));
107
+ maybeDeleteGroup(borrower);
108
+ return true;
109
+ }
110
+
111
+ function fairQueueLimit(group) {
112
+ const contenders = queuedGroups(group);
113
+ if (contenders.length <= 1) return maxQueued;
114
+ const totalWeight = contenders.reduce((sum, candidate) => sum + candidate.weight, 0);
115
+ return Math.max(
116
+ ownerFloor,
117
+ Math.floor(maxQueued * group.weight / Math.max(1, totalWeight)),
118
+ );
119
+ }
120
+
121
+ function pickGroup() {
122
+ const ready = [...groups.values()].filter((group) => group.queue.length > 0);
123
+ if (ready.length === 0) return null;
124
+ const totalWeight = ready.reduce((sum, group) => sum + group.weight, 0);
125
+ let chosen = null;
126
+ for (const group of ready) {
127
+ group.current += group.weight;
128
+ if (!chosen || group.current > chosen.current) chosen = group;
129
+ }
130
+ chosen.current -= totalWeight;
131
+ return chosen;
132
+ }
133
+
134
+ function scheduleDispatch() {
135
+ if (closed || scheduled || active >= maxActive || queued === 0) return;
136
+ scheduled = true;
137
+ schedule(() => {
138
+ scheduled = false;
139
+ let started = 0;
140
+ while (!closed && active < maxActive && queued > 0 && started < maxBurst) {
141
+ const group = pickGroup();
142
+ const item = group?.queue.shift();
143
+ if (!group || !item) break;
144
+ queued = Math.max(0, queued - 1);
145
+ detach(item);
146
+ active += 1;
147
+ group.active += 1;
148
+ started += 1;
149
+ Promise.resolve()
150
+ .then(item.run)
151
+ .then(item.resolve, item.reject)
152
+ .finally(() => {
153
+ active = Math.max(0, active - 1);
154
+ group.active = Math.max(0, group.active - 1);
155
+ maybeDeleteGroup(group);
156
+ scheduleDispatch();
157
+ });
158
+ }
159
+ // A bounded burst amortizes loopback dispatch without letting synchronous
160
+ // setup monopolize the supervisor's socket/control-plane turn.
161
+ scheduleDispatch();
162
+ });
163
+ }
164
+
165
+ function enqueue(owner, run, { weight = 1, signal = null } = {}) {
166
+ if (closed) return Promise.reject(schedulerError(`${name} scheduler is closed`));
167
+ if (typeof run !== 'function') return Promise.reject(new TypeError('scheduled call must be a function'));
168
+ if (signal?.aborted) return Promise.reject(abortError(signal));
169
+ const group = groupFor(owner, weight);
170
+ // Default daemon lanes are unbounded. Do not enqueue them only to bounce
171
+ // through setImmediate: independent loopback calls start in this microtask,
172
+ // while finite operator-configured lanes retain the fair queue below.
173
+ if (maxActive === Infinity && !yieldUnbounded && queued === 0) {
174
+ active += 1;
175
+ group.active += 1;
176
+ return Promise.resolve()
177
+ .then(run)
178
+ .finally(() => {
179
+ active = Math.max(0, active - 1);
180
+ group.active = Math.max(0, group.active - 1);
181
+ maybeDeleteGroup(group);
182
+ scheduleDispatch();
183
+ });
184
+ }
185
+ const hasCompetitor = queuedGroups(group).some((candidate) =>
186
+ candidate !== group && candidate.queue.length > 0);
187
+ if (hasCompetitor && group.queue.length >= fairQueueLimit(group)) {
188
+ return Promise.reject(schedulerError(
189
+ `${name} client queue is full; retry after this client's running work completes`,
190
+ 429,
191
+ ));
192
+ }
193
+ if (queued >= maxQueued && !rejectBorrowedTail(group)) {
194
+ return Promise.reject(schedulerError(`${name} queue is full`, 503));
195
+ }
196
+ let item;
197
+ const promise = new Promise((resolve, reject) => {
198
+ item = {
199
+ run,
200
+ resolve,
201
+ reject,
202
+ signal,
203
+ onAbort: null,
204
+ queuedAt: now(),
205
+ };
206
+ if (signal) {
207
+ item.onAbort = () => {
208
+ const index = group.queue.indexOf(item);
209
+ if (index < 0) return;
210
+ group.queue.splice(index, 1);
211
+ queued = Math.max(0, queued - 1);
212
+ detach(item);
213
+ maybeDeleteGroup(group);
214
+ reject(abortError(signal));
215
+ };
216
+ signal.addEventListener('abort', item.onAbort, { once: true });
217
+ }
218
+ group.queue.push(item);
219
+ queued += 1;
220
+ });
221
+ scheduleDispatch();
222
+ return promise;
223
+ }
224
+
225
+ function close(reason = `${name} scheduler is closed`) {
226
+ if (closed) return;
227
+ closed = true;
228
+ const error = schedulerError(reason);
229
+ for (const group of groups.values()) {
230
+ for (const item of group.queue.splice(0)) {
231
+ detach(item);
232
+ item.reject(error);
233
+ }
234
+ }
235
+ queued = 0;
236
+ for (const group of [...groups.values()]) maybeDeleteGroup(group);
237
+ }
238
+
239
+ function snapshot() {
240
+ let oldestQueuedAt = Infinity;
241
+ for (const group of groups.values()) {
242
+ for (const item of group.queue) {
243
+ oldestQueuedAt = Math.min(oldestQueuedAt, Number(item.queuedAt) || Infinity);
244
+ }
245
+ }
246
+ return {
247
+ active,
248
+ queued,
249
+ owners: [...groups.values()].filter((group) => group.active || group.queue.length).length,
250
+ activeMax: Number.isFinite(maxActive) ? maxActive : null,
251
+ dispatchBurst: maxBurst,
252
+ queueMax: maxQueued,
253
+ oldestWaitMs: Number.isFinite(oldestQueuedAt) ? Math.max(0, now() - oldestQueuedAt) : 0,
254
+ };
255
+ }
256
+
257
+ return {
258
+ enqueue,
259
+ close,
260
+ snapshot,
261
+ get active() { return active; },
262
+ get queued() { return queued; },
263
+ };
264
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Explicit session protocol shared by every Mixdog client.
3
+ *
4
+ * The wire exposes session lifetime operations directly. Engine capabilities
5
+ * are finite and split into read/configure lanes; clients cannot discover or
6
+ * invoke arbitrary properties on the daemon-hosted runtime.
7
+ */
8
+
9
+ export const SESSION_READ_ACTIONS = Object.freeze([
10
+ 'contextStatus',
11
+ 'getAgentDefinition',
12
+ 'getAutoClear',
13
+ 'getChannelSettings',
14
+ 'getChannelSetup',
15
+ 'getChannelWorkerStatus',
16
+ 'getCompactionSettings',
17
+ 'getDisabledSkills',
18
+ 'getMediaJob',
19
+ 'getOnboardingStatus',
20
+ 'getOutputStyle',
21
+ 'getProfile',
22
+ 'getProviderSetup',
23
+ 'getRecapSettings',
24
+ 'getSearchRoute',
25
+ 'getSettingsSnapshot',
26
+ 'getSystemShell',
27
+ 'getTheme',
28
+ 'getTurnReviewDiff',
29
+ 'getUpdateSettings',
30
+ 'getUpdateStatus',
31
+ 'getUsageDashboard',
32
+ 'getVoiceStatus',
33
+ 'getWorkflowPack',
34
+ 'hooksStatus',
35
+ 'isRemoteEnabled',
36
+ 'listAgents',
37
+ 'listMediaAssets',
38
+ 'listMediaLanes',
39
+ 'listOutputStyles',
40
+ 'listPresets',
41
+ 'listProviderModels',
42
+ 'listProviders',
43
+ 'listSearchModels',
44
+ 'listThemes',
45
+ 'listWorkflows',
46
+ 'mcpStatus',
47
+ 'peekSessionTranscript',
48
+ 'pluginsStatus',
49
+ 'readMediaAsset',
50
+ 'resolveMediaFile',
51
+ 'skillContent',
52
+ 'skillsStatus',
53
+ 'toolsStatus',
54
+ ]);
55
+
56
+ export const SESSION_CONFIGURE_ACTIONS = Object.freeze([
57
+ 'addHookRule',
58
+ 'addMcpServer',
59
+ 'addPlugin',
60
+ 'addSkill',
61
+ 'agentControl',
62
+ 'authenticateProvider',
63
+ 'beginOAuthProviderLogin',
64
+ 'cacheMediaThumbnail',
65
+ 'cancelMediaJob',
66
+ 'checkForUpdate',
67
+ 'claimRemote',
68
+ 'clear',
69
+ 'compact',
70
+ 'completeOnboarding',
71
+ 'createWorkflow',
72
+ 'deleteAgentDefinition',
73
+ 'deleteHookRule',
74
+ 'deleteMediaAsset',
75
+ 'deleteSchedule',
76
+ 'deleteSession',
77
+ 'deleteWebhook',
78
+ 'deleteWorkflow',
79
+ 'enablePluginMcp',
80
+ 'forgetDiscordToken',
81
+ 'forgetProviderAuth',
82
+ 'forgetTelegramToken',
83
+ 'loginOAuthProvider',
84
+ 'loginOpenCodeGoUsage',
85
+ 'memoryControl',
86
+ 'openMediaAsset',
87
+ 'openMediaFolder',
88
+ 'pushNotice',
89
+ 'recall',
90
+ 'reconnectMcp',
91
+ 'releaseRemote',
92
+ 'reloadPlugins',
93
+ 'reloadSkills',
94
+ 'rememberPromptHistory',
95
+ 'removeMcpServer',
96
+ 'removeNotice',
97
+ 'removePlugin',
98
+ 'renameSessionTitle',
99
+ 'restoreNewerTranscript',
100
+ 'restoreOlderTranscript',
101
+ 'restoreQueued',
102
+ 'revertTurnReviewFile',
103
+ 'rewindToItem',
104
+ 'runDoctor',
105
+ 'runScheduleNow',
106
+ 'runUpdateNow',
107
+ 'saveAgentDefinition',
108
+ 'saveDiscordToken',
109
+ 'saveOpenAIUsageSessionKey',
110
+ 'saveOpenCodeGoUsageAuth',
111
+ 'saveProviderApiKey',
112
+ 'saveSchedule',
113
+ 'saveTelegramToken',
114
+ 'saveWebhook',
115
+ 'saveWorkflowPack',
116
+ 'selectTools',
117
+ 'setAgentRoute',
118
+ 'setAutoClear',
119
+ 'setAutoUpdate',
120
+ 'setBackend',
121
+ 'setChannel',
122
+ 'setChannelsEnabled',
123
+ 'setCompactionSettings',
124
+ 'setCwd',
125
+ 'setDisabledSkills',
126
+ 'setEffort',
127
+ 'setFast',
128
+ 'setHookRuleEnabled',
129
+ 'setLocalProvider',
130
+ 'setMcpServerEnabled',
131
+ 'setModel',
132
+ 'setOutputStyle',
133
+ 'setProfile',
134
+ 'setProgressHint',
135
+ 'setRecapEnabled',
136
+ 'setRoute',
137
+ 'setScheduleEnabled',
138
+ 'setSearchRoute',
139
+ 'setSystemShell',
140
+ 'setTheme',
141
+ 'setToolMode',
142
+ 'setWebhookConfig',
143
+ 'setWebhookEnabled',
144
+ 'setWorkflow',
145
+ 'skipOnboarding',
146
+ 'startMediaJob',
147
+ 'switchContext',
148
+ 'toggleFast',
149
+ 'toggleRemote',
150
+ 'toggleVoice',
151
+ 'transcribeAudio',
152
+ 'updatePlugin',
153
+ ]);
154
+
155
+ export const SESSION_READ_ACTION_SET = new Set(SESSION_READ_ACTIONS);
156
+ export const SESSION_CONFIGURE_ACTION_SET = new Set(SESSION_CONFIGURE_ACTIONS);
157
+
158
+ function requireTransport(transport) {
159
+ if (!transport || typeof transport.call !== 'function') {
160
+ throw new TypeError('session transport is invalid');
161
+ }
162
+ return transport;
163
+ }
164
+
165
+ /** Build the one finite client used by TUI, desktop, and in-daemon adapters. */
166
+ export function createSessionProtocolClient(rawTransport) {
167
+ const transport = requireTransport(rawTransport);
168
+ const route = (name, params = {}, options = {}) => transport.call(name, params, options);
169
+ return Object.freeze({
170
+ list: (params = {}, options = {}) => route('session.list', params, options),
171
+ create: (params = {}, options = {}) => route('session.create', params, options),
172
+ read: (params = {}, options = {}) => route('session.read', params, options),
173
+ subscribe: (params = {}, options = {}) => route('session.subscribe', params, options),
174
+ unsubscribe: (params = {}, options = {}) => route('session.unsubscribe', params, options),
175
+ submit: (params = {}, options = {}) => route('session.submit', params, options),
176
+ abort: (params = {}, options = {}) => route('session.abort', params, options),
177
+ approve: (params = {}, options = {}) => route('session.approve', params, options),
178
+ configure: (params = {}, options = {}) => route('session.configure', params, options),
179
+ projectList: (params = {}, options = {}) => route('project.list', params, options),
180
+ projectInspect: (params = {}, options = {}) => route('project.inspect', params, options),
181
+ projectAdd: (params = {}, options = {}) => route('project.add', params, options),
182
+ projectTouch: (params = {}, options = {}) => route('project.touch', params, options),
183
+ projectRename: (params = {}, options = {}) => route('project.rename', params, options),
184
+ projectRemove: (params = {}, options = {}) => route('project.remove', params, options),
185
+ projectEnsureDirectory: (params = {}, options = {}) => route('project.ensureDirectory', params, options),
186
+ close: (reason) => Promise.resolve(transport.close?.(reason)),
187
+ });
188
+ }
package/src/tui/App.jsx CHANGED
@@ -42,16 +42,6 @@ import {
42
42
  splitPastedImagePathCandidates,
43
43
  } from './paste-attachments.mjs';
44
44
  import { formatDuration } from './time-format.mjs';
45
- import {
46
- listProjects,
47
- addProject,
48
- touchProjectSelected,
49
- renameProject,
50
- isDirectory,
51
- pathExists,
52
- ensureDir,
53
- resolveProjectPath,
54
- } from '../standalone/projects.mjs';
55
45
  import { pickFolder } from '../standalone/folder-dialog.mjs';
56
46
  import {
57
47
  formatHookDenialDetail,
@@ -100,6 +90,7 @@ import { usePromptDraftFlow } from './app/use-prompt-draft-flow.mjs';
100
90
  import { useTranscriptActivity } from './app/use-transcript-activity.mjs';
101
91
  import { createAppPickers } from './app/create-app-pickers.mjs';
102
92
  import { usePromptQueueHistory } from './app/use-prompt-queue-history.mjs';
93
+ import { useMessageSelector } from './app/message-selector.mjs';
103
94
  import { useTerminalChrome } from './app/use-terminal-chrome.mjs';
104
95
  import { useTranscriptWindow } from './app/use-transcript-window.mjs';
105
96
  import {
@@ -120,6 +111,7 @@ import {
120
111
  transcriptMeasuredRowsCache,
121
112
  buildTranscriptRowIndex,
122
113
  transcriptRenderWindow,
114
+ transcriptSwapReturnsToTail,
123
115
  } from './app/transcript-window.mjs';
124
116
  import {
125
117
  SEARCH_DEFAULT_ROUTE,
@@ -326,6 +318,9 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
326
318
  // same glide back to the bottom.
327
319
  const followingRef = useRef(false);
328
320
  const lastItemsCountRef = useRef(0);
321
+ // Head item of the last committed transcript: a bulk swap (session load /
322
+ // clear / compaction trim) changes it, a live append never does.
323
+ const lastFirstItemIdRef = useRef(null);
329
324
  // picker = null | { type, title, items, onSelect }
330
325
  // Rendered as an option panel attached directly above the bottom prompt.
331
326
  const pickerOpenedFromEnterRef = useRef(false);
@@ -413,10 +408,6 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
413
408
  setSettingsPrompt,
414
409
  setContextPanel,
415
410
  closeUsagePanel,
416
- listProjects,
417
- addProject,
418
- touchProjectSelected,
419
- resolveProjectPath,
420
411
  projectNameFromPath,
421
412
  pickFolder,
422
413
  });
@@ -429,9 +420,13 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
429
420
  initialPickerBuiltRef.current = true;
430
421
  const onboardingOwnsScreen = !resolveOnboardingCompleted(store, onboardingCompleted) || forceOnboarding;
431
422
  if (!onboardingOwnsScreen && state.items.length === 0) {
432
- setPicker(projectPicker.buildProjectPickerState({ initialEntry: true }));
423
+ setPicker(projectPicker.buildProjectPickerState({ initialEntry: true, loading: true }));
433
424
  }
434
425
  }
426
+ useEffect(() => {
427
+ if (livePickerRef.current?._projectInitialPending !== true) return;
428
+ void projectPickerRef.current?.openProjectPicker({ initialEntry: true });
429
+ }, [store]);
435
430
  const {
436
431
  beginNewProject,
437
432
  registerProject,
@@ -793,12 +788,17 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
793
788
  const count = state.items.length;
794
789
  const previousCount = lastItemsCountRef.current;
795
790
  lastItemsCountRef.current = count;
796
- if (count < previousCount || previousCount === 0) {
791
+ const firstId = count > 0 ? (state.items[0]?.id ?? null) : null;
792
+ const previousFirstId = lastFirstItemIdRef.current;
793
+ lastFirstItemIdRef.current = firstId;
794
+ // A bulk swap (compaction included) is an explicit return to the tail: the
795
+ // rows the reader was anchored to no longer exist.
796
+ if (transcriptSwapReturnsToTail({ count, previousCount, firstId, previousFirstId })) {
797
797
  resetTranscriptScroll();
798
798
  return;
799
799
  }
800
800
  if (count === previousCount || dragRef.current.active) return;
801
- }, [state.items.length, resetTranscriptScroll]);
801
+ }, [state.items, resetTranscriptScroll]);
802
802
 
803
803
  // Exit + queued-restore + prompt history: app/use-prompt-queue-history.mjs.
804
804
  const {
@@ -823,6 +823,17 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
823
823
  promptHistoryNavRef,
824
824
  });
825
825
 
826
+ // Double-Esc message selector (Claude Code parity): app/message-selector.mjs.
827
+ const { hasUserMessages, openMessageSelector } = useMessageSelector({
828
+ store,
829
+ state,
830
+ setPicker,
831
+ setPromptDraftOverride,
832
+ syncPromptLayoutRows,
833
+ showPromptHint,
834
+ clearPromptHint,
835
+ });
836
+
826
837
  // PROMPT HANDLER cluster extracted to app/use-prompt-handlers.mjs.
827
838
  const {
828
839
  handlePromptPaste,
@@ -849,6 +860,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
849
860
  recentPromptHistory,
850
861
  resetPromptHistoryNav,
851
862
  restoreQueuedToPrompt,
863
+ openMessageSelector,
852
864
  usagePanel,
853
865
  closeUsagePanel,
854
866
  contextPanel,
@@ -1257,6 +1269,7 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false, on
1257
1269
  handlePromptHistoryNavigate,
1258
1270
  handlePromptInterrupt,
1259
1271
  handlePromptPaste,
1272
+ hasUserMessages,
1260
1273
  hookPrompt,
1261
1274
  initialStatusLine,
1262
1275
  onPromptDraftChange,
@@ -20,7 +20,7 @@ import { UsagePanel } from '../components/UsagePanel.jsx';
20
20
  import { TextEntryPanel } from '../components/TextEntryPanel.jsx';
21
21
  import { Item } from '../components/TranscriptItem.jsx';
22
22
  export function renderAppView(ctx) {
23
- const { PANEL_MAX_VISIBLE, acceptSlashPalette, activeSlashQuery, activeTools, agentRevision, cancelChannelPrompt, cancelHookPrompt, cancelProviderPrompt, cancelSettingsPrompt, cancelSlashPalette, channelPrompt, clearPromptHint, completeSlashPalette, contextPanel, cycleWorkflowFromPrompt, exiting, expandedOptionPanel, floatingPanelRows, frameColumns, gridSelectionActiveRef, guardHintWidth, handlePromptEscape, handlePromptHistoryNavigate, handlePromptInterrupt, handlePromptPaste, hookPrompt, initialStatusLine, inputBoxHidden, inputHint, inputHintTone, liveSpinner, onPromptDraftChange, onSubmit, overlayHintAttachItemIndex, overlayHintBandRows, overlayHintFallbackRow, overlayHintOnLastItem, panelCloseMaskRows, panelInkMaskEpoch, panelTransitionClearRows, panelTransitionEpoch, picker, pickerOpenedFromEnterRef, pickerOpenedFromEnterTimerRef, pickerVisibleRows, promptBoxRectRef, promptBoxRows, promptDraft, promptDraftOverride, promptMetaVisible, promptMouseSelectionRef, promptSelectionRef, promptSpinnerColumns, promptValueRef, providerPrompt, queuedCompact, queuedVisible, renderedTranscriptItems, resizeEpoch, resizeState, restoreQueuedToPrompt, setPicker, setSlashIndex, setTextEntryLayoutRows, settingsPrompt, showWelcomeBanner, slashCommands, slashIndex, slashPaletteOpen, state, statuslineStats, store, toolApproval, toolOutputExpanded, transcriptContentHeight, transcriptGuardRows, transcriptMeasureRef, transcriptTailPinned, transcriptWindow, transientStatusWidth, tuiReady, usagePanel, viewportHeight, welcomePromptHintRows, welcomePromptHintText } = ctx; /* DESTRUCTURE */
23
+ const { PANEL_MAX_VISIBLE, acceptSlashPalette, activeSlashQuery, activeTools, agentRevision, cancelChannelPrompt, cancelHookPrompt, cancelProviderPrompt, cancelSettingsPrompt, cancelSlashPalette, channelPrompt, clearPromptHint, completeSlashPalette, contextPanel, cycleWorkflowFromPrompt, exiting, expandedOptionPanel, floatingPanelRows, frameColumns, gridSelectionActiveRef, guardHintWidth, handlePromptEscape, handlePromptHistoryNavigate, handlePromptInterrupt, handlePromptPaste, hasUserMessages, hookPrompt, initialStatusLine, inputBoxHidden, inputHint, inputHintTone, liveSpinner, onPromptDraftChange, onSubmit, overlayHintAttachItemIndex, overlayHintBandRows, overlayHintFallbackRow, overlayHintOnLastItem, panelCloseMaskRows, panelInkMaskEpoch, panelTransitionClearRows, panelTransitionEpoch, picker, pickerOpenedFromEnterRef, pickerOpenedFromEnterTimerRef, pickerVisibleRows, promptBoxRectRef, promptBoxRows, promptDraft, promptDraftOverride, promptMetaVisible, promptMouseSelectionRef, promptSelectionRef, promptSpinnerColumns, promptValueRef, providerPrompt, queuedCompact, queuedVisible, renderedTranscriptItems, resizeEpoch, resizeState, restoreQueuedToPrompt, setPicker, setSlashIndex, setTextEntryLayoutRows, settingsPrompt, showWelcomeBanner, slashCommands, slashIndex, slashPaletteOpen, state, statuslineStats, store, toolApproval, toolOutputExpanded, transcriptContentHeight, transcriptGuardRows, transcriptMeasureRef, transcriptTailPinned, transcriptWindow, transientStatusWidth, tuiReady, usagePanel, viewportHeight, welcomePromptHintRows, welcomePromptHintText } = ctx; /* DESTRUCTURE */
24
24
  const promptInputControl = (
25
25
  <PromptInput
26
26
  onSubmit={onSubmit}
@@ -47,6 +47,7 @@ export function renderAppView(ctx) {
47
47
  // actually be accepted — otherwise Enter must submit the raw text as
48
48
  // before instead of dead-ending in the palette accept path.
49
49
  commandPaletteActive={slashPaletteOpen && slashCommands.length > 0}
50
+ commandPaletteOpen={slashPaletteOpen}
50
51
  onCommandPaletteNavigate={(direction) => {
51
52
  setSlashIndex((index) => {
52
53
  const total = slashCommands.length;
@@ -65,7 +66,9 @@ export function renderAppView(ctx) {
65
66
  onCommandPaletteAccept={acceptSlashPalette}
66
67
  onCommandPaletteCancel={cancelSlashPalette}
67
68
  onCommandPaletteComplete={completeSlashPalette}
68
- onRestoreQueued={(currentText) => restoreQueuedToPrompt({ restoreDraft: true, showHint: false, currentText })}
69
+ onRestoreQueued={restoreQueuedToPrompt}
70
+ hasQueuedMessages={Array.isArray(state.queued) && state.queued.length > 0}
71
+ hasMessages={hasUserMessages}
69
72
  />
70
73
  );
71
74
 
@@ -39,20 +39,20 @@ export function createCoreMemoryPicker({
39
39
  });
40
40
  void store.memoryControl?.({ action: 'core', op: 'list', project_id: '*' }, { silent: true })
41
41
  .then(async (result) => {
42
- // Memory on/off lives here too so /memory is a one-stop surface:
42
+ // The recap toggle lives here too so /memory is a one-stop surface:
43
43
  // toggle row + add row + curated entries.
44
- const memory = (await store.getMemorySettings?.()) || { enabled: true };
45
- const memoryOn = memory.enabled !== false;
44
+ const recap = (await store.getRecapSettings?.()) || { enabled: true };
45
+ const recapOn = recap.enabled !== false;
46
46
  const coreRows = parseMemoryCoreRows(result);
47
47
  const rows = [
48
48
  {
49
- value: 'memory-toggle',
50
- label: 'Memory',
51
- meta: memoryOn ? 'On' : 'Off',
52
- description: memoryOn
53
- ? 'Background memory cycles on'
54
- : 'Background memory cycles off',
55
- _action: 'toggle-memory',
49
+ value: 'recap-toggle',
50
+ label: 'Recap',
51
+ meta: recapOn ? 'On' : 'Off',
52
+ description: recapOn
53
+ ? 'Background session recaps on'
54
+ : 'Background recaps off; core memory remains available',
55
+ _action: 'toggle-recap',
56
56
  },
57
57
  { value: 'core-add', label: 'Add Memory', description: 'store a new curated memory sentence', _action: 'add-core' },
58
58
  {
@@ -76,17 +76,17 @@ export function createCoreMemoryPicker({
76
76
  labelWidth: 12,
77
77
  footer: (item) => (item && item._action === 'core-entry' ? (item._summary || item._element || '') : ''),
78
78
  onSelect: (_value, item) => {
79
- if (item?._action === 'toggle-memory') toggleMemoryEnabled();
79
+ if (item?._action === 'toggle-recap') toggleRecapEnabled();
80
80
  else if (item?._action === 'add-core') beginAddCoreMemory();
81
81
  else if (item?._action === 'core-list') openCoreMemoryListPicker(item._rows);
82
82
  else if (item?._line) store.pushNotice(item._line, 'info');
83
83
  },
84
84
  // Toggle rows also respond to ←/→ (matches settings/channels UX).
85
85
  onLeft: (item) => {
86
- if (item?._action === 'toggle-memory') toggleMemoryEnabled();
86
+ if (item?._action === 'toggle-recap') toggleRecapEnabled();
87
87
  },
88
88
  onRight: (item) => {
89
- if (item?._action === 'toggle-memory') toggleMemoryEnabled();
89
+ if (item?._action === 'toggle-recap') toggleRecapEnabled();
90
90
  },
91
91
  onCancel: closeMemoryCorePicker,
92
92
  });
@@ -137,14 +137,14 @@ export function createCoreMemoryPicker({
137
137
  });
138
138
  };
139
139
 
140
- const toggleMemoryEnabled = async () => {
141
- const memory = (await store.getMemorySettings?.()) || { enabled: true };
142
- void Promise.resolve(store.setMemoryEnabled?.(!(memory.enabled !== false)))
140
+ const toggleRecapEnabled = async () => {
141
+ const recap = (await store.getRecapSettings?.()) || { enabled: true };
142
+ void Promise.resolve(store.setRecapEnabled?.(!(recap.enabled !== false)))
143
143
  .then((next) => {
144
- if (!next) store.pushNotice('memory setting is busy', 'warn');
145
- else store.pushNotice(`Memory ${next.enabled ? 'on' : 'off'}`, 'info');
144
+ if (!next) store.pushNotice('recap setting is busy', 'warn');
145
+ else store.pushNotice(`Recap ${next.enabled ? 'on' : 'off'}`, 'info');
146
146
  })
147
- .catch((e) => store.pushNotice(`memory toggle failed: ${e?.message || e}`, 'error'))
147
+ .catch((e) => store.pushNotice(`recap toggle failed: ${e?.message || e}`, 'error'))
148
148
  .finally(() => openMemoryCorePicker());
149
149
  };
150
150