mixdog 0.9.19 → 0.9.21
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/LICENSE +21 -0
- package/README.md +100 -34
- package/package.json +3 -2
- package/scripts/build-tui.mjs +6 -0
- package/scripts/hook-bus-test.mjs +8 -0
- package/scripts/log-writer-guard-smoke.mjs +131 -0
- package/scripts/reactive-compact-persist-smoke.mjs +22 -6
- package/scripts/recall-bench-cases.json +0 -1
- package/scripts/recall-quality-cases.json +1 -2
- package/scripts/session-ingest-compaction-smoke.mjs +241 -0
- package/scripts/tool-smoke.mjs +150 -45
- package/src/defaults/skills/setup/SKILL.md +327 -0
- package/src/help.mjs +2 -5
- package/src/lib/mixdog-debug.cjs +13 -0
- package/src/mixdog-session-runtime.mjs +7 -3328
- package/src/runtime/agent/orchestrator/context/collect.mjs +33 -9
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -10
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +40 -3
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +73 -3
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +663 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +6 -0
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -1
- package/src/runtime/agent/orchestrator/session/loop.mjs +8 -1860
- package/src/runtime/agent/orchestrator/session/manager/agent-runtime-singleton.mjs +29 -0
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +686 -0
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -12
- package/src/runtime/agent/orchestrator/session/manager/env-utils.mjs +8 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +159 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +143 -0
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +268 -0
- package/src/runtime/agent/orchestrator/session/manager/provider-cache-key.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +124 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +258 -0
- package/src/runtime/agent/orchestrator/session/manager/session-errors.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/manager/session-id.mjs +9 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +475 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +23 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +88 -2285
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +440 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +153 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -1
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +642 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +23 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +108 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +15 -3
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +7 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +24 -2
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +72 -8
- package/src/runtime/agent/orchestrator/tools/shell-policy-danger-target.mjs +5 -1
- package/src/runtime/channels/index.mjs +6 -2183
- package/src/runtime/channels/lib/inbound-handler.mjs +328 -0
- package/src/runtime/channels/lib/interaction-handlers.mjs +260 -0
- package/src/runtime/channels/lib/network-retry.mjs +23 -0
- package/src/runtime/channels/lib/owned-runtime.mjs +627 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +20 -9
- package/src/runtime/channels/lib/transcript-binding.mjs +336 -0
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +39 -2
- package/src/runtime/channels/lib/worker-bootstrap.mjs +97 -0
- package/src/runtime/channels/lib/worker-ipc.mjs +201 -0
- package/src/runtime/channels/lib/worker-main.mjs +777 -0
- package/src/runtime/memory/index.mjs +163 -1725
- package/src/runtime/memory/lib/embedding-provider.mjs +4 -2
- package/src/runtime/memory/lib/embedding-worker.mjs +47 -6
- package/src/runtime/memory/lib/http-router.mjs +811 -0
- package/src/runtime/memory/lib/memory-action-handlers.mjs +901 -0
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +6 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +46 -9
- package/src/runtime/memory/lib/memory-cycle2.mjs +87 -11
- package/src/runtime/memory/lib/memory-embed.mjs +28 -7
- package/src/runtime/memory/lib/memory-recall-store.mjs +4 -4
- package/src/runtime/memory/lib/memory.mjs +39 -0
- package/src/runtime/memory/lib/query-handlers.mjs +2 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +401 -0
- package/src/runtime/shared/atomic-file.mjs +138 -80
- package/src/runtime/shared/child-guardian.mjs +61 -3
- package/src/session-runtime/boot-profile.mjs +36 -0
- package/src/session-runtime/channel-config-api.mjs +70 -0
- package/src/session-runtime/context-status.mjs +181 -0
- package/src/session-runtime/env.mjs +17 -0
- package/src/session-runtime/lifecycle-api.mjs +242 -0
- package/src/session-runtime/model-route-api.mjs +198 -0
- package/src/session-runtime/provider-auth-api.mjs +135 -0
- package/src/session-runtime/resource-api.mjs +282 -0
- package/src/session-runtime/runtime-core.mjs +2104 -0
- package/src/session-runtime/session-turn-api.mjs +274 -0
- package/src/session-runtime/tool-catalog.mjs +18 -264
- package/src/session-runtime/tool-defs.mjs +2 -2
- package/src/session-runtime/workflow-agents-api.mjs +238 -0
- package/src/standalone/agent-tool.mjs +2 -2
- package/src/standalone/channel-worker.mjs +67 -7
- package/src/standalone/folder-dialog.mjs +4 -1
- package/src/standalone/memory-runtime-proxy.mjs +154 -17
- package/src/standalone/seeds.mjs +28 -1
- package/src/tui/App.jsx +40 -28
- package/src/tui/app/core-memory-picker.mjs +1 -1
- package/src/tui/app/doctor.mjs +175 -0
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +9 -0
- package/src/tui/app/use-mouse-input.mjs +6 -0
- package/src/tui/app/use-transcript-scroll.mjs +77 -3
- package/src/tui/dist/index.mjs +2851 -2162
- package/src/tui/engine/context-state.mjs +145 -0
- package/src/tui/engine/prompt-history.mjs +27 -0
- package/src/tui/engine/session-api-ext.mjs +478 -0
- package/src/tui/engine/session-api.mjs +564 -0
- package/src/tui/engine/session-flow.mjs +485 -0
- package/src/tui/engine/turn.mjs +1078 -0
- package/src/tui/engine.mjs +68 -2620
- package/src/tui/index.jsx +7 -0
- package/vendor/ink/build/ink.js +16 -1
- package/vendor/ink/build/output.js +30 -4
- package/vendor/ink/build/render.js +5 -0
- package/src/workflows/sequential/WORKFLOW.md +0 -51
|
@@ -58,6 +58,50 @@ function addCompactUsageToSession(session, usage) {
|
|
|
58
58
|
session.totalUncachedInputTokens = (session.totalUncachedInputTokens || 0) + uncachedInputTokens;
|
|
59
59
|
session.tokensCumulative = (session.tokensCumulative || 0) + inputTokens + outputTokens;
|
|
60
60
|
}
|
|
61
|
+
// A dead/unreachable memory runtime makes each proxy RPC wait ~30s (waitForPort)
|
|
62
|
+
// and retry once (memory-runtime-proxy.mjs) — that must NEVER wedge the
|
|
63
|
+
// compact//clear path. Bound every recall-fasttrack memory call with a short
|
|
64
|
+
// local timeout: on timeout we abort (best-effort cancel via a chained signal)
|
|
65
|
+
// and treat it exactly like an RPC failure, so compaction proceeds WITHOUT
|
|
66
|
+
// recall-fasttrack instead of hanging.
|
|
67
|
+
const RECALL_MEMORY_CALL_TIMEOUT_MS = Math.max(
|
|
68
|
+
250,
|
|
69
|
+
Number(process.env.MIXDOG_AGENT_COMPACT_RECALL_TIMEOUT_MS) || 4000,
|
|
70
|
+
);
|
|
71
|
+
function recallMemoryTimeoutMs(session) {
|
|
72
|
+
const configured = positiveContextWindow(session?.compaction?.recallMemoryTimeoutMs);
|
|
73
|
+
// Clamp ALL sources (session-config included) to the 250ms floor so a
|
|
74
|
+
// misconfigured tiny value can't turn the bound into a busy no-wait.
|
|
75
|
+
return Math.max(250, configured || RECALL_MEMORY_CALL_TIMEOUT_MS);
|
|
76
|
+
}
|
|
77
|
+
async function callMemoryBounded(args, callerCtx, timeoutMs) {
|
|
78
|
+
const ac = new AbortController();
|
|
79
|
+
const outer = callerCtx?.signal;
|
|
80
|
+
const onOuterAbort = () => { try { ac.abort(); } catch {} };
|
|
81
|
+
if (outer) {
|
|
82
|
+
if (outer.aborted) ac.abort();
|
|
83
|
+
else { try { outer.addEventListener?.('abort', onOuterAbort, { once: true }); } catch {} }
|
|
84
|
+
}
|
|
85
|
+
let timer = null;
|
|
86
|
+
const timeout = new Promise((_, reject) => {
|
|
87
|
+
timer = setTimeout(() => {
|
|
88
|
+
try { ac.abort(); } catch {}
|
|
89
|
+
reject(new Error(`memory ${args?.action || 'call'} timed out after ${timeoutMs}ms`));
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
try { timer.unref?.(); } catch {}
|
|
92
|
+
});
|
|
93
|
+
try {
|
|
94
|
+
return await Promise.race([
|
|
95
|
+
executeInternalTool('memory', args, { ...callerCtx, signal: ac.signal }),
|
|
96
|
+
timeout,
|
|
97
|
+
]);
|
|
98
|
+
} finally {
|
|
99
|
+
if (timer) clearTimeout(timer);
|
|
100
|
+
// Drop the chained-abort listener when the call settles first, so a
|
|
101
|
+
// later outer abort can't fire into a dead controller / leak.
|
|
102
|
+
try { outer?.removeEventListener?.('abort', onOuterAbort); } catch {}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
61
105
|
async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
62
106
|
const sessionId = opts.sessionId || session?.id || null;
|
|
63
107
|
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
@@ -72,16 +116,22 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
|
72
116
|
};
|
|
73
117
|
const hydrateLimit = positiveContextWindow(session?.compaction?.recallIngestLimit)
|
|
74
118
|
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
119
|
+
const memoryTimeoutMs = recallMemoryTimeoutMs(session);
|
|
75
120
|
try {
|
|
76
|
-
await
|
|
121
|
+
await callMemoryBounded({
|
|
77
122
|
action: 'ingest_session',
|
|
78
123
|
sessionId,
|
|
79
124
|
messages,
|
|
80
125
|
cwd: session?.cwd,
|
|
81
126
|
limit: hydrateLimit,
|
|
82
|
-
}, callerCtx);
|
|
127
|
+
}, callerCtx, memoryTimeoutMs);
|
|
83
128
|
} catch (err) {
|
|
84
|
-
|
|
129
|
+
// Ingest failed (dead/timed-out memory runtime). The transcript is NOT
|
|
130
|
+
// in memory, so recall-fasttrack MUST NOT proceed with a false "history
|
|
131
|
+
// is in memory" digest — that would drop un-ingested head history.
|
|
132
|
+
// Throw so the caller falls back to the normal compaction path.
|
|
133
|
+
try { process.stderr.write(`[session] recall-fasttrack ingest failed — bailing (sess=${sessionId}): ${err?.message || err}\n`); } catch {}
|
|
134
|
+
throw new Error(`recall-fasttrack ingest failed: ${err?.message || err}`);
|
|
85
135
|
}
|
|
86
136
|
// Digest injection (mirrors loop/recall-fasttrack.mjs): no dump + cycle1
|
|
87
137
|
// drain — that ran memory-pipeline LLM chunking inside the compaction.
|
|
@@ -89,24 +139,25 @@ async function runRecallFastTrackForSession(session, messages, opts = {}) {
|
|
|
89
139
|
// chunks it on its own schedule and recall serves anything beyond the
|
|
90
140
|
// digest.
|
|
91
141
|
let recallText = '';
|
|
92
|
-
let cycle1Error = null;
|
|
93
142
|
try {
|
|
94
|
-
const browsed = await
|
|
143
|
+
const browsed = await callMemoryBounded({
|
|
95
144
|
action: 'search',
|
|
96
145
|
sessionId,
|
|
97
146
|
limit: positiveContextWindow(session?.compaction?.recallDigestLimit) || 30,
|
|
98
147
|
includeMembers: true,
|
|
99
148
|
includeRaw: true,
|
|
100
|
-
}, callerCtx);
|
|
149
|
+
}, callerCtx, memoryTimeoutMs);
|
|
101
150
|
recallText = typeof browsed === 'string' ? browsed : String(browsed?.text ?? browsed ?? '');
|
|
102
151
|
} catch (err) {
|
|
103
|
-
|
|
104
|
-
|
|
152
|
+
// Search failed (dead/timed-out memory runtime). Same hazard as a failed
|
|
153
|
+
// ingest: without a real recall dump we can't safely replace head
|
|
154
|
+
// history — bail out to the normal compaction path.
|
|
155
|
+
try { process.stderr.write(`[session] recall-digest browse failed — bailing (sess=${sessionId}): ${err?.message || err}\n`); } catch {}
|
|
156
|
+
throw new Error(`recall-fasttrack search failed: ${err?.message || err}`);
|
|
105
157
|
}
|
|
106
158
|
return {
|
|
107
159
|
query,
|
|
108
160
|
querySha,
|
|
109
|
-
cycle1Error,
|
|
110
161
|
recallText: [
|
|
111
162
|
`session_id=${sessionId}`,
|
|
112
163
|
`Full history is in memory — use the recall tool for details beyond this digest.`,
|
|
@@ -221,9 +272,6 @@ export async function runSessionCompaction(session, opts = {}) {
|
|
|
221
272
|
if (Array.isArray(recallFastTrackResult?.messages)) {
|
|
222
273
|
compacted = recallFastTrackResult.messages;
|
|
223
274
|
}
|
|
224
|
-
if (recallPayload.cycle1Error) {
|
|
225
|
-
recallFastTrackError = new Error(String(recallPayload.cycle1Error));
|
|
226
|
-
}
|
|
227
275
|
} catch (err) {
|
|
228
276
|
recallFastTrackError = err;
|
|
229
277
|
compactError = err;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// manager/env-utils.mjs
|
|
2
|
+
// Shared non-negative integer env parser. Extracted verbatim from manager.mjs
|
|
3
|
+
// so ask-session (terminal save timeout) and idle-cleanup (interval constants)
|
|
4
|
+
// read the identical contract without importing manager.mjs back.
|
|
5
|
+
export function nonNegativeIntEnv(name, fallback) {
|
|
6
|
+
const value = Number(process.env[name]);
|
|
7
|
+
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
|
|
8
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// manager/idle-cleanup.mjs
|
|
2
|
+
// Periodic idle-session + tombstone sweep extracted verbatim from manager.mjs.
|
|
3
|
+
// Drives sweepStaleSessions on an unref'd interval; closeSession is imported
|
|
4
|
+
// from session-close.mjs (one-way dependency, no cycle).
|
|
5
|
+
import { sweepStaleSessions } from '../store.mjs';
|
|
6
|
+
import { sweepOrphanedPendingMessages } from './pending-messages.mjs';
|
|
7
|
+
import { _getRuntimeEntry, _clearSessionRuntime, _runtimeEntries } from './runtime-liveness.mjs';
|
|
8
|
+
import { _closeBashSessionLazy } from './runtime-loaders.mjs';
|
|
9
|
+
import { closeSession } from './session-close.mjs';
|
|
10
|
+
import { nonNegativeIntEnv } from './env-utils.mjs';
|
|
11
|
+
|
|
12
|
+
// --- Periodic idle session cleanup ---
|
|
13
|
+
const CLEANUP_INTERVAL_MS = nonNegativeIntEnv('MIXDOG_SESSION_CLEANUP_INTERVAL_MS', 5 * 60 * 1000); // check every 5 minutes
|
|
14
|
+
const CLEANUP_INITIAL_DELAY_MS = nonNegativeIntEnv('MIXDOG_SESSION_CLEANUP_INITIAL_DELAY_MS', CLEANUP_INTERVAL_MS > 0 ? CLEANUP_INTERVAL_MS : 0);
|
|
15
|
+
const CLEANUP_SLOW_LOG_MS = nonNegativeIntEnv('MIXDOG_SESSION_CLEANUP_SLOW_LOG_MS', 250);
|
|
16
|
+
const TOMBSTONE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h — far longer than any realistic ask race window
|
|
17
|
+
let _cleanupTimer = null;
|
|
18
|
+
let _cleanupInitialTimer = null;
|
|
19
|
+
|
|
20
|
+
function _previewIds(items, limit = 5) {
|
|
21
|
+
const ids = (items || []).slice(0, limit).map((item) => item.id).filter(Boolean);
|
|
22
|
+
if (ids.length === 0) return '';
|
|
23
|
+
const more = items.length > limit ? `, +${items.length - limit} more` : '';
|
|
24
|
+
return ` (${ids.join(', ')}${more})`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function sweepIdleSessions({ includeTombstones = true } = {}) {
|
|
28
|
+
const startedAt = Date.now();
|
|
29
|
+
try {
|
|
30
|
+
const result = sweepStaleSessions({
|
|
31
|
+
tombstoneMaxAgeMs: includeTombstones ? TOMBSTONE_MAX_AGE_MS : 0,
|
|
32
|
+
});
|
|
33
|
+
const {
|
|
34
|
+
cleaned,
|
|
35
|
+
remaining,
|
|
36
|
+
details,
|
|
37
|
+
tombstonesCleaned = 0,
|
|
38
|
+
tombstoneDetails = [],
|
|
39
|
+
tombstoneErrors = [],
|
|
40
|
+
} = result;
|
|
41
|
+
if (cleaned > 0) {
|
|
42
|
+
for (const d of details) {
|
|
43
|
+
// Skip entries with an active in-flight controller — aborting
|
|
44
|
+
// them via closeSession() is the safe path; clearing the runtime
|
|
45
|
+
// without signalling the controller leaves orphan provider work.
|
|
46
|
+
const rtEntry = _getRuntimeEntry(d.id);
|
|
47
|
+
if (rtEntry && rtEntry.controller && !rtEntry.controller.signal?.aborted) {
|
|
48
|
+
try { closeSession(d.id, 'idle-sweep'); } catch { /* ignore */ }
|
|
49
|
+
} else {
|
|
50
|
+
_clearSessionRuntime(d.id);
|
|
51
|
+
if (d.bashSessionId) {
|
|
52
|
+
try { _closeBashSessionLazy(d.bashSessionId, `idle-sweep:${d.id}`); } catch { /* ignore */ }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
process.stderr.write(`[agent-session] idle cleanup: closed ${d.id} (idle ${d.idleMinutes}m, owner=${d.owner})\n`);
|
|
56
|
+
}
|
|
57
|
+
process.stderr.write(`[agent-session] idle sweep: cleaned ${cleaned} session(s), ${remaining} remaining\n`);
|
|
58
|
+
}
|
|
59
|
+
if (tombstonesCleaned > 0) {
|
|
60
|
+
for (const d of tombstoneDetails) {
|
|
61
|
+
if (d?.id) _clearSessionRuntime(d.id);
|
|
62
|
+
}
|
|
63
|
+
process.stderr.write(`[session-sweep] unlinked ${tombstonesCleaned} tombstone(s)${_previewIds(tombstoneDetails)}\n`);
|
|
64
|
+
}
|
|
65
|
+
if (tombstoneErrors.length > 0) {
|
|
66
|
+
const first = tombstoneErrors[0];
|
|
67
|
+
process.stderr.write(`[session-sweep] tombstone unlink failed for ${tombstoneErrors.length} session(s): ${first?.id || 'unknown'} ${first?.message || ''}\n`);
|
|
68
|
+
}
|
|
69
|
+
const elapsed = Date.now() - startedAt;
|
|
70
|
+
if (elapsed >= CLEANUP_SLOW_LOG_MS) {
|
|
71
|
+
process.stderr.write(`[session-sweep] cleanup took ${elapsed}ms (idle=${cleaned}, tombstones=${tombstonesCleaned}, remaining=${remaining})\n`);
|
|
72
|
+
}
|
|
73
|
+
} catch (e) {
|
|
74
|
+
process.stderr.write(`[agent-session] idle sweep error: ${e && e.message || e}\n`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Unlink tombstone session files (closed=true) older than TOMBSTONE_MAX_AGE_MS.
|
|
80
|
+
*
|
|
81
|
+
* Rationale: closeSession() leaves the tombstone on disk as the authoritative
|
|
82
|
+
* resurrection-blocker for racing saveSession() calls. That race resolves in
|
|
83
|
+
* microseconds (the window inside _doSave between temp write and rename), so
|
|
84
|
+
* 24h is vastly safe. After the TTL expires we reclaim the disk slot.
|
|
85
|
+
*
|
|
86
|
+
* Uses `getStoredSessionsRaw()` rather than `listStoredSessions()` because the
|
|
87
|
+
* latter's inline 30-min idle cleanup would race-unlink tombstones before we
|
|
88
|
+
* get to log them — we want to own the unlink decision and stderr line here.
|
|
89
|
+
*/
|
|
90
|
+
export function sweepTombstones() {
|
|
91
|
+
try {
|
|
92
|
+
const { tombstonesCleaned = 0, tombstoneDetails = [], tombstoneErrors = [] } = sweepStaleSessions({
|
|
93
|
+
sweepIdle: false,
|
|
94
|
+
tombstoneMaxAgeMs: TOMBSTONE_MAX_AGE_MS,
|
|
95
|
+
});
|
|
96
|
+
for (const d of tombstoneDetails) {
|
|
97
|
+
if (d?.id) _clearSessionRuntime(d.id);
|
|
98
|
+
}
|
|
99
|
+
if (tombstonesCleaned > 0) {
|
|
100
|
+
process.stderr.write(`[session-sweep] unlinked ${tombstonesCleaned} tombstone(s)${_previewIds(tombstoneDetails)}\n`);
|
|
101
|
+
}
|
|
102
|
+
if (tombstoneErrors.length > 0) {
|
|
103
|
+
const first = tombstoneErrors[0];
|
|
104
|
+
process.stderr.write(`[session-sweep] tombstone unlink failed for ${tombstoneErrors.length} session(s): ${first?.id || 'unknown'} ${first?.message || ''}\n`);
|
|
105
|
+
}
|
|
106
|
+
return tombstonesCleaned;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
process.stderr.write(`[session-sweep] tombstone sweep error: ${e && e.message || e}\n`);
|
|
109
|
+
return 0;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasActiveRuntimeWork() {
|
|
114
|
+
for (const [, entry] of _runtimeEntries()) {
|
|
115
|
+
if (!entry || entry.closed === true) continue;
|
|
116
|
+
if (entry.controller && !entry.controller.signal?.aborted) return true;
|
|
117
|
+
if (['connecting', 'requesting', 'streaming', 'tool_running', 'cancelling'].includes(entry.stage)) return true;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _runCleanupCycle() {
|
|
123
|
+
if (hasActiveRuntimeWork()) return;
|
|
124
|
+
sweepOrphanedPendingMessages();
|
|
125
|
+
sweepIdleSessions({ includeTombstones: true });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function _startCleanupInterval() {
|
|
129
|
+
if (_cleanupTimer) return;
|
|
130
|
+
if (CLEANUP_INTERVAL_MS <= 0) return;
|
|
131
|
+
_cleanupTimer = setInterval(_runCleanupCycle, CLEANUP_INTERVAL_MS);
|
|
132
|
+
if (_cleanupTimer.unref) _cleanupTimer.unref(); // don't block process exit
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function startIdleCleanup() {
|
|
136
|
+
if (_cleanupTimer || _cleanupInitialTimer) return;
|
|
137
|
+
if (CLEANUP_INITIAL_DELAY_MS <= 0) {
|
|
138
|
+
_runCleanupCycle();
|
|
139
|
+
_startCleanupInterval();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
_cleanupInitialTimer = setTimeout(() => {
|
|
143
|
+
_cleanupInitialTimer = null;
|
|
144
|
+
_runCleanupCycle();
|
|
145
|
+
_startCleanupInterval();
|
|
146
|
+
}, CLEANUP_INITIAL_DELAY_MS);
|
|
147
|
+
if (_cleanupInitialTimer.unref) _cleanupInitialTimer.unref();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function stopIdleCleanup() {
|
|
151
|
+
if (_cleanupInitialTimer) {
|
|
152
|
+
clearTimeout(_cleanupInitialTimer);
|
|
153
|
+
_cleanupInitialTimer = null;
|
|
154
|
+
}
|
|
155
|
+
if (_cleanupTimer) {
|
|
156
|
+
clearInterval(_cleanupTimer);
|
|
157
|
+
_cleanupTimer = null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// manager/message-sanitize.mjs
|
|
2
|
+
// Message sanitization + compaction-failure persistence helpers extracted
|
|
3
|
+
// verbatim from manager.mjs. sanitizeSessionMessagesForModel drops internal
|
|
4
|
+
// runtime-notification turns and cancelled-assistant stubs while keeping image
|
|
5
|
+
// content intact (reference-agent parity); the compact-failure helpers persist
|
|
6
|
+
// a compacted outgoing transcript when an ask throws mid-turn.
|
|
7
|
+
import { promptContentText, isInternalRuntimeNotificationText } from './prompt-utils.mjs';
|
|
8
|
+
import { saveSessionAsync } from '../store.mjs';
|
|
9
|
+
import { _getRuntimeEntry } from './runtime-liveness.mjs';
|
|
10
|
+
|
|
11
|
+
function isInternalCancelledAssistantMessage(message) {
|
|
12
|
+
if (!message || message.role !== 'assistant') return false;
|
|
13
|
+
if (message.cancelled === true) return true;
|
|
14
|
+
const text = promptContentText(message.content).trim();
|
|
15
|
+
return /^\[cancelled\]\s+This turn was interrupted before completion\./i.test(text)
|
|
16
|
+
|| /Preserve the user request above as the active task context/i.test(text);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function sanitizeSessionMessagesForModel(messages) {
|
|
20
|
+
// Drop internal runtime-notification turns and cancelled-assistant stubs so
|
|
21
|
+
// they never reach the model, but KEEP image content intact. Reference-agent
|
|
22
|
+
// parity: the live transcript and every model request retain attached
|
|
23
|
+
// images across turns; only the compaction-summary call strips them. The
|
|
24
|
+
// disk-stored session JSON replaces image bytes with a text placeholder at
|
|
25
|
+
// serialization time (see store.mjs), so this no longer touches images.
|
|
26
|
+
if (!Array.isArray(messages) || messages.length === 0) return [];
|
|
27
|
+
const out = [];
|
|
28
|
+
let droppingInternalTurn = false;
|
|
29
|
+
for (const message of messages) {
|
|
30
|
+
if (isInternalCancelledAssistantMessage(message)) {
|
|
31
|
+
droppingInternalTurn = false;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (message?.role === 'user' && isInternalRuntimeNotificationText(message.content)) {
|
|
35
|
+
droppingInternalTurn = true;
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
if (droppingInternalTurn) {
|
|
39
|
+
if (message?.role === 'user') {
|
|
40
|
+
droppingInternalTurn = false;
|
|
41
|
+
} else {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
out.push(message);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function sessionMessagesSnapshotChanged(before, after) {
|
|
51
|
+
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
52
|
+
if (before.length !== after.length) return true;
|
|
53
|
+
for (let i = 0; i < before.length; i += 1) {
|
|
54
|
+
if (before[i] !== after[i]) return true;
|
|
55
|
+
try {
|
|
56
|
+
if (JSON.stringify(before[i]) !== JSON.stringify(after[i])) return true;
|
|
57
|
+
} catch {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isCompactedOutgoingFinalAssistantMessage(message) {
|
|
65
|
+
if (!message || message.role !== 'assistant') return false;
|
|
66
|
+
if (message.emptyFinal === true) return true;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function sessionMessagesAdvancedBeyondCompactedOutgoing(currentSanitized, compactedSanitized) {
|
|
71
|
+
if (!Array.isArray(currentSanitized) || !Array.isArray(compactedSanitized)) return false;
|
|
72
|
+
if (currentSanitized.length !== compactedSanitized.length + 1) return false;
|
|
73
|
+
const prefix = currentSanitized.slice(0, compactedSanitized.length);
|
|
74
|
+
if (sessionMessagesSnapshotChanged(compactedSanitized, prefix)) return false;
|
|
75
|
+
return isCompactedOutgoingFinalAssistantMessage(currentSanitized[currentSanitized.length - 1]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const _sessionMessagesAdvancedBeyondCompactedOutgoing = sessionMessagesAdvancedBeyondCompactedOutgoing;
|
|
79
|
+
|
|
80
|
+
function applyCompactFailurePersistToSession(activeSession, {
|
|
81
|
+
priorSanitized,
|
|
82
|
+
sanitized,
|
|
83
|
+
messagesAdvanced,
|
|
84
|
+
error = null,
|
|
85
|
+
}) {
|
|
86
|
+
if (!messagesAdvanced && !sessionMessagesSnapshotChanged(priorSanitized, sanitized)) return false;
|
|
87
|
+
if (!messagesAdvanced) {
|
|
88
|
+
activeSession.messages = sanitized;
|
|
89
|
+
activeSession.providerState = undefined;
|
|
90
|
+
}
|
|
91
|
+
activeSession.updatedAt = Date.now();
|
|
92
|
+
activeSession.lastUsedAt = Date.now();
|
|
93
|
+
if (activeSession.compaction && typeof activeSession.compaction === 'object'
|
|
94
|
+
&& (activeSession.compaction.lastStage === 'compacting'
|
|
95
|
+
|| activeSession.compaction.lastStage === 'overflow_failed')) {
|
|
96
|
+
const prev = activeSession.compaction;
|
|
97
|
+
const cause = error?.cause;
|
|
98
|
+
const overflow = error?.code === 'AGENT_CONTEXT_OVERFLOW';
|
|
99
|
+
activeSession.compaction = {
|
|
100
|
+
...prev,
|
|
101
|
+
lastStage: prev.lastStage === 'overflow_failed'
|
|
102
|
+
? 'overflow_failed'
|
|
103
|
+
: (overflow ? 'overflow_failed' : 'failed'),
|
|
104
|
+
lastCheckedAt: Date.now(),
|
|
105
|
+
lastError: prev.lastError || cause?.message || error?.message || null,
|
|
106
|
+
lastSemanticError: prev.lastSemanticError || cause?.message || null,
|
|
107
|
+
lastRecallFastTrackError: prev.lastRecallFastTrackError
|
|
108
|
+
|| (cause?.message && String(cause?.name || '').includes('Recall') ? cause.message : null),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export const _applyCompactFailurePersistToSession = applyCompactFailurePersistToSession;
|
|
115
|
+
|
|
116
|
+
export async function persistCompactedOutgoingAfterAskFailure({
|
|
117
|
+
sessionId,
|
|
118
|
+
activeSession,
|
|
119
|
+
askGeneration,
|
|
120
|
+
turnOutgoing,
|
|
121
|
+
error = null,
|
|
122
|
+
}) {
|
|
123
|
+
if (!activeSession || activeSession.closed === true) return;
|
|
124
|
+
if (!Array.isArray(turnOutgoing) || turnOutgoing.length === 0) return;
|
|
125
|
+
const currentRuntime = _getRuntimeEntry(sessionId);
|
|
126
|
+
if (currentRuntime?.closed || currentRuntime?.generation !== askGeneration) return;
|
|
127
|
+
const sanitized = sanitizeSessionMessagesForModel(turnOutgoing);
|
|
128
|
+
const priorSanitized = sanitizeSessionMessagesForModel(
|
|
129
|
+
Array.isArray(activeSession.messages) ? activeSession.messages : [],
|
|
130
|
+
);
|
|
131
|
+
const messagesAdvanced = sessionMessagesAdvancedBeyondCompactedOutgoing(priorSanitized, sanitized);
|
|
132
|
+
const applied = applyCompactFailurePersistToSession(activeSession, {
|
|
133
|
+
priorSanitized,
|
|
134
|
+
sanitized,
|
|
135
|
+
messagesAdvanced,
|
|
136
|
+
error,
|
|
137
|
+
});
|
|
138
|
+
if (!applied) return;
|
|
139
|
+
try {
|
|
140
|
+
await saveSessionAsync(activeSession, { expectedGeneration: askGeneration });
|
|
141
|
+
} catch { /* best-effort: preserve in-memory compaction even if disk is slow */ }
|
|
142
|
+
if (currentRuntime) currentRuntime.session = activeSession;
|
|
143
|
+
}
|