mixdog 0.9.38 → 0.9.40
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 +9 -4
- package/scripts/abort-recovery-test.mjs +43 -2
- package/scripts/agent-tag-reuse-smoke.mjs +146 -6
- package/scripts/agent-terminal-reap-test.mjs +127 -0
- package/scripts/agent-trace-io-test.mjs +69 -0
- package/scripts/code-graph-disk-hit-test.mjs +224 -0
- package/scripts/execution-completion-dedup-test.mjs +157 -0
- package/scripts/execution-pending-resume-kick-test.mjs +57 -2
- package/scripts/execution-resume-esc-integration-test.mjs +176 -0
- package/scripts/explore-bench.mjs +38 -2
- package/scripts/explore-prompt-policy-test.mjs +152 -11
- package/scripts/find-fuzzy-hidden-test.mjs +122 -0
- package/scripts/internal-comms-bench-test.mjs +226 -0
- package/scripts/internal-comms-bench.mjs +185 -58
- package/scripts/internal-comms-smoke.mjs +171 -23
- package/scripts/live-worker-smoke.mjs +38 -2
- package/scripts/memory-cycle-routing-test.mjs +111 -0
- package/scripts/memory-rule-contract-test.mjs +93 -0
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/rg-runner-test.mjs +240 -0
- package/scripts/routing-corpus-test.mjs +349 -0
- package/scripts/routing-corpus.mjs +211 -32
- package/scripts/session-orphan-sweep-test.mjs +83 -0
- package/scripts/steering-drain-buckets-test.mjs +316 -2
- package/scripts/tool-smoke.mjs +21 -13
- package/scripts/tool-tui-presentation-test.mjs +202 -0
- package/scripts/tui-transcript-perf-test.mjs +279 -0
- package/src/agents/heavy-worker/AGENT.md +10 -7
- package/src/agents/reviewer/AGENT.md +6 -4
- package/src/agents/worker/AGENT.md +7 -5
- package/src/rules/agent/00-common.md +4 -4
- package/src/rules/agent/00-core.md +11 -14
- package/src/rules/agent/20-skip-protocol.md +3 -3
- package/src/rules/agent/30-explorer.md +50 -60
- package/src/rules/agent/40-cycle1-agent.md +15 -24
- package/src/rules/agent/41-cycle2-agent.md +33 -57
- package/src/rules/agent/42-cycle3-agent.md +28 -42
- package/src/rules/lead/01-general.md +7 -10
- package/src/rules/lead/lead-brief.md +11 -14
- package/src/rules/lead/lead-tool.md +6 -5
- package/src/rules/shared/01-tool.md +44 -45
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -1
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +20 -5
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +21 -1
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +17 -38
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +17 -8
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +270 -78
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -0
- package/src/runtime/agent/orchestrator/session/manager/delivered-completions.mjs +123 -0
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +67 -2
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +232 -43
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +43 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +27 -13
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -20
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +118 -53
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +106 -29
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-range-index.mjs +3 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +33 -2
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +151 -64
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +37 -13
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +84 -49
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +172 -23
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +3 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +68 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +17 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +24 -10
- package/src/runtime/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -7
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -4
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +4 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +12 -2
- package/src/runtime/shared/buffered-appender.mjs +13 -2
- package/src/runtime/shared/tool-primitives.mjs +4 -1
- package/src/runtime/shared/tool-status.mjs +27 -0
- package/src/runtime/shared/tool-surface.mjs +6 -3
- package/src/session-runtime/config-helpers.mjs +14 -0
- package/src/session-runtime/context-status.mjs +1 -0
- package/src/session-runtime/effort.mjs +6 -2
- package/src/session-runtime/lifecycle-api.mjs +4 -0
- package/src/session-runtime/model-recency.mjs +5 -2
- package/src/session-runtime/runtime-core.mjs +36 -2
- package/src/session-runtime/session-turn-api.mjs +12 -0
- package/src/session-runtime/tool-catalog.mjs +34 -0
- package/src/standalone/agent-tool/notify.mjs +13 -0
- package/src/standalone/agent-tool.mjs +53 -70
- package/src/standalone/explore-tool.mjs +6 -7
- package/src/tui/App.jsx +33 -1
- package/src/tui/app/model-options.mjs +5 -3
- package/src/tui/app/model-picker.mjs +12 -24
- package/src/tui/app/transcript-window.mjs +10 -10
- package/src/tui/app/use-transcript-window.mjs +38 -56
- package/src/tui/components/ToolExecution.jsx +11 -6
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +24 -10
- package/src/tui/components/tool-execution/text-format.mjs +10 -19
- package/src/tui/dist/index.mjs +866 -300
- package/src/tui/engine/agent-job-feed.mjs +216 -19
- package/src/tui/engine/agent-response-tail.mjs +68 -0
- package/src/tui/engine/notification-plan.mjs +16 -0
- package/src/tui/engine/session-api.mjs +14 -2
- package/src/tui/engine/session-flow.mjs +22 -2
- package/src/tui/engine/tool-card-results.mjs +64 -37
- package/src/tui/engine/tool-result-status.mjs +75 -21
- package/src/tui/engine/turn.mjs +172 -77
- package/src/tui/engine.mjs +199 -39
- package/src/tui/index.jsx +2 -2
- package/src/workflows/bench/WORKFLOW.md +25 -35
- package/src/workflows/default/WORKFLOW.md +38 -32
- package/src/workflows/solo/WORKFLOW.md +19 -22
- package/scripts/_jitter-fuzz.mjs +0 -44410
- package/scripts/_jitter-fuzz2.mjs +0 -44400
- package/scripts/_jitter-probe.mjs +0 -44397
- package/scripts/_jp2.mjs +0 -45614
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// Process-local delivered-completion registry.
|
|
2
|
+
//
|
|
3
|
+
// Purpose: a task-completion body that was already injected into the active TUI
|
|
4
|
+
// loop and ACKed (modelVisibleDelivered) must never ALSO be enqueued into — or
|
|
5
|
+
// drained from — the session pending queue one turn later. The TUI ack and the
|
|
6
|
+
// racing background/fallback enqueue run on different code paths within the SAME
|
|
7
|
+
// process, so this in-memory registry lets an enqueue/drain site recognize "this
|
|
8
|
+
// completion was already delivered" and skip it.
|
|
9
|
+
//
|
|
10
|
+
// Recorded keys (either one matching = already delivered):
|
|
11
|
+
// - meta.execution_id (when present)
|
|
12
|
+
// - a hash of the model-visible completion text
|
|
13
|
+
//
|
|
14
|
+
// Bounded + TTL'd so a long-lived process never accumulates unbounded entries.
|
|
15
|
+
// Both record and a matching lookup REFRESH the entry (sliding TTL), so an
|
|
16
|
+
// ACKed completion survives across long-idle turns as long as it keeps being
|
|
17
|
+
// checked. Headless/API sessions never ack, so the registry stays empty for
|
|
18
|
+
// them and marked in-memory completions drain exactly once (behavior unchanged).
|
|
19
|
+
//
|
|
20
|
+
// Residual risk: with a 6h sliding TTL the only remaining eviction path is the
|
|
21
|
+
// bounded-size cap — if MORE than DELIVERED_MAX_ENTRIES distinct completions are
|
|
22
|
+
// delivered before an older ACKed key is next checked, that oldest key is
|
|
23
|
+
// evicted and its (now un-refreshed) completion could enqueue/drain a second
|
|
24
|
+
// time. This requires >512 live completions between a delivery and its drain,
|
|
25
|
+
// which is far outside normal single-process interactive use.
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
27
|
+
|
|
28
|
+
const DELIVERED_TTL_MS = 6 * 60 * 60 * 1000; // 6h, sliding (refreshed on record + hit)
|
|
29
|
+
const DELIVERED_MAX_ENTRIES = 512;
|
|
30
|
+
// key -> expiresAt (ms). Insertion order = age order (Map guarantee), so the
|
|
31
|
+
// oldest key is always the first — used for bounded-size eviction.
|
|
32
|
+
const _delivered = new Map();
|
|
33
|
+
|
|
34
|
+
export function hashCompletionText(text) {
|
|
35
|
+
const value = String(text ?? '').trim();
|
|
36
|
+
if (!value) return null;
|
|
37
|
+
return createHash('sha1').update(value).digest('hex');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function execKey(executionId) {
|
|
41
|
+
const id = executionId == null ? '' : String(executionId).trim();
|
|
42
|
+
return id ? `id:${id}` : null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function textKey(text) {
|
|
46
|
+
const h = hashCompletionText(text);
|
|
47
|
+
return h ? `tx:${h}` : null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function pruneExpired(now) {
|
|
51
|
+
for (const [key, expiresAt] of _delivered) {
|
|
52
|
+
if (expiresAt <= now) _delivered.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function enforceBound() {
|
|
57
|
+
while (_delivered.size > DELIVERED_MAX_ENTRIES) {
|
|
58
|
+
const oldest = _delivered.keys().next().value;
|
|
59
|
+
if (oldest === undefined) break;
|
|
60
|
+
_delivered.delete(oldest);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Record a delivered completion under its execution_id (when present) and its
|
|
65
|
+
// model-visible text hash. Both keys point at the same expiry so a later
|
|
66
|
+
// enqueue/drain matching EITHER is recognized as already delivered. Re-recording
|
|
67
|
+
// an existing key refreshes its expiry (sliding TTL).
|
|
68
|
+
export function recordDeliveredCompletion({ executionId, text } = {}) {
|
|
69
|
+
const now = Date.now();
|
|
70
|
+
pruneExpired(now);
|
|
71
|
+
const expiresAt = now + DELIVERED_TTL_MS;
|
|
72
|
+
let recorded = false;
|
|
73
|
+
for (const key of [execKey(executionId), textKey(text)]) {
|
|
74
|
+
if (!key) continue;
|
|
75
|
+
_delivered.delete(key); // re-insert to refresh age order
|
|
76
|
+
_delivered.set(key, expiresAt);
|
|
77
|
+
recorded = true;
|
|
78
|
+
}
|
|
79
|
+
if (recorded) enforceBound();
|
|
80
|
+
return recorded;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function isDeliveredCompletion({ executionId, text } = {}) {
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const keys = [execKey(executionId), textKey(text)].filter(Boolean);
|
|
86
|
+
let hit = false;
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
const expiresAt = _delivered.get(key);
|
|
89
|
+
if (expiresAt === undefined) continue;
|
|
90
|
+
if (expiresAt <= now) { _delivered.delete(key); continue; }
|
|
91
|
+
hit = true;
|
|
92
|
+
}
|
|
93
|
+
if (!hit) return false;
|
|
94
|
+
// Sliding refresh: extend every present key (and move it to the age tail) so
|
|
95
|
+
// a long-idle drain that checks it later still recognizes it as delivered.
|
|
96
|
+
const expiresAt = now + DELIVERED_TTL_MS;
|
|
97
|
+
for (const key of keys) {
|
|
98
|
+
if (!_delivered.has(key)) continue;
|
|
99
|
+
_delivered.delete(key);
|
|
100
|
+
_delivered.set(key, expiresAt);
|
|
101
|
+
}
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// One diagnostic stderr line per dedup skip, naming the site
|
|
106
|
+
// (mirror/fallback/notify-enqueue/drain) so the actual racing edge is
|
|
107
|
+
// empirically confirmable in logs.
|
|
108
|
+
export function logDuplicateSkip(site, { executionId, text } = {}) {
|
|
109
|
+
try {
|
|
110
|
+
const id = executionId == null ? '' : String(executionId).trim();
|
|
111
|
+
const h = hashCompletionText(text);
|
|
112
|
+
process.stderr.write(
|
|
113
|
+
`[completion-dedup] skip site=${site}`
|
|
114
|
+
+ `${id ? ` execution_id=${id}` : ''}`
|
|
115
|
+
+ `${h ? ` hash=${h.slice(0, 12)}` : ''}\n`,
|
|
116
|
+
);
|
|
117
|
+
} catch { /* ignore */ }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Test-only reset hook.
|
|
121
|
+
export function _clearDeliveredCompletions() {
|
|
122
|
+
_delivered.clear();
|
|
123
|
+
}
|
|
@@ -13,10 +13,22 @@ import { nonNegativeIntEnv } from './env-utils.mjs';
|
|
|
13
13
|
const CLEANUP_INTERVAL_MS = nonNegativeIntEnv('MIXDOG_SESSION_CLEANUP_INTERVAL_MS', 5 * 60 * 1000); // check every 5 minutes
|
|
14
14
|
const CLEANUP_INITIAL_DELAY_MS = nonNegativeIntEnv('MIXDOG_SESSION_CLEANUP_INITIAL_DELAY_MS', CLEANUP_INTERVAL_MS > 0 ? CLEANUP_INTERVAL_MS : 0);
|
|
15
15
|
const CLEANUP_SLOW_LOG_MS = nonNegativeIntEnv('MIXDOG_SESSION_CLEANUP_SLOW_LOG_MS', 250);
|
|
16
|
-
|
|
16
|
+
// Tombstone unlink TTL. The guarded resurrection race (temp-write→rename inside
|
|
17
|
+
// _doSave) resolves in microseconds, so 1h is still vastly longer than any
|
|
18
|
+
// realistic in-flight ask race — while short enough that matured tombstones are
|
|
19
|
+
// reclaimed promptly instead of accumulating for a full day.
|
|
20
|
+
const TOMBSTONE_MAX_AGE_MS = 60 * 60 * 1000; // 1h
|
|
17
21
|
let _cleanupTimer = null;
|
|
18
22
|
let _cleanupInitialTimer = null;
|
|
19
23
|
|
|
24
|
+
// A session is "live" when it still owns a non-closed runtime entry. Passed to
|
|
25
|
+
// the retention cap so the active/current and any in-flight session is never
|
|
26
|
+
// pruned by the open-session max-age/max-count bounds.
|
|
27
|
+
function _isSessionLive(id) {
|
|
28
|
+
const entry = _getRuntimeEntry(id);
|
|
29
|
+
return !!(entry && entry.closed !== true);
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
function _previewIds(items, limit = 5) {
|
|
21
33
|
const ids = (items || []).slice(0, limit).map((item) => item.id).filter(Boolean);
|
|
22
34
|
if (ids.length === 0) return '';
|
|
@@ -29,6 +41,7 @@ function sweepIdleSessions({ includeTombstones = true } = {}) {
|
|
|
29
41
|
try {
|
|
30
42
|
const result = sweepStaleSessions({
|
|
31
43
|
tombstoneMaxAgeMs: includeTombstones ? TOMBSTONE_MAX_AGE_MS : 0,
|
|
44
|
+
isSessionLive: _isSessionLive,
|
|
32
45
|
});
|
|
33
46
|
const {
|
|
34
47
|
cleaned,
|
|
@@ -81,7 +94,7 @@ function sweepIdleSessions({ includeTombstones = true } = {}) {
|
|
|
81
94
|
* Rationale: closeSession() leaves the tombstone on disk as the authoritative
|
|
82
95
|
* resurrection-blocker for racing saveSession() calls. That race resolves in
|
|
83
96
|
* microseconds (the window inside _doSave between temp write and rename), so
|
|
84
|
-
*
|
|
97
|
+
* 1h is vastly safe. After the TTL expires we reclaim the disk slot.
|
|
85
98
|
*
|
|
86
99
|
* Uses `getStoredSessionsRaw()` rather than `listStoredSessions()` because the
|
|
87
100
|
* latter's inline 30-min idle cleanup would race-unlink tombstones before we
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
// Steering / pending-message queue with sync buffered + atomic-file persistence.
|
|
2
2
|
// Extracted verbatim from manager.mjs (behavior-preserving).
|
|
3
3
|
import { join } from 'path';
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
4
5
|
import { resolvePluginData } from '../../../../shared/plugin-paths.mjs';
|
|
5
6
|
import { updateJsonAtomicSync, updateJsonAtomic } from '../../../../shared/atomic-file.mjs';
|
|
6
7
|
import { promptContentText, isInternalRuntimeNotificationText } from './prompt-utils.mjs';
|
|
7
8
|
import { loadSession } from '../store.mjs';
|
|
9
|
+
import { isDeliveredCompletion, logDuplicateSkip } from './delivered-completions.mjs';
|
|
8
10
|
|
|
9
11
|
const _sessionPendingMessages = new Map();
|
|
10
12
|
const PENDING_MESSAGES_FILE = 'session-pending-messages.json';
|
|
@@ -24,6 +26,31 @@ function isCompletionNotificationEntry(entry) {
|
|
|
24
26
|
&& entry.notificationKind === COMPLETION_NOTIFICATION_KIND;
|
|
25
27
|
}
|
|
26
28
|
|
|
29
|
+
// Pre-marker completion notifications were persisted as plain strings,
|
|
30
|
+
// indistinguishable from genuine user/steering messages except by their
|
|
31
|
+
// model-visible wrapper shape. Such stale strings must never replay into a
|
|
32
|
+
// resumed session, but they carry no notificationKind marker, so the marker
|
|
33
|
+
// check alone leaves them behind. Because this is a SILENT-drop path, the
|
|
34
|
+
// shared lenient wrapper detector is too broad (a user message quoting a
|
|
35
|
+
// completion card could be dropped), so this uses its OWN strict recognizer:
|
|
36
|
+
// the string must be a verbatim full-card paste and nothing else —
|
|
37
|
+
// (1) start with the exact instruction preamble + "\n\nResult:\n",
|
|
38
|
+
// (2) have EVERY non-empty body line quoted with "> " (100%),
|
|
39
|
+
// (3) carry no extra leading/trailing prose (whitespace-trim only).
|
|
40
|
+
// Conservative by design: a false negative just keeps a legacy string, but a
|
|
41
|
+
// false positive on genuine user text would silently drop a real message.
|
|
42
|
+
const LEGACY_COMPLETION_CARD_PREAMBLE_RE = /^The async \S+ task \S+ has finished \([^)]*\) - review this result in your next step\.\n\nResult:\n/;
|
|
43
|
+
function isLegacyUnmarkedCompletionNotification(entry) {
|
|
44
|
+
if (typeof entry !== 'string') return false;
|
|
45
|
+
const value = entry.trim();
|
|
46
|
+
const match = LEGACY_COMPLETION_CARD_PREAMBLE_RE.exec(value);
|
|
47
|
+
if (!match) return false;
|
|
48
|
+
const body = value.slice(match[0].length);
|
|
49
|
+
const lines = body.split(/\r?\n/).filter((line) => line.length > 0);
|
|
50
|
+
if (lines.length === 0) return false;
|
|
51
|
+
return lines.every((line) => line.startsWith('> '));
|
|
52
|
+
}
|
|
53
|
+
|
|
27
54
|
// Canonical completion-enqueue tagger. Every deferred tool/agent completion
|
|
28
55
|
// notification MUST be enqueued through this so drain can discard it on resume
|
|
29
56
|
// (never replay out-of-order). Pass the model-visible completion text (or an
|
|
@@ -393,8 +420,21 @@ export function drainPendingMessages(sessionId) {
|
|
|
393
420
|
// in-memory queue is empty, so keeping it only ever delivers live entries.
|
|
394
421
|
// Genuine user/steering messages carry no marker and are kept in order in
|
|
395
422
|
// both paths.
|
|
396
|
-
|
|
397
|
-
|
|
423
|
+
// Drain-time belt: drop MARKED in-memory completion entries whose text hash
|
|
424
|
+
// was already delivered+ACKed (TUI execution-ui) this process — those would
|
|
425
|
+
// double-inject next turn. Only marked completion entries are eligible;
|
|
426
|
+
// genuine user/steering entries carry no marker and are always kept.
|
|
427
|
+
const memoryKept = memory.filter((m) => {
|
|
428
|
+
if (!isCompletionNotificationEntry(m)) return true;
|
|
429
|
+
const text = pendingMessageText(m);
|
|
430
|
+
if (text && isDeliveredCompletion({ text })) {
|
|
431
|
+
logDuplicateSkip('drain', { text });
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
});
|
|
436
|
+
const persistedKept = persisted.filter((m) => !isCompletionNotificationEntry(m)
|
|
437
|
+
&& !isLegacyUnmarkedCompletionNotification(m));
|
|
398
438
|
const memoryVisible = modelVisiblePendingMessages(memoryKept);
|
|
399
439
|
const persistedVisible = modelVisiblePendingMessages(persistedKept);
|
|
400
440
|
if (memoryVisible.length === 0) return persistedVisible;
|
|
@@ -419,6 +459,31 @@ export function drainPendingMessages(sessionId) {
|
|
|
419
459
|
return out;
|
|
420
460
|
}
|
|
421
461
|
|
|
462
|
+
// Snapshot queued entries without draining them. Compaction uses this to keep
|
|
463
|
+
// sidecars referenced by a message that is waiting for the next turn, whether
|
|
464
|
+
// it is still in memory, buffered for persistence, or already on disk.
|
|
465
|
+
export function _getPendingMessagesForSession(sessionId) {
|
|
466
|
+
if (!isValidPendingSessionId(sessionId)) return [];
|
|
467
|
+
const queued = [
|
|
468
|
+
...(_sessionPendingMessages.get(sessionId) || []),
|
|
469
|
+
...(_pendingPersistBuffers.get(sessionId) || []),
|
|
470
|
+
];
|
|
471
|
+
let raw;
|
|
472
|
+
try {
|
|
473
|
+
raw = readFileSync(pendingMessagesPath(), 'utf8');
|
|
474
|
+
} catch (err) {
|
|
475
|
+
if (err?.code === 'ENOENT') return queued;
|
|
476
|
+
throw err;
|
|
477
|
+
}
|
|
478
|
+
try {
|
|
479
|
+
const persisted = normalizePendingStore(JSON.parse(raw)).sessions[sessionId];
|
|
480
|
+
if (Array.isArray(persisted)) queued.push(...persisted);
|
|
481
|
+
} catch (err) {
|
|
482
|
+
throw err;
|
|
483
|
+
}
|
|
484
|
+
return queued;
|
|
485
|
+
}
|
|
486
|
+
|
|
422
487
|
// Cleanup hook for closeSession — drop the in-memory queue and buffered-persist
|
|
423
488
|
// entry so both Maps do not accumulate one entry per closed session.
|
|
424
489
|
export function _dropPendingMessageState(id, { clearPersisted = true } = {}) {
|
|
@@ -24,7 +24,7 @@ import { _closeBashSessionLazy } from './runtime-loaders.mjs';
|
|
|
24
24
|
* and recreates the session in its pre-close state.
|
|
25
25
|
*
|
|
26
26
|
* Long-term cleanup: `sweepTombstones()` below unlinks tombstones older than
|
|
27
|
-
* TOMBSTONE_MAX_AGE_MS (
|
|
27
|
+
* TOMBSTONE_MAX_AGE_MS (1h — vastly longer than the microsecond in-flight race).
|
|
28
28
|
*/
|
|
29
29
|
export function closeSession(id, reason = 'manual', opts = {}) {
|
|
30
30
|
// tombstone=false: detach runtime resources (heartbeat, bash shells,
|
|
@@ -163,6 +163,8 @@ export function bumpUsageMetricsTurnId(session) {
|
|
|
163
163
|
if (!session || typeof session !== 'object') return 0;
|
|
164
164
|
const next = (Number(session.usageMetricsTurnId) || 0) + 1;
|
|
165
165
|
session.usageMetricsTurnId = next;
|
|
166
|
+
const seen = _metricSeenIter.get(session.id);
|
|
167
|
+
if (seen) seen.clear();
|
|
166
168
|
return next;
|
|
167
169
|
}
|
|
168
170
|
|