mixdog 0.9.92 → 0.9.94
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/README.md +147 -51
- package/package.json +6 -5
- package/scripts/code-graph-description-contract.mjs +6 -8
- package/scripts/tmp-cdp-errors.mjs +41 -0
- package/scripts/tmp-cdp-inspect.mjs +41 -0
- package/scripts/tui-transcript-jitter-harness.mjs +2 -18
- package/src/rules/agent/00-core.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -16
- package/src/rules/lead/01-general.md +1 -0
- package/src/rules/shared/01-tool.md +27 -25
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +16 -3
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +22 -6
- package/src/runtime/agent/orchestrator/agent-trace.mjs +17 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +8 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +9 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +79 -21
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +40 -19
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +18 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +75 -15
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +44 -8
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +3 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +22 -5
- package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +42 -2
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +35 -29
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +11 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +5 -6
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +31 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +60 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +60 -31
- package/src/runtime/agent/orchestrator/session/manager.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +12 -3
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +17 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +30 -0
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +119 -109
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +98 -3
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -21
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +8 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +16 -10
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +12 -3
- package/src/runtime/agent/orchestrator/tools/builtin/grep-formatting.mjs +22 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +491 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +91 -13
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +90 -27
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +19 -9
- package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +7 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +32 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +16 -1
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +543 -19
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +48 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +71 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +4 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +51 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +6 -17
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -3
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +47 -14
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +100 -0
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +6 -5
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +32 -2
- package/src/runtime/channels/backends/discord-gateway.mjs +6 -32
- package/src/runtime/channels/lib/inbound-handler.mjs +19 -3
- package/src/runtime/channels/lib/scheduler.mjs +51 -3
- package/src/runtime/channels/lib/worker-main.mjs +4 -0
- package/src/runtime/channels/tool-defs.mjs +4 -2
- package/src/runtime/memory/lib/query-handlers.mjs +11 -3
- package/src/runtime/memory/lib/tool-call-handler.mjs +16 -1
- package/src/runtime/memory/tool-defs.mjs +5 -5
- package/src/runtime/shared/background-tasks.mjs +10 -3
- package/src/runtime/shared/channel-notification-routing.mjs +8 -2
- package/src/runtime/shared/child-spawn-gate.mjs +50 -26
- package/src/runtime/shared/llm/http-agent.mjs +11 -0
- package/src/runtime/shared/task-notification-envelope.mjs +11 -2
- package/src/runtime/shared/tool-card-model.mjs +6 -2
- package/src/runtime/shared/tool-surface.mjs +7 -2
- package/src/session-runtime/lifecycle-api.mjs +26 -1
- package/src/session-runtime/provider-usage.mjs +26 -2
- package/src/session-runtime/tool-catalog-data.mjs +5 -2
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/tag-registry.mjs +5 -1
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/tui/app/use-transcript-window.mjs +7 -1
- package/src/tui/components/Spinner.jsx +18 -9
- package/src/tui/dist/index.mjs +179 -70
- package/src/tui/engine/agent-envelope.mjs +52 -3
- package/src/tui/engine/live-share.mjs +23 -3
- package/src/tui/engine/session-api.mjs +7 -0
- package/src/tui/engine/turn.mjs +85 -13
- package/src/tui/engine.mjs +32 -47
- package/src/tui/index.jsx +7 -0
- package/src/workflows/solo/WORKFLOW.md +0 -6
- package/src/workflows/solo-bench/WORKFLOW.md +17 -0
|
@@ -19,16 +19,41 @@ const PENDING_MESSAGES_MODE = 0o600;
|
|
|
19
19
|
const PENDING_ORPHAN_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
20
20
|
const PENDING_ORPHAN_GRACE_MS = 60 * 60 * 1000;
|
|
21
21
|
// Replay window for genuine user/steering entries. A cross-surface submit is
|
|
22
|
-
// meant for a LIVE owner;
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
22
|
+
// meant for a LIVE owner; entries that predate this process and exceeded the
|
|
23
|
+
// window are still DELIVERED (CC parity, reference messageQueueManager.ts:
|
|
24
|
+
// queued user input is never silently discarded), but annotated with an
|
|
25
|
+
// explicit late-delivery header so a session resumed hours later reads them
|
|
26
|
+
// as clearly-late input instead of a surprise self-injection.
|
|
26
27
|
const STALE_USER_INJECTION_TTL_MS = 30 * 60 * 1000;
|
|
27
28
|
|
|
29
|
+
// Ownership epoch for the staleness gate below: entries enqueued while THIS
|
|
30
|
+
// process was already alive were aimed at a live owner that still exists.
|
|
31
|
+
const _PENDING_PROCESS_START_MS = Date.now();
|
|
32
|
+
|
|
28
33
|
function isStaleUserInjection(entry, now = Date.now()) {
|
|
29
34
|
if (isCompletionNotificationEntry(entry)) return false;
|
|
30
35
|
const enqueuedAt = Number(entry?.enqueuedAt) || 0;
|
|
31
|
-
|
|
36
|
+
if (enqueuedAt <= 0) return false;
|
|
37
|
+
// A submit that arrived AFTER this owner process booted is CURRENT input
|
|
38
|
+
// the owner was merely too busy to take yet (observed: remote sends
|
|
39
|
+
// silently discarded after 30m while the owner ground through long
|
|
40
|
+
// turns). It must deliver regardless of age. The stale window only
|
|
41
|
+
// guards entries that PREDATE this process — the resumed-session replay
|
|
42
|
+
// case the TTL was built for (surprise self-injection on re-entry).
|
|
43
|
+
if (enqueuedAt >= _PENDING_PROCESS_START_MS) return false;
|
|
44
|
+
return (now - enqueuedAt) > STALE_USER_INJECTION_TTL_MS;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// CC parity: never silently discard queued user input. A stale entry is
|
|
48
|
+
// delivered with this explicit age-annotated header so neither the user nor
|
|
49
|
+
// the model mistakes it for fresh input.
|
|
50
|
+
function lateDeliveryText(text, entry, now = Date.now()) {
|
|
51
|
+
const value = String(text ?? '');
|
|
52
|
+
if (!value.trim()) return value;
|
|
53
|
+
const enqueuedAt = Number(entry?.enqueuedAt) || 0;
|
|
54
|
+
const ageMinutes = Math.max(1, Math.round((now - enqueuedAt) / 60000));
|
|
55
|
+
const age = ageMinutes >= 120 ? `~${Math.round(ageMinutes / 60)}h` : `~${ageMinutes}m`;
|
|
56
|
+
return `[late delivery: queued ${age} ago, before the current session owner started]\n${value}`;
|
|
32
57
|
}
|
|
33
58
|
// Marker for deferred agent/tool *completion* notifications. Such entries must
|
|
34
59
|
// never be replayed into a later turn on session resume (out-of-order delivery
|
|
@@ -765,7 +790,6 @@ export function hydratePendingMessages(sessionId, options = {}) {
|
|
|
765
790
|
let hydrated = [];
|
|
766
791
|
let alreadyDelivered = [];
|
|
767
792
|
let staleLedgerEntries = [];
|
|
768
|
-
const staleUserEntries = [];
|
|
769
793
|
const ledgerSession = loadSession(sessionId);
|
|
770
794
|
// Durable lifecycle epoch this hydration started under. Publishing claims the
|
|
771
795
|
// durable entries into THIS process's memory, so it must be revalidated
|
|
@@ -797,16 +821,19 @@ export function hydratePendingMessages(sessionId, options = {}) {
|
|
|
797
821
|
return false;
|
|
798
822
|
}
|
|
799
823
|
if (!id || inDelivery.has(id) || acked.has(id)) return false;
|
|
800
|
-
// Stale genuine user/steering entries must not surprise-inject
|
|
801
|
-
// into a session resumed long after they were queued (user:
|
|
802
|
-
// "re-entering the session suddenly injected old messages").
|
|
803
|
-
// Completion entries keep their own resume-drop policy.
|
|
804
|
-
if (isStaleUserInjection(entry)) {
|
|
805
|
-
staleUserEntries.push(entry);
|
|
806
|
-
return false;
|
|
807
|
-
}
|
|
808
824
|
return true;
|
|
809
825
|
});
|
|
826
|
+
// CC parity: stale genuine user/steering entries DELIVER with a
|
|
827
|
+
// late-delivery header instead of being silently dropped (the old
|
|
828
|
+
// behavior discarded them; user report: remote/steering sends
|
|
829
|
+
// silently ignored around owner restarts). Completion entries
|
|
830
|
+
// keep their own resume-drop policy (drain discards them).
|
|
831
|
+
for (const entry of hydrated) {
|
|
832
|
+
if (!isStaleUserInjection(entry)) continue;
|
|
833
|
+
if (typeof entry.message === 'string') {
|
|
834
|
+
entry.message = lateDeliveryText(entry.message, entry);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
810
837
|
// Read-only claim: durable data remains until successful delivery
|
|
811
838
|
// acknowledges these exact ids. A crash here therefore redelivers.
|
|
812
839
|
return undefined;
|
|
@@ -828,13 +855,6 @@ export function hydratePendingMessages(sessionId, options = {}) {
|
|
|
828
855
|
runPendingTestHook('hydrate:betweenCleanups', { sessionId });
|
|
829
856
|
if (pendingLifecycleInvalidated(sessionId, startToken)) return 0;
|
|
830
857
|
}
|
|
831
|
-
if (staleUserEntries.length > 0) {
|
|
832
|
-
try { process.stderr.write(`[session] dropped ${staleUserEntries.length} stale queued message(s) (older than ${Math.round(STALE_USER_INJECTION_TTL_MS / 60000)}m) sessionId=${sessionId}\n`); } catch {}
|
|
833
|
-
const cleaned = await acknowledgePendingMessages(sessionId, staleUserEntries, { expectedToken: startToken });
|
|
834
|
-
if (cleaned) cleanupConfirmed.push(...staleUserEntries);
|
|
835
|
-
runPendingTestHook('hydrate:betweenCleanups', { sessionId });
|
|
836
|
-
if (pendingLifecycleInvalidated(sessionId, startToken)) return 0;
|
|
837
|
-
}
|
|
838
858
|
if (cleanupConfirmed.length > 0) {
|
|
839
859
|
try {
|
|
840
860
|
// One session save prunes both IDs whose replay spool was removed
|
|
@@ -968,7 +988,19 @@ function modelVisiblePendingMessages(messages) {
|
|
|
968
988
|
}
|
|
969
989
|
|
|
970
990
|
export function _mergePendingMessageEntries(entries) {
|
|
971
|
-
|
|
991
|
+
// CC-parity delivery priority (reference messageQueueManager.ts: user
|
|
992
|
+
// input enqueues at 'next', task notifications at 'later', and dequeue
|
|
993
|
+
// always serves 'next' first). Our single merged turn message is the
|
|
994
|
+
// analogue of that dequeue order: genuine user/steering entries are
|
|
995
|
+
// merged BEFORE deferred completion notifications so queued user input
|
|
996
|
+
// is never buried under system notification text. FIFO is preserved
|
|
997
|
+
// within each group (stable partition).
|
|
998
|
+
const source = Array.isArray(entries) ? entries : [];
|
|
999
|
+
const ordered = [
|
|
1000
|
+
...source.filter((entry) => !isCompletionNotificationEntry(entry)),
|
|
1001
|
+
...source.filter(isCompletionNotificationEntry),
|
|
1002
|
+
];
|
|
1003
|
+
const normalized = ordered
|
|
972
1004
|
.map(normalizePendingMessageEntry)
|
|
973
1005
|
.filter(Boolean);
|
|
974
1006
|
if (normalized.length === 0) return null;
|
|
@@ -1106,7 +1138,6 @@ export function drainForeignUserInjections(sessionId) {
|
|
|
1106
1138
|
}
|
|
1107
1139
|
} catch { /* ledger unavailable — in-memory sets still guard */ }
|
|
1108
1140
|
const taken = [];
|
|
1109
|
-
let droppedStale = 0;
|
|
1110
1141
|
// The mtime memo may ONLY be armed by a scan that actually reached a
|
|
1111
1142
|
// lifecycle-valid decision under the spool lock. Arming it upfront meant a
|
|
1112
1143
|
// drain refused in-lock (a close/detach/reopen landing in the window) still
|
|
@@ -1137,14 +1168,15 @@ export function drainForeignUserInjections(sessionId) {
|
|
|
1137
1168
|
&& !isCompletionNotificationEntry(entry)
|
|
1138
1169
|
&& !isLegacyUnmarkedCompletionNotification(text)
|
|
1139
1170
|
&& text && !isInternalRuntimeNotificationText(text);
|
|
1140
|
-
//
|
|
1141
|
-
//
|
|
1142
|
-
//
|
|
1143
|
-
|
|
1171
|
+
// CC parity: stale foreign submits still deliver, carrying a
|
|
1172
|
+
// late-delivery header instead of being silently removed
|
|
1173
|
+
// (user report: remote sends silently discarded around owner
|
|
1174
|
+
// restarts).
|
|
1175
|
+
if (foreignUser && isStaleUserInjection(entry)) taken.push(lateDeliveryText(text, entry));
|
|
1144
1176
|
else if (foreignUser) taken.push(text);
|
|
1145
1177
|
else kept.push(entry);
|
|
1146
1178
|
}
|
|
1147
|
-
if (taken.length === 0
|
|
1179
|
+
if (taken.length === 0) return undefined;
|
|
1148
1180
|
if (kept.length > 0) next.sessions[sessionId] = kept;
|
|
1149
1181
|
else {
|
|
1150
1182
|
delete next.sessions[sessionId];
|
|
@@ -1158,9 +1190,6 @@ export function drainForeignUserInjections(sessionId) {
|
|
|
1158
1190
|
return [];
|
|
1159
1191
|
}
|
|
1160
1192
|
if (lifecycleDecided) _rememberForeignSpoolScan(sessionId, mtime);
|
|
1161
|
-
if (droppedStale > 0) {
|
|
1162
|
-
try { process.stderr.write(`[session] dropped ${droppedStale} stale foreign submit(s) (older than ${Math.round(STALE_USER_INJECTION_TTL_MS / 60000)}m) sessionId=${sessionId}\n`); } catch {}
|
|
1163
|
-
}
|
|
1164
1193
|
return taken;
|
|
1165
1194
|
}
|
|
1166
1195
|
|
|
@@ -126,7 +126,7 @@ export {
|
|
|
126
126
|
updateSessionManualTitle,
|
|
127
127
|
flushSessionMetrics,
|
|
128
128
|
} from './manager/session-crud.mjs';
|
|
129
|
-
export { deleteSession } from './store.mjs';
|
|
129
|
+
export { deleteSession, listOwnedAgentSessionIds } from './store.mjs';
|
|
130
130
|
// Read-only parsed-session access (desktop pane peek): no resume, no
|
|
131
131
|
// ownership, no liveness side effects.
|
|
132
132
|
export { loadSession } from './store.mjs';
|
|
@@ -211,9 +211,13 @@ export async function sendWithRecovery(ctx) {
|
|
|
211
211
|
// acknowledgement replays on a fresh request instead of
|
|
212
212
|
// failing the turn. Non-acked (or tool-bearing) shapes keep
|
|
213
213
|
// the explicit-failure contract below unchanged.
|
|
214
|
+
// NOTE: no classifyError gate here — an exposed stall is
|
|
215
|
+
// stamped unsafeToRetry, which the general classifier reads as
|
|
216
|
+
// terminal, but that unsafety is exactly what the retraction
|
|
217
|
+
// removes (observed live: make-mips-interpreter failed twice
|
|
218
|
+
// because this guard demanded 'transient' and never fired).
|
|
214
219
|
if (
|
|
215
220
|
transportRetriesUsed < TRANSPORT_RETRY_MAX
|
|
216
|
-
&& classifyError(sendErr) === 'transient'
|
|
217
221
|
&& await retractExposedTextForReplay()
|
|
218
222
|
) {
|
|
219
223
|
const waitMs = TRANSPORT_RETRY_BACKOFF_MS[transportRetriesUsed];
|
|
@@ -325,8 +329,13 @@ export async function sendWithRecovery(ctx) {
|
|
|
325
329
|
// send after a bounded wait instead of failing the turn.
|
|
326
330
|
if (
|
|
327
331
|
transportRetriesUsed < TRANSPORT_RETRY_MAX
|
|
328
|
-
&&
|
|
329
|
-
|
|
332
|
+
&& (
|
|
333
|
+
(outcome.replaySafe === true && classifyError(sendErr) === 'transient')
|
|
334
|
+
|| (
|
|
335
|
+
(classifyError(sendErr) === 'transient' || outcome.stallObserved === true)
|
|
336
|
+
&& await retractExposedTextForReplay()
|
|
337
|
+
)
|
|
338
|
+
)
|
|
330
339
|
) {
|
|
331
340
|
const waitMs = TRANSPORT_RETRY_BACKOFF_MS[transportRetriesUsed];
|
|
332
341
|
try {
|
|
@@ -64,6 +64,16 @@ const RESUMABLE_OPEN_MAX_COUNT = 300;
|
|
|
64
64
|
// idle this long — see the blank-scratch branch in sweepStaleSessions.
|
|
65
65
|
const BLANK_SCRATCH_MAX_AGE_MS = 60 * 60 * 1000; // 1h
|
|
66
66
|
|
|
67
|
+
/** Child-agent transcripts share their visible parent's retention boundary.
|
|
68
|
+
* Presence (including a tombstone or unreadable file) preserves the child;
|
|
69
|
+
* only proven parent absence releases it to ordinary cleanup. */
|
|
70
|
+
function retainedLinkedAgent(session) {
|
|
71
|
+
if (!session || !isAgentOwner(session)) return false;
|
|
72
|
+
const parentId = String(session.ownerSessionId || session.parentSessionId || '').trim();
|
|
73
|
+
if (!/^[A-Za-z0-9_-]+$/.test(parentId) || parentId === session.id) return false;
|
|
74
|
+
return probePath(sessionPath(parentId)).state !== PROBE_ABSENT;
|
|
75
|
+
}
|
|
76
|
+
|
|
67
77
|
|
|
68
78
|
export function listStoredSessions(options = {}) {
|
|
69
79
|
const dir = getStoreDir();
|
|
@@ -423,6 +433,13 @@ function* sweepStaleSessionSteps(ttlMs, options = {}) {
|
|
|
423
433
|
// messages), so there is no second, divergent parse of this file.
|
|
424
434
|
const actual = record.doc;
|
|
425
435
|
const diskClosed = record.closed === true || actual.status === 'closed';
|
|
436
|
+
if (retainedLinkedAgent(actual)) {
|
|
437
|
+
// Parent-owned agent transcripts are task history, not
|
|
438
|
+
// ephemeral worker cache. This covers both new open sessions
|
|
439
|
+
// and tombstones created by older Mixdog versions.
|
|
440
|
+
remaining++;
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
426
443
|
if (diskClosed) {
|
|
427
444
|
// A shared store can be tombstoned by another process while
|
|
428
445
|
// this process still owns an in-flight controller for the same
|
|
@@ -59,6 +59,106 @@ function cleanValue(value) {
|
|
|
59
59
|
return String(value || '').trim();
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
function archivedAgentNotification(content, sessionId) {
|
|
63
|
+
const text = typeof content === 'string' ? content : '';
|
|
64
|
+
const marker = '\n\nResult:\n';
|
|
65
|
+
const markerAt = text.indexOf(marker);
|
|
66
|
+
if (markerAt < 0 || !/The async agent task .* has finished \(/.test(text.slice(0, markerAt))) return null;
|
|
67
|
+
const lines = text.slice(markerAt + marker.length)
|
|
68
|
+
.split(/\r?\n/)
|
|
69
|
+
.map((line) => line.replace(/^>\s?/, ''));
|
|
70
|
+
const divider = lines.findIndex((line) => line.trim() === '');
|
|
71
|
+
if (divider < 0) return null;
|
|
72
|
+
const headers = new Map();
|
|
73
|
+
for (const line of lines.slice(0, divider)) {
|
|
74
|
+
const match = /^([A-Za-z][A-Za-z0-9_]*):\s*(.*?)\s*$/.exec(line);
|
|
75
|
+
if (match) headers.set(match[1], match[2]);
|
|
76
|
+
}
|
|
77
|
+
if (headers.get('surface') !== 'agent' || headers.get('sessionId') !== sessionId) return null;
|
|
78
|
+
const status = cleanValue(headers.get('status')).toLowerCase();
|
|
79
|
+
if (!/^(?:completed|failed|cancelled)$/.test(status)) return null;
|
|
80
|
+
const body = lines.slice(divider + 1).join('\n').trim();
|
|
81
|
+
if (!body) return null;
|
|
82
|
+
return {
|
|
83
|
+
body,
|
|
84
|
+
status,
|
|
85
|
+
tag: cleanValue(headers.get('tag') || headers.get('label')),
|
|
86
|
+
agent: cleanValue(headers.get('agent')),
|
|
87
|
+
provider: cleanValue(headers.get('provider')),
|
|
88
|
+
model: cleanValue(headers.get('model')),
|
|
89
|
+
effort: cleanValue(headers.get('effort')),
|
|
90
|
+
fast: cleanValue(headers.get('fast')).toLowerCase() === 'true',
|
|
91
|
+
finishedAt: Date.parse(cleanValue(headers.get('finished'))) || 0,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Legacy recovery for child transcripts already unlinked by terminal reaping.
|
|
96
|
+
* The parent owns one canonical body-carrying completion notification, so scan
|
|
97
|
+
* only files containing the exact child id and project the newest valid body. */
|
|
98
|
+
function readArchivedAgentResult(sessionId) {
|
|
99
|
+
const dir = join(dataDir(), 'sessions');
|
|
100
|
+
if (probePath(dir).state !== PROBE_PRESENT) return null;
|
|
101
|
+
let files;
|
|
102
|
+
try { files = readdirSync(dir).filter((file) => file.endsWith('.json')); }
|
|
103
|
+
catch { return null; }
|
|
104
|
+
let best = null;
|
|
105
|
+
for (const file of files) {
|
|
106
|
+
let raw;
|
|
107
|
+
try { raw = readFileSync(join(dir, file), 'utf8'); } catch { continue; }
|
|
108
|
+
if (!raw.includes(sessionId)) continue;
|
|
109
|
+
const record = readTopLevelLifecycleRecord(raw);
|
|
110
|
+
if (isLifecycleUnreadable(record) || record.id === sessionId) continue;
|
|
111
|
+
const parent = record.doc;
|
|
112
|
+
const messages = Array.isArray(parent.messages) ? parent.messages : [];
|
|
113
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
114
|
+
const message = messages[index];
|
|
115
|
+
if (message?.role !== 'user') continue;
|
|
116
|
+
const archived = archivedAgentNotification(message.content, sessionId);
|
|
117
|
+
if (!archived) continue;
|
|
118
|
+
const at = positiveNumber(
|
|
119
|
+
message?.meta?.transcript?.at,
|
|
120
|
+
archived.finishedAt || positiveNumber(parent.updatedAt),
|
|
121
|
+
);
|
|
122
|
+
if (best && best.at > at) continue;
|
|
123
|
+
best = { ...archived, at, parent };
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (!best) return null;
|
|
128
|
+
const text = `# Archived agent result\n\n${best.body}`;
|
|
129
|
+
return {
|
|
130
|
+
sessionId,
|
|
131
|
+
items: [{
|
|
132
|
+
id: `archived-agent-result:${sessionId}`,
|
|
133
|
+
kind: 'assistant',
|
|
134
|
+
text,
|
|
135
|
+
status: best.status,
|
|
136
|
+
...(best.at ? { at: best.at } : {}),
|
|
137
|
+
...(best.model ? { model: best.model } : {}),
|
|
138
|
+
...(best.provider ? { provider: best.provider } : {}),
|
|
139
|
+
...(best.agent ? { agent: best.agent } : {}),
|
|
140
|
+
}],
|
|
141
|
+
provider: best.provider,
|
|
142
|
+
model: best.model,
|
|
143
|
+
effort: best.effort,
|
|
144
|
+
fast: best.fast,
|
|
145
|
+
cwd: cleanValue(best.parent.cwd),
|
|
146
|
+
desktopSession: desktopSession(best.parent.desktopSession, best.parent.cwd),
|
|
147
|
+
workflow: null,
|
|
148
|
+
stats: {
|
|
149
|
+
currentContextTokens: 0,
|
|
150
|
+
currentEstimatedContextTokens: 0,
|
|
151
|
+
currentContextSource: null,
|
|
152
|
+
},
|
|
153
|
+
contextWindow: null,
|
|
154
|
+
rawContextWindow: null,
|
|
155
|
+
displayContextWindow: null,
|
|
156
|
+
autoCompactTokenLimit: null,
|
|
157
|
+
archivedAgentResult: true,
|
|
158
|
+
readOnlyDetachedAgent: false,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
62
162
|
function activeAgentWorker(row) {
|
|
63
163
|
const statuses = [row?.stage, row?.status]
|
|
64
164
|
.map(cleanValue)
|
|
@@ -421,6 +521,7 @@ export async function readStoredSessionTranscript(id, options = {}) {
|
|
|
421
521
|
// Same strict authority as the store, and the same fail-closed rule: an
|
|
422
522
|
// absent, unreadable, ambiguous or foreign record yields no transcript.
|
|
423
523
|
const read = readTextFile(join(dataDir(), 'sessions', `${sessionId}.json`));
|
|
524
|
+
if (read.state === PROBE_ABSENT) return readArchivedAgentResult(sessionId);
|
|
424
525
|
if (read.state !== PROBE_PRESENT) return null;
|
|
425
526
|
const record = readTopLevelLifecycleRecord(read.text);
|
|
426
527
|
if (isLifecycleUnreadable(record) || record.id !== sessionId) return null;
|
|
@@ -1188,6 +1188,36 @@ export function loadSession(id) {
|
|
|
1188
1188
|
return stored ? _ensureLifecycleFields(stored) : null;
|
|
1189
1189
|
}
|
|
1190
1190
|
|
|
1191
|
+
/** Strictly enumerate child-agent session files linked to one visible parent.
|
|
1192
|
+
* Used only by explicit parent deletion; ordinary close/context switches keep
|
|
1193
|
+
* the relationship intact. */
|
|
1194
|
+
export function listOwnedAgentSessionIds(ownerSessionId) {
|
|
1195
|
+
const ownerId = String(ownerSessionId || '').trim();
|
|
1196
|
+
if (!/^[A-Za-z0-9_-]+$/.test(ownerId)) return [];
|
|
1197
|
+
const dir = getStoreDir();
|
|
1198
|
+
if (probePath(dir).state !== PROBE_PRESENT) return [];
|
|
1199
|
+
let files;
|
|
1200
|
+
try {
|
|
1201
|
+
files = readdirSync(dir).filter((file) => file.endsWith('.json'));
|
|
1202
|
+
} catch {
|
|
1203
|
+
return [];
|
|
1204
|
+
}
|
|
1205
|
+
const ids = [];
|
|
1206
|
+
for (const file of files) {
|
|
1207
|
+
const candidateId = file.slice(0, -5);
|
|
1208
|
+
if (!candidateId || candidateId === ownerId || !/^[A-Za-z0-9_-]+$/.test(candidateId)) continue;
|
|
1209
|
+
try {
|
|
1210
|
+
const record = readTopLevelLifecycleRecord(readFileSync(join(dir, file), 'utf8'));
|
|
1211
|
+
if (isLifecycleUnreadable(record) || record.id !== candidateId) continue;
|
|
1212
|
+
const session = record.doc;
|
|
1213
|
+
if (!isAgentOwner(session)) continue;
|
|
1214
|
+
const linkedOwner = String(session.ownerSessionId || session.parentSessionId || '').trim();
|
|
1215
|
+
if (linkedOwner === ownerId) ids.push(candidateId);
|
|
1216
|
+
} catch { /* unreadable/vanished records are never deletion targets */ }
|
|
1217
|
+
}
|
|
1218
|
+
return ids;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1191
1221
|
|
|
1192
1222
|
export function deleteSession(id, options = {}) {
|
|
1193
1223
|
// Keep caller probes and all vetoes ahead of the non-reentrant lock and
|