mixdog 0.9.13 → 0.9.15

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,12 +21886,185 @@ 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 = (() => {
21892
22052
  const value = Number(process.env.MIXDOG_TOOL_APPROVAL_TIMEOUT_MS);
21893
22053
  return Number.isFinite(value) && value > 0 ? Math.max(1e3, Math.round(value)) : 12e4;
21894
22054
  })();
22055
+ var LEAD_TURN_TIMEOUT_MS = (() => {
22056
+ const value = Number(process.env.MIXDOG_LEAD_TURN_TIMEOUT_MS);
22057
+ return Number.isFinite(value) && value > 0 ? Math.max(1e4, Math.round(value)) : 30 * 6e4;
22058
+ })();
22059
+ var TUI_DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_TUI_DEBUG || ""));
22060
+ var tuiDebug = (msg) => {
22061
+ if (!TUI_DEBUG) return;
22062
+ try {
22063
+ process.stderr.write(`[tui] ${msg}
22064
+ `);
22065
+ } catch {
22066
+ }
22067
+ };
21895
22068
  var _idSeq = 0;
21896
22069
  var nextId = () => `it_${++_idSeq}`;
21897
22070
  async function createEngineSession({
@@ -22327,6 +22500,7 @@ async function createEngineSession({
22327
22500
  async function runTurn(userText, options = {}) {
22328
22501
  const turnIndex = state.stats.turns || 0;
22329
22502
  const startedAt2 = Date.now();
22503
+ const turnEpoch = ++leadTurnEpoch;
22330
22504
  const inputBaseline = state.stats.inputTokens;
22331
22505
  const outputBaseline = state.stats.outputTokens;
22332
22506
  const submittedIds = Array.isArray(options.submittedIds) ? options.submittedIds : [];
@@ -22346,6 +22520,44 @@ async function createEngineSession({
22346
22520
  set({ busy: true, lastTurn: null, spinner: { active: true, verb: pickVerb(turnIndex), startedAt: startedAt2, responseLength: 0, inputTokens: 0, outputTokens: 0, mode: "requesting" } });
22347
22521
  let assistantText = "";
22348
22522
  let currentAssistantId = null;
22523
+ tuiDebug(`runTurn start turn=${turnIndex} pending=${pending.length} timeoutMs=${LEAD_TURN_TIMEOUT_MS}`);
22524
+ let watchdogTripped = false;
22525
+ let watchdogTimer = null;
22526
+ let watchdogGraceTimer = null;
22527
+ const clearWatchdog = () => {
22528
+ if (watchdogTimer) {
22529
+ clearTimeout(watchdogTimer);
22530
+ watchdogTimer = null;
22531
+ }
22532
+ if (watchdogGraceTimer) {
22533
+ clearTimeout(watchdogGraceTimer);
22534
+ watchdogGraceTimer = null;
22535
+ }
22536
+ };
22537
+ watchdogTimer = setTimeout(() => {
22538
+ if (disposed) return;
22539
+ watchdogTripped = true;
22540
+ const elapsed = Date.now() - startedAt2;
22541
+ tuiDebug(`runTurn WATCHDOG TRIP turn=${turnIndex} elapsedMs=${elapsed} \u2014 aborting stuck turn`);
22542
+ pushNotice(`Turn timed out after ${Math.round(elapsed / 1e3)}s \u2014 aborting stuck request. Input is available again.`, "warn", { transcript: true });
22543
+ try {
22544
+ runtime.abort("cli-react-abort-watchdog");
22545
+ } catch {
22546
+ }
22547
+ watchdogGraceTimer = setTimeout(() => {
22548
+ if (disposed) return;
22549
+ if (!state.busy) return;
22550
+ if (leadTurnEpoch !== turnEpoch) return;
22551
+ tuiDebug(`runTurn WATCHDOG FORCE-RELEASE turn=${turnIndex} \u2014 abort unwind starved`);
22552
+ leadTurnEpoch++;
22553
+ set({ busy: false, spinner: null, thinking: null, lastTurn: null });
22554
+ activePromptRestore = null;
22555
+ if (draining) draining = false;
22556
+ if (pending.length > 0) void drain();
22557
+ }, 5e3);
22558
+ watchdogGraceTimer.unref?.();
22559
+ }, LEAD_TURN_TIMEOUT_MS);
22560
+ watchdogTimer.unref?.();
22349
22561
  let currentAssistantText = "";
22350
22562
  let thinkingText = "";
22351
22563
  let thinkingStartedAt = 0;
@@ -22948,6 +23160,8 @@ async function createEngineSession({
22948
23160
  }
22949
23161
  } finally {
22950
23162
  denyAllToolApprovals(cancelled ? "turn cancelled" : "turn finished");
23163
+ clearWatchdog();
23164
+ const isStaleUnwind = leadTurnEpoch !== turnEpoch;
22951
23165
  if (deferredEntries.length) {
22952
23166
  const last = deferredEntries[deferredEntries.length - 1];
22953
23167
  if (last) flushDeferredUpTo(last);
@@ -22956,7 +23170,7 @@ async function createEngineSession({
22956
23170
  flushDeferredBeforeImmediatePush = null;
22957
23171
  const producedTranscriptItem = state.items.length > itemsAtTurnStart;
22958
23172
  const reclaimed = cancelled && activePromptRestore?.reclaimed === true;
22959
- activePromptRestore = null;
23173
+ if (!isStaleUnwind) activePromptRestore = null;
22960
23174
  closeThinkingSegment();
22961
23175
  const elapsedMs = Date.now() - startedAt2;
22962
23176
  const thinkingElapsedMs = thinkingStartedAt ? accumulatedThinkingMs : 0;
@@ -22967,31 +23181,48 @@ async function createEngineSession({
22967
23181
  const resultContent = askResult?.content != null ? String(askResult.content).trim() : "";
22968
23182
  const assistantOutput = (currentAssistantText || assistantText || "").trim();
22969
23183
  const isNoOpTurn = turnFinishedNormally && !cancelled && toolCards.length === 0 && !resultContent && !assistantOutput && !producedTranscriptItem;
22970
- if (!isNoOpTurn) {
22971
- state.stats.turns = (state.stats.turns || 0) + 1;
22972
- }
22973
- if (!reclaimed && !isNoOpTurn) {
22974
- pushItem({ kind: "turndone", id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
23184
+ if (isStaleUnwind) {
23185
+ tuiDebug(`runTurn STALE UNWIND turn=${turnIndex} \u2014 force-released; skipping shared-state writes`);
23186
+ } else {
23187
+ if (!isNoOpTurn) {
23188
+ state.stats.turns = (state.stats.turns || 0) + 1;
23189
+ }
23190
+ if (!reclaimed && !isNoOpTurn) {
23191
+ pushItem({ kind: "turndone", id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
23192
+ }
23193
+ set({
23194
+ busy: false,
23195
+ spinner: null,
23196
+ thinking: null,
23197
+ lastTurn: null,
23198
+ stats: { ...state.stats },
23199
+ ...routeState(),
23200
+ toolMode: runtime.toolMode,
23201
+ ...agentStatusState({ force: true })
23202
+ });
23203
+ flushDeferredExecutionPendingResumeKick();
22975
23204
  }
22976
- set({
22977
- busy: false,
22978
- spinner: null,
22979
- thinking: null,
22980
- lastTurn: null,
22981
- stats: { ...state.stats },
22982
- ...routeState(),
22983
- toolMode: runtime.toolMode,
22984
- ...agentStatusState({ force: true })
22985
- });
22986
- flushDeferredExecutionPendingResumeKick();
22987
23205
  }
22988
- clearActiveToolSummary();
23206
+ if (leadTurnEpoch === turnEpoch) clearActiveToolSummary();
22989
23207
  _publishedThinkingActive = false;
23208
+ tuiDebug(`runTurn end turn=${turnIndex} status=${cancelled ? "cancelled" : "done"} elapsedMs=${Date.now() - startedAt2}${watchdogTripped ? " watchdogTripped=1" : ""} pending=${pending.length}`);
22990
23209
  return cancelled ? "cancelled" : "done";
22991
23210
  }
22992
23211
  const pending = [];
22993
23212
  let draining = false;
22994
23213
  let activePromptRestore = null;
23214
+ let leadTurnEpoch = 0;
23215
+ const leadSessionId = () => runtime.id;
23216
+ function shouldMirrorSteeringEntry(entry) {
23217
+ return isQueuedEntryEditable(entry) && !isSlashQueuedEntry(entry);
23218
+ }
23219
+ function commitSteeringQueueEntries(entries) {
23220
+ callCommitCallbacks(entries);
23221
+ const mirrored = (Array.isArray(entries) ? entries : []).filter(
23222
+ (entry) => shouldMirrorSteeringEntry(entry) && !entry.steeringPersistRestored
23223
+ );
23224
+ if (mirrored.length > 0) dropTuiSteeringPersist(leadSessionId(), mirrored);
23225
+ }
22995
23226
  function makeQueueEntry(text, options = {}) {
22996
23227
  const mode = options.mode || "prompt";
22997
23228
  const priority = options.priority || defaultQueuePriority(mode);
@@ -23007,7 +23238,9 @@ async function createEngineSession({
23007
23238
  priority,
23008
23239
  key: options.key || null,
23009
23240
  skipSlashCommands: options.skipSlashCommands === true,
23010
- displayText: mode === "task-notification" ? notificationDisplayText(displayText) : String(displayText || "")
23241
+ displayText: mode === "task-notification" ? notificationDisplayText(displayText) : String(displayText || ""),
23242
+ steeringPersistId: options.steeringPersistId || null,
23243
+ steeringPersistRestored: options.steeringPersistRestored === true
23011
23244
  };
23012
23245
  }
23013
23246
  function removeQueuedEntries(entries) {
@@ -23079,6 +23312,7 @@ async function createEngineSession({
23079
23312
  const batch = dequeueQueueBatch("later", { limit: firstBatch ? 1 : Infinity });
23080
23313
  firstBatch = false;
23081
23314
  if (batch.length === 0) break;
23315
+ tuiDebug(`busy-queue drain batch=${batch.length} remaining=${pending.length}`);
23082
23316
  const ids = new Set(batch.map((e) => e.id));
23083
23317
  const merged = mergePromptContents(batch);
23084
23318
  for (const entry of batch) {
@@ -23092,7 +23326,7 @@ async function createEngineSession({
23092
23326
  displayText: batch.map((entry) => entry.text).filter((text) => String(text || "").trim()).join("\n"),
23093
23327
  pastedImages: batchPastedImages,
23094
23328
  pastedTexts: batchPastedTexts,
23095
- onCommitted: () => callCommitCallbacks(batch),
23329
+ onCommitted: () => commitSteeringQueueEntries(batch),
23096
23330
  submittedIds: [...ids],
23097
23331
  restorable: nonEditable.length === 0,
23098
23332
  requeueOnAbort: nonEditable
@@ -23122,7 +23356,11 @@ async function createEngineSession({
23122
23356
  pendingNotificationKeys.add(entry.key);
23123
23357
  }
23124
23358
  pending.push(entry);
23359
+ if (state.busy && shouldMirrorSteeringEntry(entry)) {
23360
+ appendTuiSteeringPersist(leadSessionId(), entry);
23361
+ }
23125
23362
  if (isQueuedEntryVisible(entry)) set({ queued: [...state.queued, entry] });
23363
+ if (state.busy) tuiDebug(`busy-queue enqueue mode=${entry.mode} pending=${pending.length}`);
23126
23364
  void drain();
23127
23365
  return true;
23128
23366
  }
@@ -23138,9 +23376,24 @@ async function createEngineSession({
23138
23376
  if (Array.isArray(entry?.content)) return entry.content.length > 0;
23139
23377
  return String(entry?.content ?? "").trim().length > 0;
23140
23378
  });
23141
- callCommitCallbacks(batch);
23379
+ commitSteeringQueueEntries(batch);
23142
23380
  return out;
23143
23381
  }
23382
+ function restoreLeadSteeringFromDisk() {
23383
+ const rows = drainTuiSteeringPersist(leadSessionId());
23384
+ if (!rows.length) return;
23385
+ const restored = [];
23386
+ for (const row of rows) {
23387
+ const entry = makeQueueEntry(row.text, {
23388
+ steeringPersistRestored: true,
23389
+ steeringPersistId: row.steeringPersistId || void 0
23390
+ });
23391
+ pending.push(entry);
23392
+ if (isQueuedEntryVisible(entry)) restored.push(entry);
23393
+ }
23394
+ if (restored.length > 0) set({ queued: [...state.queued, ...restored] });
23395
+ void drain();
23396
+ }
23144
23397
  async function autoClearBeforeSubmit() {
23145
23398
  const cfg = autoClearState();
23146
23399
  const now = Date.now();
@@ -23291,6 +23544,7 @@ async function createEngineSession({
23291
23544
  syncContextStats({ allowEstimated: true });
23292
23545
  return state.stats;
23293
23546
  };
23547
+ restoreLeadSteeringFromDisk();
23294
23548
  return {
23295
23549
  getState: () => state,
23296
23550
  patchItem,
@@ -24213,6 +24467,7 @@ async function createEngineSession({
24213
24467
  ...routeState(),
24214
24468
  stats: { ...state.stats }
24215
24469
  });
24470
+ restoreLeadSteeringFromDisk();
24216
24471
  return true;
24217
24472
  } finally {
24218
24473
  set({ commandBusy: false, commandStatus: null });
@@ -24392,7 +24647,7 @@ function installProcessSignalCleanup({
24392
24647
 
24393
24648
  // src/runtime/channels/lib/runtime-paths.mjs
24394
24649
  import { tmpdir as tmpdir3 } from "os";
24395
- import { basename as basename4, join as join7, resolve as resolve4 } from "path";
24650
+ import { basename as basename4, join as join8, resolve as resolve4 } from "path";
24396
24651
 
24397
24652
  // src/runtime/channels/lib/state-file.mjs
24398
24653
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync6, unlinkSync as unlinkSync4, writeFileSync as writeFileSync4 } from "fs";
@@ -24401,9 +24656,9 @@ function ensureDir2(dirPath) {
24401
24656
  }
24402
24657
 
24403
24658
  // 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");
24659
+ var RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT ? resolve4(process.env.MIXDOG_RUNTIME_ROOT) : join8(tmpdir3(), "mixdog");
24660
+ var OWNER_DIR = join8(RUNTIME_ROOT, "owners");
24661
+ var ACTIVE_INSTANCE_FILE = join8(RUNTIME_ROOT, "active-instance.json");
24407
24662
  var RUNTIME_STALE_TTL = 24 * 60 * 60 * 1e3;
24408
24663
  var STATUS_FILE_TTL = 6 * 60 * 60 * 1e3;
24409
24664
  var TMP_FILE_TTL = 60 * 60 * 1e3;
@@ -24426,7 +24681,7 @@ function touchUiHeartbeat(instanceId) {
24426
24681
  } catch {
24427
24682
  }
24428
24683
  }
24429
- var SERVER_PID_FILE = join7(
24684
+ var SERVER_PID_FILE = join8(
24430
24685
  RUNTIME_ROOT,
24431
24686
  `server-${sanitize(resolvePluginData() ?? "default")}.pid`
24432
24687
  );
@@ -24552,7 +24807,7 @@ function scheduleHardExit(code = 0) {
24552
24807
  timer.unref?.();
24553
24808
  }
24554
24809
  function resolveTuiStderrLogPath() {
24555
- return process.env.MIXDOG_TUI_STDERR_LOG || join8(process.env.MIXDOG_RUNTIME_ROOT || join8(tmpdir4(), "mixdog"), "mixdog-tui.stderr.log");
24810
+ return process.env.MIXDOG_TUI_STDERR_LOG || join9(process.env.MIXDOG_RUNTIME_ROOT || join9(tmpdir4(), "mixdog"), "mixdog-tui.stderr.log");
24556
24811
  }
24557
24812
  function ansiFg(rgb) {
24558
24813
  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
+ }