mixdog 0.9.83 → 0.9.85

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 (30) hide show
  1. package/package.json +1 -1
  2. package/scripts/channel-daemon-stub.mjs +2 -1
  3. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +30 -3
  4. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +38 -4
  5. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +48 -32
  6. package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +25 -14
  7. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +8 -2
  8. package/src/runtime/agent/orchestrator/session/store/listing.mjs +38 -1
  9. package/src/runtime/agent/orchestrator/session/store.mjs +1 -0
  10. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +44 -73
  11. package/src/runtime/channels/lib/owned-runtime.mjs +6 -8
  12. package/src/runtime/channels/lib/parent-bridge.mjs +27 -15
  13. package/src/runtime/channels/lib/worker-ipc.mjs +17 -4
  14. package/src/runtime/memory/index.mjs +14 -5
  15. package/src/runtime/memory/lib/agent-ipc.mjs +11 -12
  16. package/src/runtime/shared/atomic-file.mjs +51 -24
  17. package/src/runtime/shared/err-text.mjs +1 -0
  18. package/src/runtime/shared/safe-ipc-send.mjs +34 -0
  19. package/src/session-runtime/lifecycle-api.mjs +9 -3
  20. package/src/session-runtime/runtime-core.mjs +5 -1
  21. package/src/standalone/channel-daemon.mjs +2 -1
  22. package/src/standalone/channel-worker.mjs +21 -19
  23. package/src/standalone/memory-runtime-proxy.mjs +98 -23
  24. package/src/tui/app/slash-dispatch.mjs +23 -13
  25. package/src/tui/app/theme-effort-pickers.mjs +3 -1
  26. package/src/tui/dist/index.mjs +115 -57
  27. package/src/tui/engine/live-share.mjs +2 -2
  28. package/src/tui/engine/session-flow.mjs +20 -10
  29. package/src/tui/engine/turn.mjs +28 -16
  30. package/src/tui/engine.mjs +17 -1
@@ -103,10 +103,52 @@ function isMemoryReadOnlyToolCall(name, args = {}) {
103
103
  return false;
104
104
  }
105
105
 
