mixdog 0.9.12 → 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.
- package/package.json +1 -1
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +11 -7
- package/src/defaults/cycle3-review-prompt.md +14 -0
- package/src/defaults/memory-promote-prompt.md +9 -9
- package/src/lib/rules-builder.cjs +15 -11
- package/src/mixdog-session-runtime.mjs +10 -0
- package/src/rules/agent/00-common.md +5 -17
- package/src/rules/agent/00-core.md +21 -0
- package/src/rules/lead/lead-brief.md +7 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +39 -2
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +0 -25
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +120 -3
- package/src/runtime/agent/orchestrator/agent-trace.mjs +43 -1
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +48 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +42 -4
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +24 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +6 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +61 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +57 -2
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +40 -4
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +0 -62
- package/src/runtime/agent/orchestrator/session/compact.mjs +0 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +12 -9
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +48 -157
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +6 -23
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +0 -13
- package/src/runtime/agent/orchestrator/session/loop.mjs +32 -77
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +33 -45
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +132 -11
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +7 -6
- package/src/runtime/agent/orchestrator/session/manager.mjs +30 -15
- package/src/runtime/agent/orchestrator/stall-policy.mjs +16 -0
- package/src/runtime/channels/index.mjs +28 -1
- package/src/runtime/channels/lib/memory-client.mjs +200 -15
- package/src/runtime/memory/index.mjs +18 -13
- package/src/runtime/memory/lib/core-memory-store.mjs +108 -4
- package/src/runtime/memory/lib/cycle-scheduler.mjs +52 -18
- package/src/runtime/memory/lib/memory-cycle1.mjs +166 -23
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +37 -25
- 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-embed.mjs +35 -1
- package/src/runtime/memory/lib/memory.mjs +7 -1
- package/src/runtime/memory/lib/query-handlers.mjs +1 -0
- package/src/standalone/agent-tool.mjs +89 -7
- package/src/tui/dist/index.mjs +203 -10
- package/src/tui/engine/tui-steering-persist.mjs +175 -0
- package/src/tui/engine.mjs +46 -2
- package/src/ui/statusline.mjs +2 -4
- package/src/workflows/default/WORKFLOW.md +2 -0
- package/src/workflows/sequential/WORKFLOW.md +2 -0
- package/src/workflows/solo/WORKFLOW.md +2 -0
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,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: () =>
|
|
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
|
-
|
|
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
|
|
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) :
|
|
24405
|
-
var OWNER_DIR =
|
|
24406
|
-
var ACTIVE_INSTANCE_FILE =
|
|
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 =
|
|
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 ||
|
|
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
|
+
}
|
package/src/tui/engine.mjs
CHANGED
|
@@ -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: () =>
|
|
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
|
-
|
|
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;
|
package/src/ui/statusline.mjs
CHANGED
|
@@ -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"
|
|
348
|
-
// (
|
|
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);
|
|
@@ -17,6 +17,8 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
|
|
|
17
17
|
failed at least once.
|
|
18
18
|
|
|
19
19
|
1. Plan — discuss the request with the user, form a plan, wait for approval.
|
|
20
|
+
Only an explicit go-ahead ("do it", "proceed") is approval. Agreement with
|
|
21
|
+
a diagnosis, or pointing out a problem, is NOT execution approval.
|
|
20
22
|
2. Delegate — split into the maximum number of independent scopes.
|
|
21
23
|
- PARALLEL across independent scopes by default (implementation, analysis,
|
|
22
24
|
review, debugging alike). Spawn every scope in the SAME turn. Shared or
|
|
@@ -21,6 +21,8 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
|
|
|
21
21
|
failed at least once.
|
|
22
22
|
|
|
23
23
|
1. Plan — discuss the request with the user, form a plan, wait for approval.
|
|
24
|
+
Only an explicit go-ahead is approval; diagnosis agreement is not. When
|
|
25
|
+
ambiguous, restate the plan and ask before executing.
|
|
24
26
|
2. Delegate — split into ordered scopes and hand off ONE AT A TIME.
|
|
25
27
|
- NO PARALLEL, NO FAN-OUT. Even when scopes are independent, spawn exactly
|
|
26
28
|
one agent per turn; never multiple in the same turn. Wait for the current
|
|
@@ -9,6 +9,8 @@ agents:
|
|
|
9
9
|
|
|
10
10
|
1. Plan — Lead discusses the request with the user, forms a plan, and waits for
|
|
11
11
|
approval before execution.
|
|
12
|
+
Only an explicit go-ahead is approval; diagnosis agreement is not. When
|
|
13
|
+
ambiguous, restate the plan and ask before executing.
|
|
12
14
|
2. Execute — Lead performs all implementation, research, debugging, review, and
|
|
13
15
|
verification work directly. Delegation to any agent is forbidden.
|
|
14
16
|
3. Verify — Lead checks the result directly, fixes issues directly, and repeats
|