mixdog 0.9.10 → 0.9.12
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/agent-tag-reuse-smoke.mjs +12 -8
- package/scripts/bench/cache-probe-tasks.json +8 -0
- package/scripts/tool-smoke.mjs +109 -0
- package/src/defaults/mixdog-config.template.json +1 -0
- package/src/mixdog-session-runtime.mjs +46 -0
- package/src/runtime/agent/orchestrator/config.mjs +30 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +160 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +25 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +24 -9
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +18 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +79 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +1 -22
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +29 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +4 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +30 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +333 -14
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +129 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +58 -18
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +56 -83
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +26 -56
- package/src/runtime/channels/lib/webhook/deliveries.mjs +4 -1
- package/src/session-runtime/settings-api.mjs +13 -0
- package/src/session-runtime/tool-catalog.mjs +35 -7
- package/src/session-runtime/tool-defs.mjs +28 -0
- package/src/standalone/agent-tool/render.mjs +2 -0
- package/src/standalone/agent-tool.mjs +28 -6
- package/src/standalone/memory-runtime-proxy.mjs +85 -21
- package/src/tui/App.jsx +20 -1
- package/src/tui/app/extension-pickers.mjs +6 -3
- package/src/tui/dist/index.mjs +63 -16
- package/src/tui/engine.mjs +57 -12
|
@@ -1440,16 +1440,38 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1440
1440
|
|
|
1441
1441
|
// True when a tag has a lingering worker-index / role trace but no live
|
|
1442
1442
|
// session in this terminal (finished worker still inside the reap grace window).
|
|
1443
|
+
function terminalWorkerRowForTag(tag, context = {}) {
|
|
1444
|
+
const value = clean(tag);
|
|
1445
|
+
if (!value) return null;
|
|
1446
|
+
return readWorkerRows(context).find((row) => {
|
|
1447
|
+
if (clean(row.tag) !== value) return false;
|
|
1448
|
+
if (!isTerminalWorkerStatus(row.status || row.stage)) return false;
|
|
1449
|
+
if (getLiveSession(clean(row.sessionId))) return false;
|
|
1450
|
+
return true;
|
|
1451
|
+
}) || null;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1443
1454
|
function hasTerminalTrace(tag, context = {}) {
|
|
1444
1455
|
const value = clean(tag);
|
|
1445
1456
|
if (!value || value.startsWith('sess_')) return false;
|
|
1446
1457
|
if (resolveTag(value, context, { excludeTerminalTraces: true })) return false; // live -> reuse, not trace
|
|
1447
|
-
|
|
1448
|
-
return readWorkerRows(context).some((row) => clean(row.tag) === value);
|
|
1458
|
+
return Boolean(terminalWorkerRowForTag(value, context));
|
|
1449
1459
|
}
|
|
1450
1460
|
|
|
1451
|
-
function
|
|
1452
|
-
|
|
1461
|
+
function reapTerminalTraceForTag(tag, context = {}) {
|
|
1462
|
+
const value = clean(tag);
|
|
1463
|
+
if (!value || value.startsWith('sess_')) return false;
|
|
1464
|
+
if (!hasTerminalTrace(value, context)) return false;
|
|
1465
|
+
refreshTagsFromSessions({ context });
|
|
1466
|
+
let sessionId = tags.get(value) || '';
|
|
1467
|
+
if (!sessionId) {
|
|
1468
|
+
const row = readWorkerRows(context).find((item) => clean(item.tag) === value);
|
|
1469
|
+
sessionId = clean(row?.sessionId) || '';
|
|
1470
|
+
}
|
|
1471
|
+
if (sessionId) cancelReap(sessionId);
|
|
1472
|
+
forgetTag(value);
|
|
1473
|
+
removeWorkerRow({ tag: value, sessionId });
|
|
1474
|
+
return true;
|
|
1453
1475
|
}
|
|
1454
1476
|
|
|
1455
1477
|
async function execute(args = {}, context = {}) {
|
|
@@ -1511,7 +1533,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1511
1533
|
// Explicit-tag spawn priority (auto nextTag always creates a fresh session):
|
|
1512
1534
|
// 1) live + busy -> queue the prompt (reuse)
|
|
1513
1535
|
// 2) live + idle -> continue existing session (reuse)
|
|
1514
|
-
// 3) lingering terminal trace ->
|
|
1536
|
+
// 3) lingering terminal trace -> reap trace and fresh spawn under same tag
|
|
1515
1537
|
// 4) genuinely new tag -> fresh deferred spawn
|
|
1516
1538
|
const explicitTag = clean(args.tag);
|
|
1517
1539
|
if (explicitTag) {
|
|
@@ -1533,7 +1555,7 @@ export function createStandaloneAgent({ cfgMod, reg, mgr, dataDir, cwd: defaultC
|
|
|
1533
1555
|
return dispatchToExistingSession(prepared, notifyContext, { reused: true });
|
|
1534
1556
|
}
|
|
1535
1557
|
if (hasTerminalTrace(explicitTag, scopedContext)) {
|
|
1536
|
-
|
|
1558
|
+
reapTerminalTraceForTag(explicitTag, scopedContext);
|
|
1537
1559
|
}
|
|
1538
1560
|
}
|
|
1539
1561
|
const job = startDeferredSpawnJob(args, callerCwd, context, notifyContext);
|
|
@@ -54,6 +54,38 @@ function delay(ms) {
|
|
|
54
54
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
const TRANSIENT_MEMORY_RPC_BACKOFF_MS = 400;
|
|
58
|
+
|
|
59
|
+
function isConnResetLikeError(err) {
|
|
60
|
+
const code = String(err?.code || '');
|
|
61
|
+
const msg = String(err?.message || err || '');
|
|
62
|
+
return code === 'ECONNRESET'
|
|
63
|
+
|| code === 'ECONNREFUSED'
|
|
64
|
+
|| /ECONNRESET|socket hang up/i.test(msg);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isMemoryWorkerNotReadyError(err) {
|
|
68
|
+
const msg = String(err?.message || err || '');
|
|
69
|
+
return /memory worker exited before ready|memory worker ready timeout|memory runtime did not become ready|memory worker degraded/i.test(msg);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function isTransientMemoryRpcError(err) {
|
|
73
|
+
return isConnResetLikeError(err) || isMemoryWorkerNotReadyError(err);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isMemoryReadOnlyToolCall(name, args = {}) {
|
|
77
|
+
const tool = String(name || '').trim();
|
|
78
|
+
if (tool === 'recall' || tool === 'search_memories') return true;
|
|
79
|
+
if (tool !== 'memory') return false;
|
|
80
|
+
const action = String(args?.action || '').trim();
|
|
81
|
+
if (action === 'status') return true;
|
|
82
|
+
if (action === 'core') {
|
|
83
|
+
const op = String(args?.op || '').trim();
|
|
84
|
+
return op === 'list' || op === 'candidates';
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
57
89
|
function requestJson({ port, method = 'GET', path = '/', body = null, timeoutMs = 10_000, headers = {} }) {
|
|
58
90
|
return new Promise((resolvePromise, reject) => {
|
|
59
91
|
const payload = body == null ? null : JSON.stringify(body);
|
|
@@ -116,6 +148,33 @@ export function createStandaloneMemoryRuntime({
|
|
|
116
148
|
let child = null;
|
|
117
149
|
let nextCallId = 1;
|
|
118
150
|
|
|
151
|
+
function invalidateMemoryRuntimeAfterTransient(err) {
|
|
152
|
+
portCache = null;
|
|
153
|
+
if (!isMemoryWorkerNotReadyError(err)) return;
|
|
154
|
+
startPromise = null;
|
|
155
|
+
const proc = child;
|
|
156
|
+
child = null;
|
|
157
|
+
if (!proc || proc.killed) return;
|
|
158
|
+
try { proc.kill(); } catch {}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function shouldRetryMemoryRpcError(err, { readOnlyRpc = false } = {}) {
|
|
162
|
+
if (isMemoryWorkerNotReadyError(err)) return true;
|
|
163
|
+
if (readOnlyRpc && isConnResetLikeError(err)) return true;
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function withTransientMemoryRpcRetry(run, { readOnlyRpc = false } = {}) {
|
|
168
|
+
try {
|
|
169
|
+
return await run();
|
|
170
|
+
} catch (err) {
|
|
171
|
+
if (!shouldRetryMemoryRpcError(err, { readOnlyRpc })) throw err;
|
|
172
|
+
invalidateMemoryRuntimeAfterTransient(err);
|
|
173
|
+
await delay(TRANSIENT_MEMORY_RPC_BACKOFF_MS);
|
|
174
|
+
return await run();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
119
178
|
async function findLivePort({ allowStarting = false } = {}) {
|
|
120
179
|
const active = readActiveInstance();
|
|
121
180
|
const port = parsePort(active?.memory_port);
|
|
@@ -269,31 +328,36 @@ export function createStandaloneMemoryRuntime({
|
|
|
269
328
|
}
|
|
270
329
|
|
|
271
330
|
async function handleToolCall(name, args = {}) {
|
|
272
|
-
|
|
273
|
-
const port = portCache || await findLivePort({ allowStarting: true });
|
|
274
|
-
if (!port) throw new Error('memory runtime is not available');
|
|
331
|
+
const readOnlyRpc = isMemoryReadOnlyToolCall(name, args);
|
|
275
332
|
const callId = `mem_${process.pid}_${nextCallId++}`;
|
|
276
|
-
return await
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
333
|
+
return await withTransientMemoryRpcRetry(async () => {
|
|
334
|
+
await start();
|
|
335
|
+
const port = portCache || await findLivePort({ allowStarting: true });
|
|
336
|
+
if (!port) throw new Error('memory runtime is not available');
|
|
337
|
+
return await requestJson({
|
|
338
|
+
port,
|
|
339
|
+
method: 'POST',
|
|
340
|
+
path: '/api/tool',
|
|
341
|
+
body: { name, arguments: args || {} },
|
|
342
|
+
timeoutMs: Math.max(1000, Number(process.env.MIXDOG_MEMORY_TOOL_TIMEOUT_MS) || 180_000),
|
|
343
|
+
headers: { 'X-Mixdog-Call-Id': callId },
|
|
344
|
+
});
|
|
345
|
+
}, { readOnlyRpc });
|
|
284
346
|
}
|
|
285
347
|
|
|
286
348
|
async function buildSessionCoreMemoryPayload(sessionCwd) {
|
|
287
|
-
await
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
349
|
+
return await withTransientMemoryRpcRetry(async () => {
|
|
350
|
+
await start();
|
|
351
|
+
const port = portCache || await findLivePort({ allowStarting: true });
|
|
352
|
+
if (!port) throw new Error('memory runtime is not available');
|
|
353
|
+
return await requestJson({
|
|
354
|
+
port,
|
|
355
|
+
method: 'POST',
|
|
356
|
+
path: '/session-start/core-memory',
|
|
357
|
+
body: { cwd: sessionCwd || cwd },
|
|
358
|
+
timeoutMs: 30_000,
|
|
359
|
+
});
|
|
360
|
+
}, { readOnlyRpc: true });
|
|
297
361
|
}
|
|
298
362
|
|
|
299
363
|
async function stop() {
|
package/src/tui/App.jsx
CHANGED
|
@@ -386,7 +386,26 @@ export function App({ store, initialStatusLine = '', forceOnboarding = false })
|
|
|
386
386
|
setSettingsPrompt,
|
|
387
387
|
parseMemoryCoreRows,
|
|
388
388
|
});
|
|
389
|
-
const [disabledSkills,
|
|
389
|
+
const [disabledSkills, setDisabledSkillsInner] = useState(() => {
|
|
390
|
+
try {
|
|
391
|
+
const { disabled = [] } = store.getDisabledSkills?.() || {};
|
|
392
|
+
return new Set(disabled);
|
|
393
|
+
} catch {
|
|
394
|
+
return new Set();
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
const setDisabledSkills = useCallback((next) => {
|
|
398
|
+
setDisabledSkillsInner((current) => {
|
|
399
|
+
const base = current instanceof Set ? current : new Set(current);
|
|
400
|
+
const set = typeof next === 'function' ? next(base) : (next instanceof Set ? next : new Set(next));
|
|
401
|
+
try {
|
|
402
|
+
store.setDisabledSkills?.([...set]);
|
|
403
|
+
} catch (e) {
|
|
404
|
+
store.pushNotice(`skill disable persist failed: ${e?.message || e}`, 'error');
|
|
405
|
+
}
|
|
406
|
+
return set;
|
|
407
|
+
});
|
|
408
|
+
}, [store]);
|
|
390
409
|
const {
|
|
391
410
|
openMcpServersPicker,
|
|
392
411
|
openMcpPicker,
|
|
@@ -177,7 +177,10 @@ export function createExtensionPickers({
|
|
|
177
177
|
const next = new Set(disabledSet);
|
|
178
178
|
if (item._enabled) next.add(name); else next.delete(name);
|
|
179
179
|
setDisabledSkills(next);
|
|
180
|
-
store.pushNotice(
|
|
180
|
+
store.pushNotice(
|
|
181
|
+
`skill ${item._enabled ? 'disabled' : 'enabled'}: ${name} (prompt updates next session /clear)`,
|
|
182
|
+
'info',
|
|
183
|
+
);
|
|
181
184
|
openSkillsPicker({ highlightValue: name, disabledOverride: next });
|
|
182
185
|
};
|
|
183
186
|
setPicker({
|
|
@@ -221,7 +224,7 @@ export function createExtensionPickers({
|
|
|
221
224
|
next.delete(skill.name);
|
|
222
225
|
return next;
|
|
223
226
|
});
|
|
224
|
-
store.pushNotice(`skill enabled: ${skill.name}`, 'info');
|
|
227
|
+
store.pushNotice(`skill enabled: ${skill.name} (prompt updates next session /clear)`, 'info');
|
|
225
228
|
openSkillsPicker();
|
|
226
229
|
return;
|
|
227
230
|
}
|
|
@@ -231,7 +234,7 @@ export function createExtensionPickers({
|
|
|
231
234
|
next.add(skill.name);
|
|
232
235
|
return next;
|
|
233
236
|
});
|
|
234
|
-
store.pushNotice(`skill disabled: ${skill.name}`, 'info');
|
|
237
|
+
store.pushNotice(`skill disabled: ${skill.name} (prompt updates next session /clear)`, 'info');
|
|
235
238
|
openSkillsPicker();
|
|
236
239
|
return;
|
|
237
240
|
}
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -13232,7 +13232,10 @@ function createExtensionPickers({
|
|
|
13232
13232
|
if (item._enabled) next.add(name);
|
|
13233
13233
|
else next.delete(name);
|
|
13234
13234
|
setDisabledSkills(next);
|
|
13235
|
-
store.pushNotice(
|
|
13235
|
+
store.pushNotice(
|
|
13236
|
+
`skill ${item._enabled ? "disabled" : "enabled"}: ${name} (prompt updates next session /clear)`,
|
|
13237
|
+
"info"
|
|
13238
|
+
);
|
|
13236
13239
|
openSkillsPicker({ highlightValue: name, disabledOverride: next });
|
|
13237
13240
|
};
|
|
13238
13241
|
setPicker({
|
|
@@ -13275,7 +13278,7 @@ function createExtensionPickers({
|
|
|
13275
13278
|
next.delete(skill.name);
|
|
13276
13279
|
return next;
|
|
13277
13280
|
});
|
|
13278
|
-
store.pushNotice(`skill enabled: ${skill.name}`, "info");
|
|
13281
|
+
store.pushNotice(`skill enabled: ${skill.name} (prompt updates next session /clear)`, "info");
|
|
13279
13282
|
openSkillsPicker();
|
|
13280
13283
|
return;
|
|
13281
13284
|
}
|
|
@@ -13285,7 +13288,7 @@ function createExtensionPickers({
|
|
|
13285
13288
|
next.add(skill.name);
|
|
13286
13289
|
return next;
|
|
13287
13290
|
});
|
|
13288
|
-
store.pushNotice(`skill disabled: ${skill.name}`, "info");
|
|
13291
|
+
store.pushNotice(`skill disabled: ${skill.name} (prompt updates next session /clear)`, "info");
|
|
13289
13292
|
openSkillsPicker();
|
|
13290
13293
|
return;
|
|
13291
13294
|
}
|
|
@@ -18296,7 +18299,26 @@ function App({ store, initialStatusLine = "", forceOnboarding = false }) {
|
|
|
18296
18299
|
setSettingsPrompt,
|
|
18297
18300
|
parseMemoryCoreRows
|
|
18298
18301
|
});
|
|
18299
|
-
const [disabledSkills,
|
|
18302
|
+
const [disabledSkills, setDisabledSkillsInner] = useState8(() => {
|
|
18303
|
+
try {
|
|
18304
|
+
const { disabled = [] } = store.getDisabledSkills?.() || {};
|
|
18305
|
+
return new Set(disabled);
|
|
18306
|
+
} catch {
|
|
18307
|
+
return /* @__PURE__ */ new Set();
|
|
18308
|
+
}
|
|
18309
|
+
});
|
|
18310
|
+
const setDisabledSkills = useCallback6((next) => {
|
|
18311
|
+
setDisabledSkillsInner((current) => {
|
|
18312
|
+
const base = current instanceof Set ? current : new Set(current);
|
|
18313
|
+
const set = typeof next === "function" ? next(base) : next instanceof Set ? next : new Set(next);
|
|
18314
|
+
try {
|
|
18315
|
+
store.setDisabledSkills?.([...set]);
|
|
18316
|
+
} catch (e) {
|
|
18317
|
+
store.pushNotice(`skill disable persist failed: ${e?.message || e}`, "error");
|
|
18318
|
+
}
|
|
18319
|
+
return set;
|
|
18320
|
+
});
|
|
18321
|
+
}, [store]);
|
|
18300
18322
|
const {
|
|
18301
18323
|
openMcpServersPicker,
|
|
18302
18324
|
openMcpPicker,
|
|
@@ -23075,6 +23097,16 @@ async function createEngineSession({
|
|
|
23075
23097
|
restorable: nonEditable.length === 0,
|
|
23076
23098
|
requeueOnAbort: nonEditable
|
|
23077
23099
|
});
|
|
23100
|
+
const scheduledReset = runtime.consumePendingSessionReset?.();
|
|
23101
|
+
if ((scheduledReset === "clear" || scheduledReset === "compact_clear") && turnStatus !== "cancelled" && !state.commandBusy) {
|
|
23102
|
+
await performSessionClear({
|
|
23103
|
+
verb: scheduledReset === "clear" ? "Clearing conversation" : "Compacting and clearing conversation",
|
|
23104
|
+
doneLabel: scheduledReset === "clear" ? "Session cleared" : "Session compacted and cleared",
|
|
23105
|
+
skipLabel: "Session reset skipped",
|
|
23106
|
+
surface: "session-manage",
|
|
23107
|
+
useCompaction: scheduledReset === "compact_clear"
|
|
23108
|
+
});
|
|
23109
|
+
}
|
|
23078
23110
|
if (turnStatus === "cancelled" && pending.length === 0) break;
|
|
23079
23111
|
}
|
|
23080
23112
|
} finally {
|
|
@@ -23118,14 +23150,26 @@ async function createEngineSession({
|
|
|
23118
23150
|
if (!activityAt) lastUserActivityAt = now;
|
|
23119
23151
|
return false;
|
|
23120
23152
|
}
|
|
23153
|
+
return performSessionClear({
|
|
23154
|
+
verb: "Auto-clearing idle conversation",
|
|
23155
|
+
doneLabel: "Auto-clear complete",
|
|
23156
|
+
skipLabel: "Auto-clear skipped",
|
|
23157
|
+
surface: "auto-clear",
|
|
23158
|
+
useCompaction: true
|
|
23159
|
+
});
|
|
23160
|
+
}
|
|
23161
|
+
async function performSessionClear({ verb, doneLabel, skipLabel, surface, useCompaction }) {
|
|
23121
23162
|
autoClearRunning = true;
|
|
23122
23163
|
const startedAt2 = Date.now();
|
|
23123
|
-
set({ commandStatus: { active: true, verb
|
|
23164
|
+
set({ commandBusy: true, commandStatus: { active: true, verb, startedAt: startedAt2, mode: "auto-clear" } });
|
|
23124
23165
|
try {
|
|
23125
23166
|
await new Promise((resolve5) => setTimeout(resolve5, 0));
|
|
23126
|
-
|
|
23127
|
-
|
|
23128
|
-
|
|
23167
|
+
let compactType = null;
|
|
23168
|
+
if (useCompaction) {
|
|
23169
|
+
const compaction = runtime.getCompactionSettings();
|
|
23170
|
+
compactType = compaction.compactType || compaction.type || null;
|
|
23171
|
+
}
|
|
23172
|
+
await runtime.clear(compactType ? { compactType, requireCompactSuccess: true } : {});
|
|
23129
23173
|
resetStats();
|
|
23130
23174
|
clearUiActivityBeforeContextSync();
|
|
23131
23175
|
syncContextStats({ allowEstimated: true });
|
|
@@ -23142,24 +23186,24 @@ async function createEngineSession({
|
|
|
23142
23186
|
pushItem({
|
|
23143
23187
|
kind: "statusdone",
|
|
23144
23188
|
id: nextId(),
|
|
23145
|
-
label:
|
|
23189
|
+
label: doneLabel,
|
|
23146
23190
|
detail: formatElapsedSeconds(Date.now() - startedAt2)
|
|
23147
23191
|
});
|
|
23148
23192
|
return true;
|
|
23149
23193
|
} catch (error) {
|
|
23150
|
-
const message = presentErrorText(error, { surface
|
|
23194
|
+
const message = presentErrorText(error, { surface });
|
|
23151
23195
|
pushItem({
|
|
23152
23196
|
kind: "statusdone",
|
|
23153
23197
|
id: nextId(),
|
|
23154
|
-
label:
|
|
23198
|
+
label: skipLabel,
|
|
23155
23199
|
detail: `conversation kept \xB7 ${message}`
|
|
23156
23200
|
});
|
|
23157
|
-
pushNotice(
|
|
23201
|
+
pushNotice(`${surface} skipped: ${message}`, "error");
|
|
23158
23202
|
return false;
|
|
23159
23203
|
} finally {
|
|
23160
23204
|
lastUserActivityAt = Date.now();
|
|
23161
23205
|
autoClearRunning = false;
|
|
23162
|
-
set({ commandStatus: null });
|
|
23206
|
+
set({ commandBusy: false, commandStatus: null });
|
|
23163
23207
|
void drain();
|
|
23164
23208
|
}
|
|
23165
23209
|
}
|
|
@@ -23256,7 +23300,7 @@ async function createEngineSession({
|
|
|
23256
23300
|
},
|
|
23257
23301
|
submit: (text, options = {}) => {
|
|
23258
23302
|
const t = promptDisplayText(text, options).trim();
|
|
23259
|
-
if (!t
|
|
23303
|
+
if (!t) return false;
|
|
23260
23304
|
const mode = options.mode || "prompt";
|
|
23261
23305
|
const priority = options.priority;
|
|
23262
23306
|
const queueOptions = {
|
|
@@ -23265,11 +23309,12 @@ async function createEngineSession({
|
|
|
23265
23309
|
displayText: promptDisplayText(text, options),
|
|
23266
23310
|
priority
|
|
23267
23311
|
};
|
|
23268
|
-
if (
|
|
23312
|
+
if (autoClearRunning) {
|
|
23269
23313
|
enqueue(text, queueOptions);
|
|
23270
23314
|
return true;
|
|
23271
23315
|
}
|
|
23272
|
-
if (
|
|
23316
|
+
if (state.commandBusy) return false;
|
|
23317
|
+
if (state.busy) {
|
|
23273
23318
|
enqueue(text, queueOptions);
|
|
23274
23319
|
return true;
|
|
23275
23320
|
}
|
|
@@ -23512,6 +23557,8 @@ async function createEngineSession({
|
|
|
23512
23557
|
set({ commandBusy: false });
|
|
23513
23558
|
}
|
|
23514
23559
|
},
|
|
23560
|
+
getDisabledSkills: () => runtime.getDisabledSkills?.() || { disabled: [] },
|
|
23561
|
+
setDisabledSkills: (disabled) => runtime.setDisabledSkills?.(disabled) || { disabled: [] },
|
|
23515
23562
|
skillsStatus: () => {
|
|
23516
23563
|
return runtime.skillsStatus?.() || { cwd: state.cwd, count: 0, skills: [] };
|
|
23517
23564
|
},
|
package/src/tui/engine.mjs
CHANGED
|
@@ -1624,6 +1624,24 @@ export async function createEngineSession({
|
|
|
1624
1624
|
restorable: nonEditable.length === 0,
|
|
1625
1625
|
requeueOnAbort: nonEditable,
|
|
1626
1626
|
});
|
|
1627
|
+
// session_manage tool: the model scheduled a reset during this turn.
|
|
1628
|
+
// Run it now, at the turn boundary — same clear body as auto-clear.
|
|
1629
|
+
// Cancelled/interrupted turns drop the reset (consume + discard): the
|
|
1630
|
+
// user aborted the turn that asked for it, so the destructive clear
|
|
1631
|
+
// must not fire. commandBusy guards against a concurrent session
|
|
1632
|
+
// command (resume/new) racing the async clear.
|
|
1633
|
+
const scheduledReset = runtime.consumePendingSessionReset?.();
|
|
1634
|
+
if ((scheduledReset === 'clear' || scheduledReset === 'compact_clear')
|
|
1635
|
+
&& turnStatus !== 'cancelled'
|
|
1636
|
+
&& !state.commandBusy) {
|
|
1637
|
+
await performSessionClear({
|
|
1638
|
+
verb: scheduledReset === 'clear' ? 'Clearing conversation' : 'Compacting and clearing conversation',
|
|
1639
|
+
doneLabel: scheduledReset === 'clear' ? 'Session cleared' : 'Session compacted and cleared',
|
|
1640
|
+
skipLabel: 'Session reset skipped',
|
|
1641
|
+
surface: 'session-manage',
|
|
1642
|
+
useCompaction: scheduledReset === 'compact_clear',
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1627
1645
|
// If the user re-submits the reclaimed prompt while the cancelled turn
|
|
1628
1646
|
// is still unwinding, enqueue() cannot start another drain because this
|
|
1629
1647
|
// drain loop is still active. Continue when pending work appeared during
|
|
@@ -1682,18 +1700,40 @@ export async function createEngineSession({
|
|
|
1682
1700
|
if (!activityAt) lastUserActivityAt = now;
|
|
1683
1701
|
return false;
|
|
1684
1702
|
}
|
|
1703
|
+
return performSessionClear({
|
|
1704
|
+
verb: 'Auto-clearing idle conversation',
|
|
1705
|
+
doneLabel: 'Auto-clear complete',
|
|
1706
|
+
skipLabel: 'Auto-clear skipped',
|
|
1707
|
+
surface: 'auto-clear',
|
|
1708
|
+
useCompaction: true,
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
|
|
1712
|
+
// Shared clear body for idle auto-clear and the session_manage tool.
|
|
1713
|
+
// useCompaction=true mirrors auto-clear (summarize via configured
|
|
1714
|
+
// compactType, context carries forward); false is a plain /clear wipe.
|
|
1715
|
+
async function performSessionClear({ verb, doneLabel, skipLabel, surface, useCompaction }) {
|
|
1685
1716
|
autoClearRunning = true;
|
|
1686
1717
|
const startedAt = Date.now();
|
|
1687
|
-
|
|
1718
|
+
// commandBusy blocks concurrent session commands (resume/newSession/
|
|
1719
|
+
// setModel) AND new submits for the duration of the async clear — the
|
|
1720
|
+
// clear swaps the live session object, so racing commands could act on
|
|
1721
|
+
// the wrong session.
|
|
1722
|
+
set({ commandBusy: true, commandStatus: { active: true, verb, startedAt, mode: 'auto-clear' } });
|
|
1688
1723
|
try {
|
|
1689
1724
|
// Give Ink one event-loop turn to paint the auto-clear status before the
|
|
1690
1725
|
// clear/compact path starts doing synchronous session/transcript work.
|
|
1691
1726
|
// Without this, long idle clears can look like a frozen prompt followed by
|
|
1692
1727
|
// an already-complete status row.
|
|
1693
1728
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1729
|
+
let compactType = null;
|
|
1730
|
+
if (useCompaction) {
|
|
1731
|
+
const compaction = runtime.getCompactionSettings();
|
|
1732
|
+
compactType = compaction.compactType || compaction.type || null;
|
|
1733
|
+
}
|
|
1734
|
+
await runtime.clear(compactType
|
|
1735
|
+
? { compactType, requireCompactSuccess: true }
|
|
1736
|
+
: {});
|
|
1697
1737
|
resetStats();
|
|
1698
1738
|
clearUiActivityBeforeContextSync();
|
|
1699
1739
|
syncContextStats({ allowEstimated: true });
|
|
@@ -1710,24 +1750,24 @@ export async function createEngineSession({
|
|
|
1710
1750
|
pushItem({
|
|
1711
1751
|
kind: 'statusdone',
|
|
1712
1752
|
id: nextId(),
|
|
1713
|
-
label:
|
|
1753
|
+
label: doneLabel,
|
|
1714
1754
|
detail: formatElapsedSeconds(Date.now() - startedAt),
|
|
1715
1755
|
});
|
|
1716
1756
|
return true;
|
|
1717
1757
|
} catch (error) {
|
|
1718
|
-
const message = presentErrorText(error, { surface
|
|
1758
|
+
const message = presentErrorText(error, { surface });
|
|
1719
1759
|
pushItem({
|
|
1720
1760
|
kind: 'statusdone',
|
|
1721
1761
|
id: nextId(),
|
|
1722
|
-
label:
|
|
1762
|
+
label: skipLabel,
|
|
1723
1763
|
detail: `conversation kept · ${message}`,
|
|
1724
1764
|
});
|
|
1725
|
-
pushNotice(
|
|
1765
|
+
pushNotice(`${surface} skipped: ${message}`, 'error');
|
|
1726
1766
|
return false;
|
|
1727
1767
|
} finally {
|
|
1728
1768
|
lastUserActivityAt = Date.now();
|
|
1729
1769
|
autoClearRunning = false;
|
|
1730
|
-
set({ commandStatus: null });
|
|
1770
|
+
set({ commandBusy: false, commandStatus: null });
|
|
1731
1771
|
void drain();
|
|
1732
1772
|
}
|
|
1733
1773
|
}
|
|
@@ -1827,7 +1867,7 @@ export async function createEngineSession({
|
|
|
1827
1867
|
},
|
|
1828
1868
|
submit: (text, options = {}) => {
|
|
1829
1869
|
const t = promptDisplayText(text, options).trim();
|
|
1830
|
-
if (!t
|
|
1870
|
+
if (!t) return false;
|
|
1831
1871
|
const mode = options.mode || 'prompt';
|
|
1832
1872
|
// Prompt input queued while a turn is active keeps the
|
|
1833
1873
|
// default `next` priority, so it is injected at the next tool/model
|
|
@@ -1839,11 +1879,14 @@ export async function createEngineSession({
|
|
|
1839
1879
|
displayText: promptDisplayText(text, options),
|
|
1840
1880
|
priority,
|
|
1841
1881
|
};
|
|
1842
|
-
|
|
1882
|
+
// A running clear (idle auto-clear or session_manage) sets commandBusy;
|
|
1883
|
+
// queue the prompt instead of dropping it — it drains after the clear.
|
|
1884
|
+
if (autoClearRunning) {
|
|
1843
1885
|
enqueue(text, queueOptions);
|
|
1844
1886
|
return true;
|
|
1845
1887
|
}
|
|
1846
|
-
if (
|
|
1888
|
+
if (state.commandBusy) return false;
|
|
1889
|
+
if (state.busy) {
|
|
1847
1890
|
enqueue(text, queueOptions);
|
|
1848
1891
|
return true;
|
|
1849
1892
|
}
|
|
@@ -2098,6 +2141,8 @@ export async function createEngineSession({
|
|
|
2098
2141
|
set({ commandBusy: false });
|
|
2099
2142
|
}
|
|
2100
2143
|
},
|
|
2144
|
+
getDisabledSkills: () => runtime.getDisabledSkills?.() || { disabled: [] },
|
|
2145
|
+
setDisabledSkills: (disabled) => runtime.setDisabledSkills?.(disabled) || { disabled: [] },
|
|
2101
2146
|
skillsStatus: () => {
|
|
2102
2147
|
return runtime.skillsStatus?.() || { cwd: state.cwd, count: 0, skills: [] };
|
|
2103
2148
|
},
|