mixdog 0.9.11 → 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
CHANGED
|
@@ -216,6 +216,7 @@ import {
|
|
|
216
216
|
TOOL_SEARCH_TOOL,
|
|
217
217
|
CWD_TOOL,
|
|
218
218
|
SKILL_TOOL,
|
|
219
|
+
SESSION_MANAGE_TOOL,
|
|
219
220
|
LEAD_DISALLOWED_TOOLS,
|
|
220
221
|
applyStandaloneToolDefaults,
|
|
221
222
|
} from './session-runtime/tool-defs.mjs';
|
|
@@ -480,6 +481,9 @@ export async function createMixdogSessionRuntime({
|
|
|
480
481
|
let sessionCreatePromise = null;
|
|
481
482
|
let currentCwd = cwd;
|
|
482
483
|
let sessionNeedsCwdRefresh = false;
|
|
484
|
+
// session_manage tool: reset request scheduled by the model mid-turn,
|
|
485
|
+
// consumed by the TUI engine at turn end ('clear' | 'compact_clear').
|
|
486
|
+
let pendingSessionReset = null;
|
|
483
487
|
let closeRequested = false;
|
|
484
488
|
const warmupTimers = {
|
|
485
489
|
providerSetupWarmupTimer: null,
|
|
@@ -922,6 +926,7 @@ export async function createMixdogSessionRuntime({
|
|
|
922
926
|
TOOL_SEARCH_TOOL,
|
|
923
927
|
SKILL_TOOL,
|
|
924
928
|
CWD_TOOL,
|
|
929
|
+
SESSION_MANAGE_TOOL,
|
|
925
930
|
EXPLORE_TOOL,
|
|
926
931
|
...(searchToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'search' || tool?.name === 'web_fetch'),
|
|
927
932
|
...(memoryToolDefs?.TOOL_DEFS || []).filter((tool) => tool?.name === 'recall' || tool?.name === 'memory'),
|
|
@@ -1018,6 +1023,29 @@ export async function createMixdogSessionRuntime({
|
|
|
1018
1023
|
}
|
|
1019
1024
|
return JSON.stringify({ cwd: currentCwd, sessionId: session?.id || null }, null, 2);
|
|
1020
1025
|
}
|
|
1026
|
+
if (name === 'session_manage') {
|
|
1027
|
+
// Lead/owner sessions only: an agent worker resetting its own
|
|
1028
|
+
// transcript mid-task would corrupt the delegation contract, and it
|
|
1029
|
+
// must never reach the owner conversation either.
|
|
1030
|
+
const callerSessionId = callerCtx?.callerSessionId || null;
|
|
1031
|
+
if (callerSessionId && session?.id && callerSessionId !== session.id) {
|
|
1032
|
+
throw new Error('session_manage: only the lead session may reset the conversation');
|
|
1033
|
+
}
|
|
1034
|
+
if (!session?.id) throw new Error('session_manage: no active session');
|
|
1035
|
+
const action = clean(args?.action).toLowerCase();
|
|
1036
|
+
if (action !== 'clear' && action !== 'compact_clear') {
|
|
1037
|
+
throw new Error(`session_manage: unknown action "${action}" (use clear | compact_clear)`);
|
|
1038
|
+
}
|
|
1039
|
+
// Never clear mid-turn — the loop is still reading the transcript.
|
|
1040
|
+
// Schedule and let the TUI engine consume it at turn end (same
|
|
1041
|
+
// boundary the idle auto-clear uses).
|
|
1042
|
+
// Pin to the current session id so a resume/new-session between
|
|
1043
|
+
// scheduling and consumption can never clear the wrong conversation.
|
|
1044
|
+
pendingSessionReset = { action, sessionId: session.id };
|
|
1045
|
+
return action === 'clear'
|
|
1046
|
+
? 'Session reset scheduled: full clear will run when this turn ends. All prior context will be gone.'
|
|
1047
|
+
: 'Session reset scheduled: the conversation will be summarized (compact) and cleared when this turn ends; key context carries forward in the summary.';
|
|
1048
|
+
}
|
|
1021
1049
|
if (name === 'Skill') {
|
|
1022
1050
|
return skillToolContent(args?.name);
|
|
1023
1051
|
}
|
|
@@ -2518,6 +2546,16 @@ export async function createMixdogSessionRuntime({
|
|
|
2518
2546
|
invalidateContextStatusCache();
|
|
2519
2547
|
return true;
|
|
2520
2548
|
},
|
|
2549
|
+
// session_manage tool handoff: the engine polls this at turn end and, if
|
|
2550
|
+
// set, runs the same clear path the idle auto-clear uses. One-shot read.
|
|
2551
|
+
consumePendingSessionReset() {
|
|
2552
|
+
const pending = pendingSessionReset;
|
|
2553
|
+
pendingSessionReset = null;
|
|
2554
|
+
if (!pending) return null;
|
|
2555
|
+
// Session changed since scheduling (resume / new session) — drop it.
|
|
2556
|
+
if (!session?.id || pending.sessionId !== session.id) return null;
|
|
2557
|
+
return pending.action;
|
|
2558
|
+
},
|
|
2521
2559
|
async compact(options = {}) {
|
|
2522
2560
|
if (!session?.id) return null;
|
|
2523
2561
|
if (activeTurnCount > 0) {
|
|
@@ -69,6 +69,34 @@ export const SKILL_TOOL = {
|
|
|
69
69
|
},
|
|
70
70
|
};
|
|
71
71
|
|
|
72
|
+
// Owner-directed session reset tool. `clear` mirrors /clear (full wipe);
|
|
73
|
+
// `compact_clear` mirrors the auto-clear path (summarize via the configured
|
|
74
|
+
// compactType, then reset — context carries forward in the summary). The
|
|
75
|
+
// reset is SCHEDULED: it runs when the current turn ends, never mid-turn,
|
|
76
|
+
// because the live transcript is still feeding the loop. Lead-session only;
|
|
77
|
+
// the runtime executor rejects agent-worker callers.
|
|
78
|
+
export const SESSION_MANAGE_TOOL = {
|
|
79
|
+
name: 'session_manage',
|
|
80
|
+
title: 'Session Manage',
|
|
81
|
+
annotations: {
|
|
82
|
+
title: 'Session Manage',
|
|
83
|
+
readOnlyHint: false,
|
|
84
|
+
destructiveHint: true,
|
|
85
|
+
idempotentHint: false,
|
|
86
|
+
openWorldHint: false,
|
|
87
|
+
agentHidden: true,
|
|
88
|
+
},
|
|
89
|
+
description: 'Reset this conversation on explicit user request. action=clear wipes all context (like /clear); action=compact_clear summarizes first and carries context forward (auto-clear style). Applies when the current turn ends.',
|
|
90
|
+
inputSchema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
properties: {
|
|
93
|
+
action: { type: 'string', enum: ['clear', 'compact_clear'], description: 'clear = full wipe; compact_clear = summarize then reset.' },
|
|
94
|
+
},
|
|
95
|
+
required: ['action'],
|
|
96
|
+
additionalProperties: false,
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
72
100
|
export const LEAD_DISALLOWED_TOOLS = Object.freeze([]);
|
|
73
101
|
export const AGENT_HIDDEN_WRAPPER_TOOLS = new Set([]);
|
|
74
102
|
|
package/src/tui/dist/index.mjs
CHANGED
|
@@ -23097,6 +23097,16 @@ async function createEngineSession({
|
|
|
23097
23097
|
restorable: nonEditable.length === 0,
|
|
23098
23098
|
requeueOnAbort: nonEditable
|
|
23099
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
|
+
}
|
|
23100
23110
|
if (turnStatus === "cancelled" && pending.length === 0) break;
|
|
23101
23111
|
}
|
|
23102
23112
|
} finally {
|
|
@@ -23140,14 +23150,26 @@ async function createEngineSession({
|
|
|
23140
23150
|
if (!activityAt) lastUserActivityAt = now;
|
|
23141
23151
|
return false;
|
|
23142
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 }) {
|
|
23143
23162
|
autoClearRunning = true;
|
|
23144
23163
|
const startedAt2 = Date.now();
|
|
23145
|
-
set({ commandStatus: { active: true, verb
|
|
23164
|
+
set({ commandBusy: true, commandStatus: { active: true, verb, startedAt: startedAt2, mode: "auto-clear" } });
|
|
23146
23165
|
try {
|
|
23147
23166
|
await new Promise((resolve5) => setTimeout(resolve5, 0));
|
|
23148
|
-
|
|
23149
|
-
|
|
23150
|
-
|
|
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 } : {});
|
|
23151
23173
|
resetStats();
|
|
23152
23174
|
clearUiActivityBeforeContextSync();
|
|
23153
23175
|
syncContextStats({ allowEstimated: true });
|
|
@@ -23164,24 +23186,24 @@ async function createEngineSession({
|
|
|
23164
23186
|
pushItem({
|
|
23165
23187
|
kind: "statusdone",
|
|
23166
23188
|
id: nextId(),
|
|
23167
|
-
label:
|
|
23189
|
+
label: doneLabel,
|
|
23168
23190
|
detail: formatElapsedSeconds(Date.now() - startedAt2)
|
|
23169
23191
|
});
|
|
23170
23192
|
return true;
|
|
23171
23193
|
} catch (error) {
|
|
23172
|
-
const message = presentErrorText(error, { surface
|
|
23194
|
+
const message = presentErrorText(error, { surface });
|
|
23173
23195
|
pushItem({
|
|
23174
23196
|
kind: "statusdone",
|
|
23175
23197
|
id: nextId(),
|
|
23176
|
-
label:
|
|
23198
|
+
label: skipLabel,
|
|
23177
23199
|
detail: `conversation kept \xB7 ${message}`
|
|
23178
23200
|
});
|
|
23179
|
-
pushNotice(
|
|
23201
|
+
pushNotice(`${surface} skipped: ${message}`, "error");
|
|
23180
23202
|
return false;
|
|
23181
23203
|
} finally {
|
|
23182
23204
|
lastUserActivityAt = Date.now();
|
|
23183
23205
|
autoClearRunning = false;
|
|
23184
|
-
set({ commandStatus: null });
|
|
23206
|
+
set({ commandBusy: false, commandStatus: null });
|
|
23185
23207
|
void drain();
|
|
23186
23208
|
}
|
|
23187
23209
|
}
|
|
@@ -23278,7 +23300,7 @@ async function createEngineSession({
|
|
|
23278
23300
|
},
|
|
23279
23301
|
submit: (text, options = {}) => {
|
|
23280
23302
|
const t = promptDisplayText(text, options).trim();
|
|
23281
|
-
if (!t
|
|
23303
|
+
if (!t) return false;
|
|
23282
23304
|
const mode = options.mode || "prompt";
|
|
23283
23305
|
const priority = options.priority;
|
|
23284
23306
|
const queueOptions = {
|
|
@@ -23287,11 +23309,12 @@ async function createEngineSession({
|
|
|
23287
23309
|
displayText: promptDisplayText(text, options),
|
|
23288
23310
|
priority
|
|
23289
23311
|
};
|
|
23290
|
-
if (
|
|
23312
|
+
if (autoClearRunning) {
|
|
23291
23313
|
enqueue(text, queueOptions);
|
|
23292
23314
|
return true;
|
|
23293
23315
|
}
|
|
23294
|
-
if (
|
|
23316
|
+
if (state.commandBusy) return false;
|
|
23317
|
+
if (state.busy) {
|
|
23295
23318
|
enqueue(text, queueOptions);
|
|
23296
23319
|
return true;
|
|
23297
23320
|
}
|
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
|
}
|