106
- function requestJson({ port, method = 'GET', path = '/', body = null, timeoutMs = 10_000, headers = {} }) {
107
- return new Promise((resolvePromise, reject) => {
106
+ function memoryAbortError(reason) {
107
+ if (reason instanceof Error) return reason;
108
+ const error = new Error(typeof reason === 'string' && reason ? reason : 'memory proxy request aborted');
109
+ error.name = 'AbortError';
110
+ error.code = 'ABORT_ERR';
111
+ return error;
112
+ }
113
+
114
+ export function requestJson({
115
+ port,
116
+ method = 'GET',
117
+ path = '/',
118
+ body = null,
119
+ timeoutMs = 10_000,
120
+ headers = {},
121
+ signal = null,
122
+ }) {
123
+ return new Promise((resolvePromise, rejectPromise) => {
124
+ let settled = false;
125
+ let req = null;
126
+ const cleanup = () => {
127
+ try { signal?.removeEventListener?.('abort', onAbort); } catch {}
128
+ };
129
+ const resolve = (value) => {
130
+ if (settled) return;
131
+ settled = true;
132
+ cleanup();
133
+ resolvePromise(value);
134
+ };
135
+ const reject = (error) => {
136
+ if (settled) return;
137
+ settled = true;
138
+ cleanup();
139
+ rejectPromise(error);
140
+ };
141
+ const onAbort = () => {
142
+ const error = memoryAbortError(signal?.reason);
143
+ if (req) req.destroy(error);
144
+ else reject(error);
145
+ };
146
+ if (signal?.aborted) {
147
+ reject(memoryAbortError(signal.reason));
148
+ return;
149
+ }
108
150
  const payload = body == null ? null : JSON.stringify(body);
109
- const req = http.request({
151
+ req = http.request({
110
152
  hostname: '127.0.0.1',
111
153
  port,
112
154
  path,
@@ -135,14 +177,19 @@ function requestJson({ port, method = 'GET', path = '/', body = null, timeoutMs
135
177
  reject(error);
136
178
  return;
137
179
  }
138
- resolvePromise(parsed ?? { raw: data });
180
+ resolve(parsed ?? { raw: data });
139
181
  });
182
+ res.on('error', reject);
140
183
  });
141
184
  req.on('error', reject);
142
185
  req.on('timeout', () => {
143
- req.destroy();
144
- reject(new Error(`memory proxy request timed out: ${method} ${path}`));
186
+ req.destroy(new Error(`memory proxy request timed out: ${method} ${path}`));
145
187
  });
188
+ try { signal?.addEventListener?.('abort', onAbort, { once: true }); } catch {}
189
+ if (signal?.aborted) {
190
+ onAbort();
191
+ return;
192
+ }
146
193
  if (payload) req.write(payload);
147
194
  req.end();
148
195
  });
@@ -496,26 +543,54 @@ export function createStandaloneMemoryRuntime({
496
543
  return { running: true, port, mode: 'http-proxy' };
497
544
  }
498
545
 
499
- async function handleToolCall(name, args = {}) {
546
+ async function handleToolCall(name, args = {}, signalOrOptions = null) {
547
+ const signal = signalOrOptions?.signal || signalOrOptions || null;
500
548
  const readOnlyRpc = isMemoryReadOnlyToolCall(name, args);
501
549
  const callId = `mem_${process.pid}_${nextCallId++}`;
502
- return await withTransientMemoryRpcRetry(async () => {
503
- await start();
504
- let port = portCache || await findLivePort({ allowStarting: true });
505
- if (!port) throw new Error('memory runtime is not available');
506
- // ensureClientRegistered may respawn onto a fresh daemon/port; target the
507
- // port it hands back so the RPC and registration always hit the same one.
508
- port = await ensureClientRegistered(port);
509
- if (!port) throw new Error('memory runtime is not available');
510
- return await requestJson({
511
- port,
550
+ let activePort = null;
551
+ let rpcStarted = false;
552
+ const throwIfAborted = () => {
553
+ if (signal?.aborted) throw memoryAbortError(signal.reason);
554
+ };
555
+ const cancelRemote = () => {
556
+ if (!activePort || !rpcStarted) return;
557
+ void requestJson({
558
+ port: activePort,
512
559
  method: 'POST',
513
- path: '/api/tool',
514
- body: { name, arguments: args || {} },
515
- timeoutMs: Math.max(1000, Number(process.env.MIXDOG_MEMORY_TOOL_TIMEOUT_MS) || 180_000),
516
- headers: { 'X-Mixdog-Call-Id': callId },
517
- });
518
- }, { readOnlyRpc });
560
+ path: '/api/cancel',
561
+ body: { callId },
562
+ timeoutMs: 1500,
563
+ }).catch(() => {});
564
+ };
565
+ throwIfAborted();
566
+ try { signal?.addEventListener?.('abort', cancelRemote, { once: true }); } catch {}
567
+ try {
568
+ return await withTransientMemoryRpcRetry(async () => {
569
+ throwIfAborted();
570
+ await start();
571
+ throwIfAborted();
572
+ let port = portCache || await findLivePort({ allowStarting: true });
573
+ if (!port) throw new Error('memory runtime is not available');
574
+ // ensureClientRegistered may respawn onto a fresh daemon/port; target the
575
+ // port it hands back so the RPC and registration always hit the same one.
576
+ port = await ensureClientRegistered(port);
577
+ if (!port) throw new Error('memory runtime is not available');
578
+ activePort = port;
579
+ throwIfAborted();
580
+ rpcStarted = true;
581
+ return await requestJson({
582
+ port,
583
+ method: 'POST',
584
+ path: '/api/tool',
585
+ body: { name, arguments: args || {} },
586
+ timeoutMs: Math.max(1000, Number(process.env.MIXDOG_MEMORY_TOOL_TIMEOUT_MS) || 180_000),
587
+ headers: { 'X-Mixdog-Call-Id': callId },
588
+ signal,
589
+ });
590
+ }, { readOnlyRpc });
591
+ } finally {
592
+ try { signal?.removeEventListener?.('abort', cancelRemote); } catch {}
593
+ }
519
594
  }
520
595
 
521
596
  async function buildSessionCoreMemoryPayload(sessionCwd) {
@@ -9,6 +9,15 @@
9
9
  * defined later in the App factory zone are passed as lazy getters so their
10
10
  * live binding is used at call time.
11
11
  */
12
+ import { presentErrorText } from '../../runtime/shared/err-text.mjs';
13
+
14
+ export function compactFailureNotice(error) {
15
+ if (error == null || String(error).trim() === '') return 'Compact failed.';
16
+ const reason = presentErrorText(error, { surface: 'compact', max: 320 });
17
+ if (!reason || reason === 'Unknown error') return 'Compact failed.';
18
+ return /^compact(?:ion)? failed\b/i.test(reason) ? reason : `Compact failed: ${reason}`;
19
+ }
20
+
12
21
  export function createSlashDispatch({
13
22
  state,
14
23
  store,
@@ -179,24 +188,21 @@ export function createSlashDispatch({
179
188
  }
180
189
  return true;
181
190
  }
182
- case 'effort':
183
- if (state.busy) {
184
- store.pushNotice('wait for the current turn to finish before /effort', 'warn');
185
- return false;
186
- }
191
+ case 'effort': {
192
+ // A running turn no longer blocks the switch: the in-flight turn keeps
193
+ // the effort it started with, and the new level applies from the next
194
+ // turn. Same for /fast below.
195
+ const pendingTurn = state.busy ? ' (applies from the next turn)' : '';
187
196
  if (!arg) {
188
197
  openEffortPicker();
189
198
  return true;
190
199
  }
191
200
  void store.setEffort(arg)
192
- .then(result => store.pushNotice(result ? `Effort set to ${result}` : 'Effort switch is already running.', result ? 'info' : 'warn'))
201
+ .then(result => store.pushNotice(result ? `Effort set to ${result}${pendingTurn}` : 'Effort switch is already running.', result ? 'info' : 'warn'))
193
202
  .catch((e) => store.pushNotice(`Couldn’t switch effort: ${e?.message || e}`, 'error'));
194
203
  return true;
204
+ }
195
205
  case 'fast': {
196
- if (state.busy) {
197
- store.pushNotice('wait for the current turn to finish before /fast', 'warn');
198
- return false;
199
- }
200
206
  const value = String(arg || '').trim().toLowerCase();
201
207
  const setTo = value
202
208
  ? ['1', 'true', 'yes', 'on', 'enable', 'enabled'].includes(value)
@@ -216,7 +222,11 @@ export function createSlashDispatch({
216
222
  store.pushNotice('Fast mode switch is already running.', 'warn');
217
223
  return;
218
224
  }
219
- store.pushNotice(`Fast mode ${enabled ? 'on' : 'off'} for ${state.provider}/${state.model}`, 'info');
225
+ store.pushNotice(
226
+ `Fast mode ${enabled ? 'on' : 'off'} for ${state.provider}/${state.model}`
227
+ + (state.busy ? ' (applies from the next turn)' : ''),
228
+ 'info',
229
+ );
220
230
  })
221
231
  .catch((e) => store.pushNotice(`Couldn’t update fast mode: ${e?.message || e}`, 'error'));
222
232
  return true;
@@ -345,7 +355,7 @@ export function createSlashDispatch({
345
355
  return;
346
356
  }
347
357
  if (r.error) {
348
- store.pushNotice('Compact failed.', 'error');
358
+ store.pushNotice(compactFailureNotice(r.error), 'error');
349
359
  return;
350
360
  }
351
361
  if (r.changed === false && r.reason) {
@@ -358,7 +368,7 @@ export function createSlashDispatch({
358
368
  }
359
369
  store.pushNotice('Compact done.', 'info');
360
370
  })
361
- .catch(() => store.pushNotice('Compact failed.', 'error'));
371
+ .catch((error) => store.pushNotice(compactFailureNotice(error), 'error'));
362
372
  return true;
363
373
  case 'resume':
364
374
  if (state.busy) {
@@ -140,8 +140,10 @@ export function createThemeEffortPickers({
140
140
  items: pickerItems,
141
141
  onSelect: (value) => {
142
142
  setPicker(null);
143
+ // Mid-turn picks are allowed; the running turn keeps its own effort.
144
+ const pendingTurn = state.busy ? ' (applies from the next turn)' : '';
143
145
  void store.setEffort(value)
144
- .then(result => store.pushNotice(result ? `Effort set to ${result}` : 'Effort switch is already running.', result ? 'info' : 'warn'))
146
+ .then(result => store.pushNotice(result ? `Effort set to ${result}${pendingTurn}` : 'Effort switch is already running.', result ? 'info' : 'warn'))
145
147
  .catch((e) => store.pushNotice(`Couldn’t switch effort: ${e?.message || e}`, 'error'));
146
148
  },
147
149
  onCancel: () => {
@@ -454,6 +454,15 @@ import {
454
454
  unlinkSync as unlinkSync2,
455
455
  writeFileSync as writeFileSync3
456
456
  } from "fs";
457
+ import {
458
+ link as linkAsync,
459
+ mkdir as mkdirAsync,
460
+ open as openAsync,
461
+ readFile as readFileAsync2,
462
+ rename as renameAsync,
463
+ unlink as unlinkAsync,
464
+ writeFile as writeFileAsync
465
+ } from "node:fs/promises";
457
466
  import { dirname as dirname4, basename as basename4, join as join4 } from "path";
458
467
  import { randomBytes as randomBytes2 } from "crypto";
459
468
  import { execFile as execFile2, execFileSync } from "child_process";
@@ -496,6 +505,22 @@ function renameWithRetrySync(src, dst, opts = {}) {
496
505
  }
497
506
  throw lastErr;
498
507
  }
508
+ async function renameWithRetry(src, dst, opts = {}) {
509
+ const backoffs = Array.isArray(opts.backoffs) && opts.backoffs.length > 0 ? opts.backoffs : DEFAULT_BACKOFFS_MS;
510
+ let lastErr = null;
511
+ for (let attempt = 0; attempt <= backoffs.length; attempt++) {
512
+ try {
513
+ await renameAsync(src, dst);
514
+ return true;
515
+ } catch (err) {
516
+ lastErr = err;
517
+ if (!RETRY_CODES.has(err?.code) || attempt >= backoffs.length) break;
518
+ const jitter = Math.floor(Math.random() * Math.min(50, Math.max(1, backoffs[attempt])));
519
+ await _asyncSleep(backoffs[attempt] + jitter);
520
+ }
521
+ }
522
+ throw lastErr;
523
+ }
499
524
  function withFileLockSync(lockPath, fn, opts = {}) {
500
525
  const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : DEFAULT_LOCK_TIMEOUT_MS;
501
526
  const staleMs = Number.isFinite(opts.staleMs) ? opts.staleMs : 3e4;
@@ -893,55 +918,55 @@ function writeJsonAtomicSync(filePath, value, opts = {}) {
893
918
  async function writeFileAtomicAsync(filePath, data, opts = {}) {
894
919
  const run = async () => {
895
920
  const dir = dirname4(filePath);
896
- mkdirSync3(dir, { recursive: true });
921
+ await mkdirAsync(dir, { recursive: true });
897
922
  const tmp = join4(dir, `.${basename4(filePath)}.${randomBytes2(12).toString("hex")}.tmp`);
898
923
  try {
899
924
  const writeOpts = { encoding: opts.encoding || "utf8", flag: "wx", mode: opts.mode !== void 0 ? opts.mode : 384 };
900
- writeFileSync3(tmp, data, writeOpts);
925
+ await writeFileAsync(tmp, data, writeOpts);
901
926
  if (opts.secret === true) await _enforceOwnerOnlyAclWin32Async(tmp);
902
927
  if (opts.fsync !== false) {
903
928
  let fd = null;
904
929
  try {
905
- fd = openSync(tmp, "r");
906
- fsyncSync(fd);
930
+ fd = await openAsync(tmp, "r");
931
+ await fd.sync();
907
932
  } catch (err) {
908
933
  if (!["EPERM", "ENOTSUP", "EINVAL"].includes(err?.code)) throw err;
909
934
  } finally {
910
935
  try {
911
- if (fd !== null) closeSync(fd);
936
+ if (fd !== null) await fd.close();
912
937
  } catch {
913
938
  }
914
939
  }
915
940
  }
916
941
  if (opts.createOnly === true) {
917
942
  try {
918
- linkSync(tmp, filePath);
943
+ await linkAsync(tmp, filePath);
919
944
  } catch (err) {
920
945
  try {
921
- unlinkSync2(tmp);
946
+ await unlinkAsync(tmp);
922
947
  } catch {
923
948
  }
924
949
  if (err?.code === "EEXIST") return false;
925
950
  throw err;
926
951
  }
927
952
  try {
928
- unlinkSync2(tmp);
953
+ await unlinkAsync(tmp);
929
954
  } catch {
930
955
  }
931
956
  } else {
932
- renameWithRetrySync(tmp, filePath, opts);
957
+ await renameWithRetry(tmp, filePath, opts);
933
958
  }
934
959
  if (opts.secret === true) await _enforceOwnerOnlyAclWin32Async(filePath);
935
960
  if (opts.fsyncDir === true) {
936
961
  let dfd = null;
937
962
  try {
938
- dfd = openSync(dir, "r");
939
- fsyncSync(dfd);
963
+ dfd = await openAsync(dir, "r");
964
+ await dfd.sync();
940
965
  } catch (err) {
941
966
  if (!["EPERM", "ENOTSUP", "EINVAL", "EACCES"].includes(err?.code)) throw err;
942
967
  } finally {
943
968
  try {
944
- if (dfd !== null) closeSync(dfd);
969
+ if (dfd !== null) await dfd.close();
945
970
  } catch {
946
971
  }
947
972
  }
@@ -949,7 +974,7 @@ async function writeFileAtomicAsync(filePath, data, opts = {}) {
949
974
  return true;
950
975
  } catch (err) {
951
976
  try {
952
- if (existsSync4(tmp)) unlinkSync2(tmp);
977
+ await unlinkAsync(tmp);
953
978
  } catch {
954
979
  }
955
980
  throw err;
@@ -965,16 +990,16 @@ function writeJsonAtomicAsync(filePath, value, opts = {}) {
965
990
  }
966
991
  async function updateJsonAtomic(filePath, mutator, opts = {}) {
967
992
  const { lock: _lock, ...writeOpts } = opts;
968
- return withFileLock(`${filePath}.lock`, () => {
993
+ return withFileLock(`${filePath}.lock`, async () => {
969
994
  let cur = null;
970
995
  try {
971
- cur = JSON.parse(readFileSync3(filePath, "utf8"));
996
+ cur = JSON.parse(await readFileAsync2(filePath, "utf8"));
972
997
  } catch {
973
998
  cur = null;
974
999
  }
975
1000
  const next = mutator(cur);
976
1001
  if (next === void 0) return cur;
977
- writeJsonAtomicSync(filePath, next, { ...writeOpts, lock: false });
1002
+ await writeJsonAtomicAsync(filePath, next, { ...writeOpts, lock: false });
978
1003
  return next;
979
1004
  }, opts);
980
1005
  }
@@ -11989,6 +12014,7 @@ function presentErrorText(error, options = {}) {
11989
12014
  return "Context too large.";
11990
12015
  }
11991
12016
  if (/\bcompact(?:ion)?\b.*\b(?:failed|error|overflow)\b|\b(?:failed|error)\b.*\bcompact(?:ion)?\b/i.test(text)) {
12017
+ if (String(surface).toLowerCase().includes("compact")) return capText(text, max);
11992
12018
  return "Compact failed.";
11993
12019
  }
11994
12020
  const quotaRetry = /retryAfter=([^\s:]+)/i.exec(text);
@@ -15657,7 +15683,8 @@ function createThemeEffortPickers({
15657
15683
  items: pickerItems,
15658
15684
  onSelect: (value) => {
15659
15685
  setPicker(null);
15660
- void store.setEffort(value).then((result) => store.pushNotice(result ? `Effort set to ${result}` : "Effort switch is already running.", result ? "info" : "warn")).catch((e) => store.pushNotice(`Couldn\u2019t switch effort: ${e?.message || e}`, "error"));
15686
+ const pendingTurn = state.busy ? " (applies from the next turn)" : "";
15687
+ void store.setEffort(value).then((result) => store.pushNotice(result ? `Effort set to ${result}${pendingTurn}` : "Effort switch is already running.", result ? "info" : "warn")).catch((e) => store.pushNotice(`Couldn\u2019t switch effort: ${e?.message || e}`, "error"));
15661
15688
  },
15662
15689
  onCancel: () => {
15663
15690
  setPicker(null);
@@ -19206,6 +19233,12 @@ function createSettingsPicker({
19206
19233
  }
19207
19234
 
19208
19235
  // src/tui/app/slash-dispatch.mjs
19236
+ function compactFailureNotice(error) {
19237
+ if (error == null || String(error).trim() === "") return "Compact failed.";
19238
+ const reason = presentErrorText(error, { surface: "compact", max: 320 });
19239
+ if (!reason || reason === "Unknown error") return "Compact failed.";
19240
+ return /^compact(?:ion)? failed\b/i.test(reason) ? reason : `Compact failed: ${reason}`;
19241
+ }
19209
19242
  function createSlashDispatch({
19210
19243
  state,
19211
19244
  store,
@@ -19365,22 +19398,16 @@ function createSlashDispatch({
19365
19398
  }
19366
19399
  return true;
19367
19400
  }
19368
- case "effort":
19369
- if (state.busy) {
19370
- store.pushNotice("wait for the current turn to finish before /effort", "warn");
19371
- return false;
19372
- }
19401
+ case "effort": {
19402
+ const pendingTurn = state.busy ? " (applies from the next turn)" : "";
19373
19403
  if (!arg) {
19374
19404
  openEffortPicker();
19375
19405
  return true;
19376
19406
  }
19377
- void store.setEffort(arg).then((result) => store.pushNotice(result ? `Effort set to ${result}` : "Effort switch is already running.", result ? "info" : "warn")).catch((e) => store.pushNotice(`Couldn\u2019t switch effort: ${e?.message || e}`, "error"));
19407
+ void store.setEffort(arg).then((result) => store.pushNotice(result ? `Effort set to ${result}${pendingTurn}` : "Effort switch is already running.", result ? "info" : "warn")).catch((e) => store.pushNotice(`Couldn\u2019t switch effort: ${e?.message || e}`, "error"));
19378
19408
  return true;
19409
+ }
19379
19410
  case "fast": {
19380
- if (state.busy) {
19381
- store.pushNotice("wait for the current turn to finish before /fast", "warn");
19382
- return false;
19383
- }
19384
19411
  const value = String(arg || "").trim().toLowerCase();
19385
19412
  const setTo = value ? ["1", "true", "yes", "on", "enable", "enabled"].includes(value) ? true : ["0", "false", "no", "off", "disable", "disabled"].includes(value) ? false : null : void 0;
19386
19413
  if (setTo === null) {
@@ -19393,7 +19420,10 @@ function createSlashDispatch({
19393
19420
  store.pushNotice("Fast mode switch is already running.", "warn");
19394
19421
  return;
19395
19422
  }
19396
- store.pushNotice(`Fast mode ${enabled ? "on" : "off"} for ${state.provider}/${state.model}`, "info");
19423
+ store.pushNotice(
19424
+ `Fast mode ${enabled ? "on" : "off"} for ${state.provider}/${state.model}` + (state.busy ? " (applies from the next turn)" : ""),
19425
+ "info"
19426
+ );
19397
19427
  }).catch((e) => store.pushNotice(`Couldn\u2019t update fast mode: ${e?.message || e}`, "error"));
19398
19428
  return true;
19399
19429
  }
@@ -19516,7 +19546,7 @@ function createSlashDispatch({
19516
19546
  return;
19517
19547
  }
19518
19548
  if (r.error) {
19519
- store.pushNotice("Compact failed.", "error");
19549
+ store.pushNotice(compactFailureNotice(r.error), "error");
19520
19550
  return;
19521
19551
  }
19522
19552
  if (r.changed === false && r.reason) {
@@ -19528,7 +19558,7 @@ function createSlashDispatch({
19528
19558
  return;
19529
19559
  }
19530
19560
  store.pushNotice("Compact done.", "info");
19531
- }).catch(() => store.pushNotice("Compact failed.", "error"));
19561
+ }).catch((error) => store.pushNotice(compactFailureNotice(error), "error"));
19532
19562
  return true;
19533
19563
  case "resume":
19534
19564
  if (state.busy) {
@@ -26155,8 +26185,10 @@ function createSessionFlow(bag) {
26155
26185
  const mode = options.mode || "prompt";
26156
26186
  const priority = options.priority || defaultQueuePriority(mode);
26157
26187
  const displayText = promptDisplayText(text, options);
26188
+ const submittedAt = Number(options.submittedAt);
26158
26189
  return {
26159
26190
  id: options.id || nextId2(),
26191
+ submittedAt: Number.isFinite(submittedAt) && submittedAt > 0 ? Math.round(submittedAt) : Date.now(),
26160
26192
  text: displayText,
26161
26193
  content: text,
26162
26194
  pastedImages: options.pastedImages && typeof options.pastedImages === "object" ? options.pastedImages : null,
@@ -26350,6 +26382,8 @@ function createSessionFlow(bag) {
26350
26382
  set({ queued: [...getState().queued, entry] });
26351
26383
  if (isQueuedEntryEditable(entry)) flushEmitImmediate?.();
26352
26384
  }
26385
+ const queueAgeMs = Math.max(0, Date.now() - Number(entry.submittedAt || Date.now()));
26386
+ tuiDebug2(`prompt-queued id=${String(entry.id)} busy=${getState().busy ? 1 : 0} ageMs=${queueAgeMs}`);
26353
26387
  if (getState().busy) tuiDebug2(`busy-queue enqueue mode=${entry.mode} pending=${pending.length}`);
26354
26388
  void drain();
26355
26389
  return true;
@@ -26368,19 +26402,25 @@ function createSessionFlow(bag) {
26368
26402
  if (batch.length === 0) break;
26369
26403
  for (const entry of batch) {
26370
26404
  const content = entry.content;
26405
+ const steeringMeta = {
26406
+ id: entry.id,
26407
+ submittedAt: entry.submittedAt,
26408
+ ...Array.isArray(entry.images) && entry.images.length ? { images: entry.images } : {}
26409
+ };
26371
26410
  if (entry.suppressDisplay) {
26372
26411
  if (Array.isArray(content) ? content.length > 0 : String(content ?? "").trim().length > 0) {
26373
- out.push({ text: STEERING_SUPPRESSED_DISPLAY, content });
26412
+ out.push({ ...steeringMeta, text: STEERING_SUPPRESSED_DISPLAY, content });
26374
26413
  }
26375
26414
  continue;
26376
26415
  }
26377
- const value = typeof content === "string" ? content.trim() : { text: String(entry.text || "").trim(), content };
26378
- if (typeof value === "string") {
26379
- if (value.length > 0) out.push(value);
26380
- } else if (Array.isArray(value.content)) {
26381
- if (value.content.length > 0) out.push(value);
26382
- } else if (String(value.content ?? "").trim().length > 0) {
26383
- out.push(value);
26416
+ const displayText = String(entry.text || "").trim();
26417
+ if (typeof content === "string") {
26418
+ const value = content.trim();
26419
+ if (value.length > 0) out.push({ ...steeringMeta, text: displayText || value, content: value });
26420
+ } else if (Array.isArray(content)) {
26421
+ if (content.length > 0) out.push({ ...steeringMeta, text: displayText, content });
26422
+ } else if (String(content ?? "").trim().length > 0) {
26423
+ out.push({ ...steeringMeta, text: displayText, content });
26384
26424
  }
26385
26425
  }
26386
26426
  commitSteeringQueueEntries(batch);
@@ -26779,8 +26819,11 @@ function createRunTurn(bag) {
26779
26819
  drain,
26780
26820
  drainPendingSteering
26781
26821
  } = bag;
26782
- const updateStreamingTail = updateStreamingTailFromStore || ((id, patch = {}) => {
26783
- set({ streamingTail: { ...getState().streamingTail || {}, ...patch, kind: "assistant", id, streaming: true } });
26822
+ const updateStreamingTail = updateStreamingTailFromStore || ((id, patch = {}, extra = {}) => {
26823
+ set({
26824
+ streamingTail: { ...getState().streamingTail || {}, ...patch, kind: "assistant", id, streaming: true },
26825
+ ...extra
26826
+ });
26784
26827
  return true;
26785
26828
  });
26786
26829
  const settleStreamingTail = settleStreamingTailFromStore || ((id, patch = {}) => {
@@ -27293,7 +27336,12 @@ function createRunTurn(bag) {
27293
27336
  patch.spinner = { ...getState().spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || getState().spinner.thinkingLastEndedAt, mode: compactingActive ? "compacting" : "responding" };
27294
27337
  _publishedThinkingActive = false;
27295
27338
  }
27296
- if (Object.keys(patch).length > 0) set(patch);
27339
+ if (patch.streamingTail) {
27340
+ const { streamingTail, ...extra } = patch;
27341
+ updateStreamingTail(streamingTail.id, streamingTail, extra);
27342
+ } else if (Object.keys(patch).length > 0) {
27343
+ set(patch);
27344
+ }
27297
27345
  _pendingThinkingLastEndedAt = 0;
27298
27346
  }
27299
27347
  if (_pendingThinkFlush) {
@@ -27384,7 +27432,7 @@ function createRunTurn(bag) {
27384
27432
  onStreamDelta: () => {
27385
27433
  markTurnProgress("stream-delta");
27386
27434
  },
27387
- onSteerMessage: (text) => {
27435
+ onSteerMessage: (text, steeringMeta = {}) => {
27388
27436
  if (!markTurnProgress("steer-message")) return;
27389
27437
  if (text === STEERING_SUPPRESSED_DISPLAY) return;
27390
27438
  flushStreamBatch();
@@ -27393,7 +27441,13 @@ function createRunTurn(bag) {
27393
27441
  const value = String(text || "").trim();
27394
27442
  if (value) {
27395
27443
  clearAggregateContinuation();
27396
- pushUserOrSyntheticItem(value, void 0, "injected");
27444
+ const steeringIds = Array.isArray(steeringMeta?.ids) ? steeringMeta.ids.filter((id) => id !== void 0 && id !== null) : [];
27445
+ pushUserOrSyntheticItem(
27446
+ value,
27447
+ steeringIds[0],
27448
+ "injected",
27449
+ Array.isArray(steeringMeta?.images) && steeringMeta.images.length ? { images: steeringMeta.images } : null
27450
+ );
27397
27451
  }
27398
27452
  },
27399
27453
  onToolCall: async (_iter, calls) => {
@@ -27593,17 +27647,11 @@ function createRunTurn(bag) {
27593
27647
  _emittedVisibleText = "";
27594
27648
  if (currentAssistantId) {
27595
27649
  if (currentAssistantText) {
27596
- set({
27597
- streamingTail: {
27598
- ...getState().streamingTail || {},
27599
- kind: "assistant",
27600
- id: currentAssistantId,
27601
- text: currentAssistantText,
27602
- streaming: true,
27603
- at: getState().streamingTail?.at || Date.now(),
27604
- ...turnRouteMeta
27605
- }
27606
- });
27650
+ updateStreamingTail(currentAssistantId, {
27651
+ text: currentAssistantText,
27652
+ at: getState().streamingTail?.at || Date.now(),
27653
+ ...turnRouteMeta
27654
+ }, {}, { resetText: true });
27607
27655
  } else {
27608
27656
  clearStreamingTail(currentAssistantId);
27609
27657
  currentAssistantId = null;
@@ -29992,7 +30040,7 @@ function createLiveShare({
29992
30040
  const applyViewerFrame = (frame, socket) => {
29993
30041
  if (frame.t === "full") {
29994
30042
  viewerApply.replaceItems(Array.isArray(frame.items) ? frame.items : []);
29995
- if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail);
30043
+ if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail, {}, { resetText: true });
29996
30044
  viewerApply.set({ spinner: frame.spinner || null });
29997
30045
  applyLiveState(frame.live);
29998
30046
  return;
@@ -30016,7 +30064,7 @@ function createLiveShare({
30016
30064
  requestSync(socket);
30017
30065
  }
30018
30066
  } else if ("tail" in frame) {
30019
- if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail);
30067
+ if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail, {}, { resetText: true });
30020
30068
  else viewerApply.clearStreamingTail();
30021
30069
  }
30022
30070
  if ("spinner" in frame) viewerApply.set({ spinner: frame.spinner || null });
@@ -30937,10 +30985,20 @@ async function createEngineSession({
30937
30985
  ...extra
30938
30986
  });
30939
30987
  };
30940
- const updateStreamingTail = (id, patch = {}, extra = {}) => {
30988
+ const streamingTailTextEpochKey = /* @__PURE__ */ Symbol.for("mixdog.streaming-tail-text-epoch");
30989
+ let streamingTailTextEpoch = 0;
30990
+ const updateStreamingTail = (id, patch = {}, extra = {}, { resetText = false } = {}) => {
30941
30991
  if (id == null) return false;
30942
30992
  const current = state.streamingTail?.id === id ? state.streamingTail : { kind: "assistant", id, text: "", streaming: true };
30943
30993
  const next = { ...current, ...patch, kind: "assistant", id, streaming: true };
30994
+ const currentTextEpoch = current[streamingTailTextEpochKey];
30995
+ const textEpoch = !resetText && Number.isSafeInteger(currentTextEpoch) ? currentTextEpoch : ++streamingTailTextEpoch;
30996
+ Object.defineProperty(next, streamingTailTextEpochKey, {
30997
+ value: textEpoch,
30998
+ enumerable: false,
30999
+ configurable: false,
31000
+ writable: false
31001
+ });
30944
31002
  let changed = state.streamingTail !== current;
30945
31003
  if (!changed) {
30946
31004
  for (const [key, value] of Object.entries(next)) {
@@ -446,7 +446,7 @@ export function createLiveShare({
446
446
  const applyViewerFrame = (frame, socket) => {
447
447
  if (frame.t === 'full') {
448
448
  viewerApply.replaceItems(Array.isArray(frame.items) ? frame.items : []);
449
- if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail);
449
+ if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail, {}, { resetText: true });
450
450
  viewerApply.set({ spinner: frame.spinner || null });
451
451
  applyLiveState(frame.live);
452
452
  return;
@@ -472,7 +472,7 @@ export function createLiveShare({
472
472
  requestSync(socket);
473
473
  }
474
474
  } else if ('tail' in frame) {
475
- if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail);
475
+ if (frame.tail) viewerApply.updateStreamingTail(frame.tail.id, frame.tail, {}, { resetText: true });
476
476
  else viewerApply.clearStreamingTail();
477
477
  }
478
478
  if ('spinner' in frame) viewerApply.set({ spinner: frame.spinner || null });