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.
- package/package.json +1 -1
- package/src/defaults/cycle3-review-prompt.md +14 -0
- package/src/defaults/memory-promote-prompt.md +9 -9
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +132 -11
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -1
- package/src/runtime/memory/index.mjs +7 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +108 -4
- package/src/runtime/memory/lib/cycle-scheduler.mjs +46 -14
- package/src/runtime/memory/lib/memory-cycle1.mjs +166 -23
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +15 -8
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +37 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +17 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +106 -6
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-handlers.mjs +1 -0
- package/src/tui/dist/index.mjs +283 -28
- package/src/tui/engine/tui-steering-persist.mjs +175 -0
- package/src/tui/engine.mjs +137 -4
- package/src/ui/statusline.mjs +2 -4
package/src/tui/dist/index.mjs
CHANGED
|
@@ -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
|
|
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 (
|
|
22971
|
-
|
|
22972
|
-
}
|
|
22973
|
-
|
|
22974
|
-
|
|
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: () =>
|
|
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
|
-
|
|
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
|
|
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) :
|
|
24405
|
-
var OWNER_DIR =
|
|
24406
|
-
var ACTIVE_INSTANCE_FILE =
|
|
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 =
|
|
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 ||
|
|
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
|
+
}
|