mixdog 0.9.13 → 0.9.14

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.
@@ -3,7 +3,7 @@ import React21 from "react";
3
3
  import { render } from "../../../vendor/ink/build/index.js";
4
4
  import { closeSync as closeSync2, constants as fsConstants, createWriteStream, mkdirSync as mkdirSync6, openSync as openSync2, readSync } from "node:fs";
5
5
  import { tmpdir as tmpdir4 } from "node:os";
6
- import { dirname as dirname6, join as join8 } from "node:path";
6
+ import { dirname as dirname6, join as join9 } from "node:path";
7
7
  import { performance as performance3 } from "node:perf_hooks";
8
8
 
9
9
  // src/tui/App.jsx
@@ -21886,6 +21886,166 @@ function createAgentJobFeed({
21886
21886
  };
21887
21887
  }
21888
21888
 
21889
+ // src/tui/engine/tui-steering-persist.mjs
21890
+ import { randomBytes as randomBytes3 } from "crypto";
21891
+ import { join as join7 } from "path";
21892
+ var PENDING_MESSAGES_FILE = "session-pending-messages.json";
21893
+ var PENDING_MESSAGES_MODE = 384;
21894
+ function pendingMessagesPath() {
21895
+ return join7(resolvePluginData(), PENDING_MESSAGES_FILE);
21896
+ }
21897
+ function tuiSteeringSessionKey(leadSessionId) {
21898
+ if (typeof leadSessionId !== "string" || !/^[A-Za-z0-9_-]+$/.test(leadSessionId)) return null;
21899
+ return `tui_${leadSessionId}`;
21900
+ }
21901
+ function newSteeringPersistId() {
21902
+ return `ts_${Date.now().toString(36)}_${randomBytes3(4).toString("hex")}`;
21903
+ }
21904
+ function normalizeTuiSteeringQueueEntry(entry) {
21905
+ if (typeof entry === "string") {
21906
+ const text = entry.trim();
21907
+ return text || null;
21908
+ }
21909
+ if (!entry || typeof entry !== "object") return null;
21910
+ if (typeof entry.text === "string" && entry.text.trim()) {
21911
+ const text = entry.text.trim();
21912
+ const id = typeof entry.id === "string" && entry.id.trim() ? entry.id.trim() : null;
21913
+ return id ? { id, text } : text;
21914
+ }
21915
+ return null;
21916
+ }
21917
+ function normalizePendingStore(raw) {
21918
+ const sessions = raw && typeof raw === "object" && raw.sessions && typeof raw.sessions === "object" ? raw.sessions : {};
21919
+ const storeUpdatedAt = Number(raw?.updatedAt) || Date.now();
21920
+ const touchedRaw = raw && typeof raw === "object" && raw.sessionTouchedAt && typeof raw.sessionTouchedAt === "object" ? raw.sessionTouchedAt : {};
21921
+ const out = { version: 1, updatedAt: storeUpdatedAt, sessions: {}, sessionTouchedAt: {} };
21922
+ for (const [sid, value] of Object.entries(sessions)) {
21923
+ if (!/^[A-Za-z0-9_-]+$/.test(sid) || !Array.isArray(value)) continue;
21924
+ const q = sid.startsWith("tui_") ? value.map(normalizeTuiSteeringQueueEntry).filter(Boolean) : value.map((entry) => {
21925
+ if (typeof entry === "string") return entry;
21926
+ if (entry && typeof entry === "object" && typeof entry.message === "string") return entry.message;
21927
+ return "";
21928
+ }).filter(Boolean);
21929
+ if (q.length > 0) {
21930
+ out.sessions[sid] = q;
21931
+ const touched = Number(touchedRaw[sid]);
21932
+ out.sessionTouchedAt[sid] = Number.isFinite(touched) && touched > 0 ? touched : storeUpdatedAt;
21933
+ }
21934
+ }
21935
+ return out;
21936
+ }
21937
+ function touchPendingSessionEntry(next, sessionId, now = Date.now()) {
21938
+ if (!next.sessionTouchedAt || typeof next.sessionTouchedAt !== "object") next.sessionTouchedAt = {};
21939
+ next.sessionTouchedAt[sessionId] = now;
21940
+ }
21941
+ function entryPersistText(entry) {
21942
+ if (!entry) return "";
21943
+ const text = typeof entry.text === "string" && entry.text.trim() ? entry.text.trim() : promptContentText(entry.content ?? entry.text ?? "").trim();
21944
+ return text;
21945
+ }
21946
+ function rowMatchesEntry(row, entry) {
21947
+ const persistId = entry?.steeringPersistId;
21948
+ if (persistId) {
21949
+ return Boolean(row && typeof row === "object" && row.id === persistId);
21950
+ }
21951
+ const text = entryPersistText(entry);
21952
+ if (!text) return false;
21953
+ return row === text;
21954
+ }
21955
+ function removePersistRow(q, entry) {
21956
+ const idx = q.findIndex((row) => rowMatchesEntry(row, entry));
21957
+ if (idx >= 0) q.splice(idx, 1);
21958
+ }
21959
+ function appendTuiSteeringPersist(leadSessionId, entry) {
21960
+ const text = entryPersistText(entry);
21961
+ if (!text) return;
21962
+ const key = tuiSteeringSessionKey(leadSessionId);
21963
+ if (!key) return;
21964
+ if (!entry.steeringPersistId) entry.steeringPersistId = newSteeringPersistId();
21965
+ const record = { id: entry.steeringPersistId, text };
21966
+ try {
21967
+ updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
21968
+ const next = normalizePendingStore(raw);
21969
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
21970
+ q.push(record);
21971
+ next.sessions[key] = q;
21972
+ const now = Date.now();
21973
+ next.updatedAt = now;
21974
+ touchPendingSessionEntry(next, key, now);
21975
+ return next;
21976
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
21977
+ } catch (err) {
21978
+ try {
21979
+ process.stderr.write(`[tui] steering-queue append failed sessionId=${leadSessionId}: ${err?.message || err}
21980
+ `);
21981
+ } catch {
21982
+ }
21983
+ }
21984
+ }
21985
+ function dropTuiSteeringPersist(leadSessionId, entries) {
21986
+ const key = tuiSteeringSessionKey(leadSessionId);
21987
+ if (!key) return;
21988
+ const batch = Array.isArray(entries) ? entries : [];
21989
+ if (batch.length === 0) return;
21990
+ try {
21991
+ updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
21992
+ const next = normalizePendingStore(raw);
21993
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key].slice() : [];
21994
+ if (q.length === 0) return void 0;
21995
+ for (const entry of batch) {
21996
+ removePersistRow(q, entry);
21997
+ }
21998
+ if (q.length === 0) {
21999
+ delete next.sessions[key];
22000
+ if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
22001
+ } else {
22002
+ next.sessions[key] = q;
22003
+ }
22004
+ next.updatedAt = Date.now();
22005
+ return next;
22006
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22007
+ } catch (err) {
22008
+ try {
22009
+ process.stderr.write(`[tui] steering-queue drop failed sessionId=${leadSessionId}: ${err?.message || err}
22010
+ `);
22011
+ } catch {
22012
+ }
22013
+ }
22014
+ }
22015
+ function drainedRowToRestore(row) {
22016
+ if (typeof row === "string") {
22017
+ return { text: row, steeringPersistId: null };
22018
+ }
22019
+ if (row && typeof row === "object" && typeof row.text === "string") {
22020
+ return { text: row.text, steeringPersistId: typeof row.id === "string" ? row.id : null };
22021
+ }
22022
+ return null;
22023
+ }
22024
+ function drainTuiSteeringPersist(leadSessionId) {
22025
+ const key = tuiSteeringSessionKey(leadSessionId);
22026
+ if (!key) return [];
22027
+ let drained = [];
22028
+ try {
22029
+ updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
22030
+ const next = normalizePendingStore(raw);
22031
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
22032
+ drained = q.map(drainedRowToRestore).filter(Boolean);
22033
+ if (drained.length === 0) return void 0;
22034
+ delete next.sessions[key];
22035
+ if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
22036
+ next.updatedAt = Date.now();
22037
+ return next;
22038
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
22039
+ } catch (err) {
22040
+ try {
22041
+ process.stderr.write(`[tui] steering-queue drain failed sessionId=${leadSessionId}: ${err?.message || err}
22042
+ `);
22043
+ } catch {
22044
+ }
22045
+ }
22046
+ return drained;
22047
+ }
22048
+
21889
22049
  // src/tui/engine.mjs
21890
22050
  var SESSION_RUNTIME_MODULE = import.meta.url.replace(/\\/g, "/").includes("/tui/dist/") ? "../../mixdog-session-runtime.mjs" : "../mixdog-session-runtime.mjs";
21891
22051
  var TOOL_APPROVAL_TIMEOUT_MS = (() => {
@@ -22992,6 +23152,17 @@ async function createEngineSession({
22992
23152
  const pending = [];
22993
23153
  let draining = false;
22994
23154
  let activePromptRestore = null;
23155
+ const leadSessionId = () => runtime.id;
23156
+ function shouldMirrorSteeringEntry(entry) {
23157
+ return isQueuedEntryEditable(entry) && !isSlashQueuedEntry(entry);
23158
+ }
23159
+ function commitSteeringQueueEntries(entries) {
23160
+ callCommitCallbacks(entries);
23161
+ const mirrored = (Array.isArray(entries) ? entries : []).filter(
23162
+ (entry) => shouldMirrorSteeringEntry(entry) && !entry.steeringPersistRestored
23163
+ );
23164
+ if (mirrored.length > 0) dropTuiSteeringPersist(leadSessionId(), mirrored);
23165
+ }
22995
23166
  function makeQueueEntry(text, options = {}) {
22996
23167
  const mode = options.mode || "prompt";
22997
23168
  const priority = options.priority || defaultQueuePriority(mode);
@@ -23007,7 +23178,9 @@ async function createEngineSession({
23007
23178
  priority,
23008
23179
  key: options.key || null,
23009
23180
  skipSlashCommands: options.skipSlashCommands === true,
23010
- displayText: mode === "task-notification" ? notificationDisplayText(displayText) : String(displayText || "")
23181
+ displayText: mode === "task-notification" ? notificationDisplayText(displayText) : String(displayText || ""),
23182
+ steeringPersistId: options.steeringPersistId || null,
23183
+ steeringPersistRestored: options.steeringPersistRestored === true
23011
23184
  };
23012
23185
  }
23013
23186
  function removeQueuedEntries(entries) {
@@ -23092,7 +23265,7 @@ async function createEngineSession({
23092
23265
  displayText: batch.map((entry) => entry.text).filter((text) => String(text || "").trim()).join("\n"),
23093
23266
  pastedImages: batchPastedImages,
23094
23267
  pastedTexts: batchPastedTexts,
23095
- onCommitted: () => callCommitCallbacks(batch),
23268
+ onCommitted: () => commitSteeringQueueEntries(batch),
23096
23269
  submittedIds: [...ids],
23097
23270
  restorable: nonEditable.length === 0,
23098
23271
  requeueOnAbort: nonEditable
@@ -23122,6 +23295,9 @@ async function createEngineSession({
23122
23295
  pendingNotificationKeys.add(entry.key);
23123
23296
  }
23124
23297
  pending.push(entry);
23298
+ if (state.busy && shouldMirrorSteeringEntry(entry)) {
23299
+ appendTuiSteeringPersist(leadSessionId(), entry);
23300
+ }
23125
23301
  if (isQueuedEntryVisible(entry)) set({ queued: [...state.queued, entry] });
23126
23302
  void drain();
23127
23303
  return true;
@@ -23138,9 +23314,24 @@ async function createEngineSession({
23138
23314
  if (Array.isArray(entry?.content)) return entry.content.length > 0;
23139
23315
  return String(entry?.content ?? "").trim().length > 0;
23140
23316
  });
23141
- callCommitCallbacks(batch);
23317
+ commitSteeringQueueEntries(batch);
23142
23318
  return out;
23143
23319
  }
23320
+ function restoreLeadSteeringFromDisk() {
23321
+ const rows = drainTuiSteeringPersist(leadSessionId());
23322
+ if (!rows.length) return;
23323
+ const restored = [];
23324
+ for (const row of rows) {
23325
+ const entry = makeQueueEntry(row.text, {
23326
+ steeringPersistRestored: true,
23327
+ steeringPersistId: row.steeringPersistId || void 0
23328
+ });
23329
+ pending.push(entry);
23330
+ if (isQueuedEntryVisible(entry)) restored.push(entry);
23331
+ }
23332
+ if (restored.length > 0) set({ queued: [...state.queued, ...restored] });
23333
+ void drain();
23334
+ }
23144
23335
  async function autoClearBeforeSubmit() {
23145
23336
  const cfg = autoClearState();
23146
23337
  const now = Date.now();
@@ -23291,6 +23482,7 @@ async function createEngineSession({
23291
23482
  syncContextStats({ allowEstimated: true });
23292
23483
  return state.stats;
23293
23484
  };
23485
+ restoreLeadSteeringFromDisk();
23294
23486
  return {
23295
23487
  getState: () => state,
23296
23488
  patchItem,
@@ -24213,6 +24405,7 @@ async function createEngineSession({
24213
24405
  ...routeState(),
24214
24406
  stats: { ...state.stats }
24215
24407
  });
24408
+ restoreLeadSteeringFromDisk();
24216
24409
  return true;
24217
24410
  } finally {
24218
24411
  set({ commandBusy: false, commandStatus: null });
@@ -24392,7 +24585,7 @@ function installProcessSignalCleanup({
24392
24585
 
24393
24586
  // src/runtime/channels/lib/runtime-paths.mjs
24394
24587
  import { tmpdir as tmpdir3 } from "os";
24395
- import { basename as basename4, join as join7, resolve as resolve4 } from "path";
24588
+ import { basename as basename4, join as join8, resolve as resolve4 } from "path";
24396
24589
 
24397
24590
  // src/runtime/channels/lib/state-file.mjs
24398
24591
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
@@ -24401,9 +24594,9 @@ function ensureDir2(dirPath) {
24401
24594
  }
24402
24595
 
24403
24596
  // src/runtime/channels/lib/runtime-paths.mjs
24404
- var RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT ? resolve4(process.env.MIXDOG_RUNTIME_ROOT) : join7(tmpdir3(), "mixdog");
24405
- var OWNER_DIR = join7(RUNTIME_ROOT, "owners");
24406
- var ACTIVE_INSTANCE_FILE = join7(RUNTIME_ROOT, "active-instance.json");
24597
+ var RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT ? resolve4(process.env.MIXDOG_RUNTIME_ROOT) : join8(tmpdir3(), "mixdog");
24598
+ var OWNER_DIR = join8(RUNTIME_ROOT, "owners");
24599
+ var ACTIVE_INSTANCE_FILE = join8(RUNTIME_ROOT, "active-instance.json");
24407
24600
  var RUNTIME_STALE_TTL = 24 * 60 * 60 * 1e3;
24408
24601
  var STATUS_FILE_TTL = 6 * 60 * 60 * 1e3;
24409
24602
  var TMP_FILE_TTL = 60 * 60 * 1e3;
@@ -24426,7 +24619,7 @@ function touchUiHeartbeat(instanceId) {
24426
24619
  } catch {
24427
24620
  }
24428
24621
  }
24429
- var SERVER_PID_FILE = join7(
24622
+ var SERVER_PID_FILE = join8(
24430
24623
  RUNTIME_ROOT,
24431
24624
  `server-${sanitize(resolvePluginData() ?? "default")}.pid`
24432
24625
  );
@@ -24552,7 +24745,7 @@ function scheduleHardExit(code = 0) {
24552
24745
  timer.unref?.();
24553
24746
  }
24554
24747
  function resolveTuiStderrLogPath() {
24555
- return process.env.MIXDOG_TUI_STDERR_LOG || join8(process.env.MIXDOG_RUNTIME_ROOT || join8(tmpdir4(), "mixdog"), "mixdog-tui.stderr.log");
24748
+ return process.env.MIXDOG_TUI_STDERR_LOG || join9(process.env.MIXDOG_RUNTIME_ROOT || join9(tmpdir4(), "mixdog"), "mixdog-tui.stderr.log");
24556
24749
  }
24557
24750
  function ansiFg(rgb) {
24558
24751
  const m = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(String(rgb || ""));
@@ -0,0 +1,175 @@
1
+ // Lead TUI busy-input steering queue — disk mirror of in-memory `pending`
2
+ // (same store as manager pending-messages, lead-scoped session key).
3
+ import { randomBytes } from 'crypto';
4
+ import { join } from 'path';
5
+ import { resolvePluginData } from '../../runtime/shared/plugin-paths.mjs';
6
+ import { updateJsonAtomicSync } from '../../runtime/shared/atomic-file.mjs';
7
+ import { promptContentText } from './queue-helpers.mjs';
8
+
9
+ const PENDING_MESSAGES_FILE = 'session-pending-messages.json';
10
+ const PENDING_MESSAGES_MODE = 0o600;
11
+
12
+ function pendingMessagesPath() {
13
+ return join(resolvePluginData(), PENDING_MESSAGES_FILE);
14
+ }
15
+
16
+ function tuiSteeringSessionKey(leadSessionId) {
17
+ if (typeof leadSessionId !== 'string' || !/^[A-Za-z0-9_-]+$/.test(leadSessionId)) return null;
18
+ return `tui_${leadSessionId}`;
19
+ }
20
+
21
+ function newSteeringPersistId() {
22
+ return `ts_${Date.now().toString(36)}_${randomBytes(4).toString('hex')}`;
23
+ }
24
+
25
+ function normalizeTuiSteeringQueueEntry(entry) {
26
+ if (typeof entry === 'string') {
27
+ const text = entry.trim();
28
+ return text || null;
29
+ }
30
+ if (!entry || typeof entry !== 'object') return null;
31
+ if (typeof entry.text === 'string' && entry.text.trim()) {
32
+ const text = entry.text.trim();
33
+ const id = typeof entry.id === 'string' && entry.id.trim() ? entry.id.trim() : null;
34
+ return id ? { id, text } : text;
35
+ }
36
+ return null;
37
+ }
38
+
39
+ function normalizePendingStore(raw) {
40
+ const sessions = raw && typeof raw === 'object' && raw.sessions && typeof raw.sessions === 'object'
41
+ ? raw.sessions
42
+ : {};
43
+ const storeUpdatedAt = Number(raw?.updatedAt) || Date.now();
44
+ const touchedRaw = raw && typeof raw === 'object' && raw.sessionTouchedAt && typeof raw.sessionTouchedAt === 'object'
45
+ ? raw.sessionTouchedAt
46
+ : {};
47
+ const out = { version: 1, updatedAt: storeUpdatedAt, sessions: {}, sessionTouchedAt: {} };
48
+ for (const [sid, value] of Object.entries(sessions)) {
49
+ if (!/^[A-Za-z0-9_-]+$/.test(sid) || !Array.isArray(value)) continue;
50
+ const q = sid.startsWith('tui_')
51
+ ? value.map(normalizeTuiSteeringQueueEntry).filter(Boolean)
52
+ : value
53
+ .map((entry) => {
54
+ if (typeof entry === 'string') return entry;
55
+ if (entry && typeof entry === 'object' && typeof entry.message === 'string') return entry.message;
56
+ return '';
57
+ })
58
+ .filter(Boolean);
59
+ if (q.length > 0) {
60
+ out.sessions[sid] = q;
61
+ const touched = Number(touchedRaw[sid]);
62
+ out.sessionTouchedAt[sid] = Number.isFinite(touched) && touched > 0 ? touched : storeUpdatedAt;
63
+ }
64
+ }
65
+ return out;
66
+ }
67
+
68
+ function touchPendingSessionEntry(next, sessionId, now = Date.now()) {
69
+ if (!next.sessionTouchedAt || typeof next.sessionTouchedAt !== 'object') next.sessionTouchedAt = {};
70
+ next.sessionTouchedAt[sessionId] = now;
71
+ }
72
+
73
+ export function entryPersistText(entry) {
74
+ if (!entry) return '';
75
+ const text = typeof entry.text === 'string' && entry.text.trim()
76
+ ? entry.text.trim()
77
+ : promptContentText(entry.content ?? entry.text ?? '').trim();
78
+ return text;
79
+ }
80
+
81
+ function rowMatchesEntry(row, entry) {
82
+ const persistId = entry?.steeringPersistId;
83
+ if (persistId) {
84
+ return Boolean(row && typeof row === 'object' && row.id === persistId);
85
+ }
86
+ const text = entryPersistText(entry);
87
+ if (!text) return false;
88
+ return row === text;
89
+ }
90
+
91
+ function removePersistRow(q, entry) {
92
+ const idx = q.findIndex((row) => rowMatchesEntry(row, entry));
93
+ if (idx >= 0) q.splice(idx, 1);
94
+ }
95
+
96
+ export function appendTuiSteeringPersist(leadSessionId, entry) {
97
+ const text = entryPersistText(entry);
98
+ if (!text) return;
99
+ const key = tuiSteeringSessionKey(leadSessionId);
100
+ if (!key) return;
101
+ if (!entry.steeringPersistId) entry.steeringPersistId = newSteeringPersistId();
102
+ const record = { id: entry.steeringPersistId, text };
103
+ try {
104
+ updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
105
+ const next = normalizePendingStore(raw);
106
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
107
+ q.push(record);
108
+ next.sessions[key] = q;
109
+ const now = Date.now();
110
+ next.updatedAt = now;
111
+ touchPendingSessionEntry(next, key, now);
112
+ return next;
113
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
114
+ } catch (err) {
115
+ try { process.stderr.write(`[tui] steering-queue append failed sessionId=${leadSessionId}: ${err?.message || err}\n`); } catch {}
116
+ }
117
+ }
118
+
119
+ export function dropTuiSteeringPersist(leadSessionId, entries) {
120
+ const key = tuiSteeringSessionKey(leadSessionId);
121
+ if (!key) return;
122
+ const batch = Array.isArray(entries) ? entries : [];
123
+ if (batch.length === 0) return;
124
+ try {
125
+ updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
126
+ const next = normalizePendingStore(raw);
127
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key].slice() : [];
128
+ if (q.length === 0) return undefined;
129
+ for (const entry of batch) {
130
+ removePersistRow(q, entry);
131
+ }
132
+ if (q.length === 0) {
133
+ delete next.sessions[key];
134
+ if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
135
+ } else {
136
+ next.sessions[key] = q;
137
+ }
138
+ next.updatedAt = Date.now();
139
+ return next;
140
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
141
+ } catch (err) {
142
+ try { process.stderr.write(`[tui] steering-queue drop failed sessionId=${leadSessionId}: ${err?.message || err}\n`); } catch {}
143
+ }
144
+ }
145
+
146
+ function drainedRowToRestore(row) {
147
+ if (typeof row === 'string') {
148
+ return { text: row, steeringPersistId: null };
149
+ }
150
+ if (row && typeof row === 'object' && typeof row.text === 'string') {
151
+ return { text: row.text, steeringPersistId: typeof row.id === 'string' ? row.id : null };
152
+ }
153
+ return null;
154
+ }
155
+
156
+ export function drainTuiSteeringPersist(leadSessionId) {
157
+ const key = tuiSteeringSessionKey(leadSessionId);
158
+ if (!key) return [];
159
+ let drained = [];
160
+ try {
161
+ updateJsonAtomicSync(pendingMessagesPath(), (raw) => {
162
+ const next = normalizePendingStore(raw);
163
+ const q = Array.isArray(next.sessions[key]) ? next.sessions[key] : [];
164
+ drained = q.map(drainedRowToRestore).filter(Boolean);
165
+ if (drained.length === 0) return undefined;
166
+ delete next.sessions[key];
167
+ if (next.sessionTouchedAt) delete next.sessionTouchedAt[key];
168
+ next.updatedAt = Date.now();
169
+ return next;
170
+ }, { compact: true, lock: true, mode: PENDING_MESSAGES_MODE, fsync: false });
171
+ } catch (err) {
172
+ try { process.stderr.write(`[tui] steering-queue drain failed sessionId=${leadSessionId}: ${err?.message || err}\n`); } catch {}
173
+ }
174
+ return drained;
175
+ }
@@ -79,6 +79,11 @@ import {
79
79
  import { createToolApproval } from './engine/tool-approval.mjs';
80
80
  import { createToolCardResults } from './engine/tool-card-results.mjs';
81
81
  import { createAgentJobFeed } from './engine/agent-job-feed.mjs';
82
+ import {
83
+ appendTuiSteeringPersist,
84
+ dropTuiSteeringPersist,
85
+ drainTuiSteeringPersist,
86
+ } from './engine/tui-steering-persist.mjs';
82
87
 
83
88
  // Source tests resolve from src/tui/engine.mjs; the built bundle resolves from
84
89
  // src/tui/dist/index.mjs.
@@ -1512,6 +1517,20 @@ export async function createEngineSession({
1512
1517
  let draining = false;
1513
1518
  let activePromptRestore = null;
1514
1519
 
1520
+ const leadSessionId = () => runtime.id;
1521
+
1522
+ function shouldMirrorSteeringEntry(entry) {
1523
+ return isQueuedEntryEditable(entry) && !isSlashQueuedEntry(entry);
1524
+ }
1525
+
1526
+ function commitSteeringQueueEntries(entries) {
1527
+ callCommitCallbacks(entries);
1528
+ const mirrored = (Array.isArray(entries) ? entries : []).filter(
1529
+ (entry) => shouldMirrorSteeringEntry(entry) && !entry.steeringPersistRestored,
1530
+ );
1531
+ if (mirrored.length > 0) dropTuiSteeringPersist(leadSessionId(), mirrored);
1532
+ }
1533
+
1515
1534
  function makeQueueEntry(text, options = {}) {
1516
1535
  const mode = options.mode || 'prompt';
1517
1536
  const priority = options.priority || defaultQueuePriority(mode);
@@ -1528,6 +1547,8 @@ export async function createEngineSession({
1528
1547
  key: options.key || null,
1529
1548
  skipSlashCommands: options.skipSlashCommands === true,
1530
1549
  displayText: mode === 'task-notification' ? notificationDisplayText(displayText) : String(displayText || ''),
1550
+ steeringPersistId: options.steeringPersistId || null,
1551
+ steeringPersistRestored: options.steeringPersistRestored === true,
1531
1552
  };
1532
1553
  }
1533
1554
 
@@ -1619,7 +1640,7 @@ export async function createEngineSession({
1619
1640
  displayText: batch.map((entry) => entry.text).filter((text) => String(text || '').trim()).join('\n'),
1620
1641
  pastedImages: batchPastedImages,
1621
1642
  pastedTexts: batchPastedTexts,
1622
- onCommitted: () => callCommitCallbacks(batch),
1643
+ onCommitted: () => commitSteeringQueueEntries(batch),
1623
1644
  submittedIds: [...ids],
1624
1645
  restorable: nonEditable.length === 0,
1625
1646
  requeueOnAbort: nonEditable,
@@ -1661,6 +1682,9 @@ export async function createEngineSession({
1661
1682
  pendingNotificationKeys.add(entry.key);
1662
1683
  }
1663
1684
  pending.push(entry);
1685
+ if (state.busy && shouldMirrorSteeringEntry(entry)) {
1686
+ appendTuiSteeringPersist(leadSessionId(), entry);
1687
+ }
1664
1688
  if (isQueuedEntryVisible(entry)) set({ queued: [...state.queued, entry] });
1665
1689
  void drain();
1666
1690
  return true;
@@ -1687,10 +1711,26 @@ export async function createEngineSession({
1687
1711
  if (Array.isArray(entry?.content)) return entry.content.length > 0;
1688
1712
  return String(entry?.content ?? '').trim().length > 0;
1689
1713
  });
1690
- callCommitCallbacks(batch);
1714
+ commitSteeringQueueEntries(batch);
1691
1715
  return out;
1692
1716
  }
1693
1717
 
1718
+ function restoreLeadSteeringFromDisk() {
1719
+ const rows = drainTuiSteeringPersist(leadSessionId());
1720
+ if (!rows.length) return;
1721
+ const restored = [];
1722
+ for (const row of rows) {
1723
+ const entry = makeQueueEntry(row.text, {
1724
+ steeringPersistRestored: true,
1725
+ steeringPersistId: row.steeringPersistId || undefined,
1726
+ });
1727
+ pending.push(entry);
1728
+ if (isQueuedEntryVisible(entry)) restored.push(entry);
1729
+ }
1730
+ if (restored.length > 0) set({ queued: [...state.queued, ...restored] });
1731
+ void drain();
1732
+ }
1733
+
1694
1734
  async function autoClearBeforeSubmit() {
1695
1735
  const cfg = autoClearState();
1696
1736
  const now = Date.now();
@@ -1858,6 +1898,8 @@ export async function createEngineSession({
1858
1898
  return state.stats;
1859
1899
  };
1860
1900
 
1901
+ restoreLeadSteeringFromDisk();
1902
+
1861
1903
  return {
1862
1904
  getState: () => state,
1863
1905
  patchItem,
@@ -2826,11 +2868,13 @@ export async function createEngineSession({
2826
2868
  ...routeState(),
2827
2869
  stats: { ...state.stats },
2828
2870
  });
2871
+ restoreLeadSteeringFromDisk();
2829
2872
  return true;
2830
2873
  } finally {
2831
2874
  set({ commandBusy: false, commandStatus: null });
2832
2875
  }
2833
2876
  },
2877
+
2834
2878
  dispose: async (reason = 'cli-react-exit', options = {}) => {
2835
2879
  if (disposed) return;
2836
2880
  disposed = true;
@@ -344,14 +344,12 @@ function renderNativeStatusline({
344
344
  addL2(`${spin} ${B}${label}${R}${elapsedSuffix(shellStatus.elapsedLabel)}`);
345
345
  }
346
346
  // Memory cycle segment — single unified "Memory" wording for all states:
347
- // running -> "⠋ Memory · 12s"; backlog warning -> "⠋ Memory · backlog 512"
348
- // (yellow). Nothing when idle (no L2 pollution).
347
+ // running -> "⠋ Memory · 12s". Backlog is intentionally NOT rendered
348
+ // (owner preference: cycle-health WARN logs cover it); nothing when idle.
349
349
  const memStatus = memoryCycleStatus();
350
350
  if (memStatus?.kind === 'running') {
351
351
  const elapsed = formatElapsed(Date.now() - memStatus.startedAt);
352
352
  addL2(`${spin} ${B}Memory${R}${elapsedSuffix(elapsed)}`);
353
- } else if (memStatus?.kind === 'backlog') {
354
- addL2(`${spin} ${B}Memory${R} ${D}·${R} ${YLW}backlog ${memStatus.pending}${R}`);
355
353
  }
356
354
  const l1 = l1Parts.join(sep) || 'mixdog';
357
355
  const l2 = l2Parts.join(sep);