sidekick-shared 0.29.1 → 0.30.0
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/CHANGELOG.md +26 -0
- package/README.md +171 -60
- package/dist/aggregation/EventAggregator.d.ts +79 -4
- package/dist/aggregation/EventAggregator.js +391 -37
- package/dist/aggregation/types.d.ts +27 -3
- package/dist/browser.d.ts +7 -3
- package/dist/browser.js +23 -3
- package/dist/context/sessionContext.js +5 -1
- package/dist/esm/aggregation/EventAggregator.js +392 -38
- package/dist/esm/browser.js +3 -1
- package/dist/esm/context/sessionContext.js +6 -2
- package/dist/esm/fileContinuity.js +149 -0
- package/dist/esm/index.js +5 -2
- package/dist/esm/node.js +2 -0
- package/dist/esm/parsers/codexParser.js +192 -3
- package/dist/esm/parsers/subagentEndState.js +61 -0
- package/dist/esm/parsers/subagentScanner.js +4 -0
- package/dist/esm/parsers/subagentTraceParser.js +6 -0
- package/dist/esm/promptHistory.js +22 -11
- package/dist/esm/providers/claudeCode.js +50 -27
- package/dist/esm/providers/codex.js +153 -163
- package/dist/esm/providers/codexCompressedReader.js +184 -0
- package/dist/esm/providers/codexDatabase.js +47 -2
- package/dist/esm/providers/codexRollouts.js +20 -2
- package/dist/esm/providers/codexSubagents.js +456 -0
- package/dist/esm/providers/compressedRollouts.js +330 -0
- package/dist/esm/providers/factory.js +4 -1
- package/dist/esm/providers/rolloutWalker.js +32 -4
- package/dist/esm/schemas/sessionEvent.js +32 -4
- package/dist/esm/sessionMonitor.js +7 -1
- package/dist/esm/sessionPreviews.js +9 -0
- package/dist/esm/sessionPromptHistory.js +5 -3
- package/dist/esm/subagentActivity.js +125 -0
- package/dist/esm/subagentDisplayState.js +81 -0
- package/dist/esm/subagentSpawn.js +206 -6
- package/dist/esm/testing/codexSubagentFixtures.js +336 -0
- package/dist/esm/toolCallOrigins.js +17 -10
- package/dist/esm/toolCategory.js +6 -2
- package/dist/esm/transcript.js +6 -2
- package/dist/esm/turns/assistantTurn.js +6 -3
- package/dist/esm/usage/usageEvents.js +4 -1
- package/dist/esm/watchers/eventBridge.js +31 -1
- package/dist/esm/watchers/jsonlTail.js +11 -42
- package/dist/esm/watchers/jsonlWatcher.js +19 -9
- package/dist/esm/watchers/providerReaderWatcher.js +5 -4
- package/dist/fileContinuity.d.ts +95 -0
- package/dist/fileContinuity.js +187 -0
- package/dist/index.d.ts +14 -7
- package/dist/index.js +39 -10
- package/dist/node.d.ts +4 -0
- package/dist/node.js +16 -2
- package/dist/parsers/codexParser.d.ts +44 -0
- package/dist/parsers/codexParser.js +192 -3
- package/dist/parsers/subagentEndState.d.ts +20 -0
- package/dist/parsers/subagentEndState.js +65 -0
- package/dist/parsers/subagentScanner.js +4 -0
- package/dist/parsers/subagentTraceParser.d.ts +8 -0
- package/dist/parsers/subagentTraceParser.js +6 -0
- package/dist/promptHistory.d.ts +13 -3
- package/dist/promptHistory.js +22 -11
- package/dist/providers/claudeCode.js +50 -27
- package/dist/providers/codex.d.ts +35 -5
- package/dist/providers/codex.js +150 -160
- package/dist/providers/codexCompressedReader.d.ts +58 -0
- package/dist/providers/codexCompressedReader.js +221 -0
- package/dist/providers/codexDatabase.d.ts +25 -0
- package/dist/providers/codexDatabase.js +48 -2
- package/dist/providers/codexRollouts.d.ts +16 -2
- package/dist/providers/codexRollouts.js +20 -2
- package/dist/providers/codexSubagents.d.ts +66 -0
- package/dist/providers/codexSubagents.js +495 -0
- package/dist/providers/compressedRollouts.d.ts +114 -0
- package/dist/providers/compressedRollouts.js +380 -0
- package/dist/providers/factory.d.ts +5 -0
- package/dist/providers/factory.js +4 -1
- package/dist/providers/rolloutWalker.d.ts +15 -2
- package/dist/providers/rolloutWalker.js +33 -4
- package/dist/providers/types.d.ts +22 -1
- package/dist/schemas/sessionEvent.d.ts +26 -2
- package/dist/schemas/sessionEvent.js +31 -3
- package/dist/sessionMonitor.d.ts +6 -0
- package/dist/sessionMonitor.js +7 -1
- package/dist/sessionPreviews.js +9 -0
- package/dist/sessionPromptHistory.d.ts +5 -0
- package/dist/sessionPromptHistory.js +5 -36
- package/dist/subagentActivity.d.ts +91 -0
- package/dist/subagentActivity.js +132 -0
- package/dist/subagentDisplayState.d.ts +81 -0
- package/dist/subagentDisplayState.js +86 -0
- package/dist/subagentSpawn.d.ts +100 -5
- package/dist/subagentSpawn.js +218 -7
- package/dist/testing/codexSubagentFixtures.d.ts +48 -0
- package/dist/testing/codexSubagentFixtures.js +344 -0
- package/dist/toolCallOrigins.d.ts +5 -0
- package/dist/toolCallOrigins.js +17 -10
- package/dist/toolCategory.js +6 -2
- package/dist/transcript.js +5 -1
- package/dist/turns/assistantTurn.d.ts +4 -1
- package/dist/turns/assistantTurn.js +5 -2
- package/dist/types/codex.d.ts +71 -2
- package/dist/types/sessionEvent.d.ts +38 -3
- package/dist/usage/usageEvents.d.ts +4 -1
- package/dist/usage/usageEvents.js +4 -1
- package/dist/watchers/eventBridge.js +30 -0
- package/dist/watchers/jsonlTail.js +11 -42
- package/dist/watchers/jsonlWatcher.d.ts +5 -1
- package/dist/watchers/jsonlWatcher.js +18 -8
- package/dist/watchers/providerReaderWatcher.js +5 -4
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuity of a file read incrementally by byte offset: whether the file
|
|
3
|
+
* now at a path is still the content earlier reads consumed, or was
|
|
4
|
+
* truncated, rewritten, or replaced since.
|
|
5
|
+
*
|
|
6
|
+
* {@link FileContinuity} holds the rules `createJsonlTail` applies, so every
|
|
7
|
+
* incremental reader detects the same cases the same way:
|
|
8
|
+
*
|
|
9
|
+
* - `replaced`: another file (by device and inode, read from the open
|
|
10
|
+
* descriptor, so a rename between a stat and an open cannot mix two files)
|
|
11
|
+
* now has the path.
|
|
12
|
+
* - `truncated`: the file is shorter than the read position.
|
|
13
|
+
* - `rewritten`: the mtime changed and either the first {@link HEAD_BYTES}
|
|
14
|
+
* bytes differ from those recorded, or the byte before the committed
|
|
15
|
+
* (resume-safe) offset is no longer a newline.
|
|
16
|
+
*
|
|
17
|
+
* Replacement and truncation are checked on every read; the content probe
|
|
18
|
+
* only when the mtime changed or the recorded head is still short.
|
|
19
|
+
*
|
|
20
|
+
* Known limits: a rewrite that leaves the mtime unchanged (coarse mtime
|
|
21
|
+
* resolution, or an mtime restored on purpose) or that changes only bytes
|
|
22
|
+
* past the first 4 KiB while the resume point still follows a newline goes
|
|
23
|
+
* unnoticed; on filesystems without stable inode numbers (some SMB mounts,
|
|
24
|
+
* Windows) an unchanged file can read as replaced, which costs a replay.
|
|
25
|
+
*
|
|
26
|
+
* @module fileContinuity
|
|
27
|
+
*/
|
|
28
|
+
import * as fs from 'fs';
|
|
29
|
+
import { fileHeadOf, HEAD_BYTES, headMatches } from './fileHead';
|
|
30
|
+
/**
|
|
31
|
+
* What one reader knows about the file its offsets refer to: identity,
|
|
32
|
+
* mtime, and a digest of the first bytes, recorded by {@link adopt} after
|
|
33
|
+
* each read.
|
|
34
|
+
*
|
|
35
|
+
* Synchronous readers call {@link check} on the open descriptor, restart
|
|
36
|
+
* from offset 0 when it returns a reason, then call {@link adopt}. A reader
|
|
37
|
+
* that repositions without reading (a seek to a stored offset, a reset)
|
|
38
|
+
* calls {@link forget}, so its next read adopts the file instead of
|
|
39
|
+
* comparing it with the old one. The lower-level methods ({@link needsProbe},
|
|
40
|
+
* {@link reasonFor}, {@link record}) serve asynchronous readers that probe
|
|
41
|
+
* on their own.
|
|
42
|
+
*/
|
|
43
|
+
export class FileContinuity {
|
|
44
|
+
identity = null;
|
|
45
|
+
head = null;
|
|
46
|
+
lastMtimeMs = -1;
|
|
47
|
+
/** What the last {@link check} probed and decided, for {@link adopt}. */
|
|
48
|
+
checked = null;
|
|
49
|
+
/** Device and inode of the adopted file; `null` before the first read and after {@link forget}. */
|
|
50
|
+
get knownIdentity() {
|
|
51
|
+
return this.identity;
|
|
52
|
+
}
|
|
53
|
+
/** Length and digest of the adopted file's first bytes, once probed. */
|
|
54
|
+
get knownHead() {
|
|
55
|
+
return this.head;
|
|
56
|
+
}
|
|
57
|
+
/** Whether {@link reasonFor} needs bytes from the file to decide. */
|
|
58
|
+
needsProbe(stat, readOffset) {
|
|
59
|
+
return (this.identity === null ||
|
|
60
|
+
stat.dev !== this.identity.dev ||
|
|
61
|
+
stat.ino !== this.identity.ino ||
|
|
62
|
+
stat.size < readOffset ||
|
|
63
|
+
stat.mtimeMs !== this.lastMtimeMs ||
|
|
64
|
+
this.head === null ||
|
|
65
|
+
this.head.bytes < Math.min(HEAD_BYTES, stat.size));
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Why the reader must start over: the path now names another file, the
|
|
69
|
+
* file shrank below `readOffset`, its first bytes changed, or the resume
|
|
70
|
+
* point no longer follows a newline. `null` when it can continue.
|
|
71
|
+
*/
|
|
72
|
+
reasonFor(stat, readOffset, probe) {
|
|
73
|
+
if (this.identity && (stat.dev !== this.identity.dev || stat.ino !== this.identity.ino)) {
|
|
74
|
+
return 'replaced';
|
|
75
|
+
}
|
|
76
|
+
if (stat.size < readOffset)
|
|
77
|
+
return 'truncated';
|
|
78
|
+
if (this.identity && stat.mtimeMs !== this.lastMtimeMs && probe) {
|
|
79
|
+
if (this.head && !headMatches(this.head, probe.prefix))
|
|
80
|
+
return 'rewritten';
|
|
81
|
+
if (probe.before && probe.before[0] !== 0x0a)
|
|
82
|
+
return 'rewritten';
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/** Records the identity, mtime, and (from `probe`) first bytes of the file the offsets now refer to. */
|
|
87
|
+
record(stat, probe) {
|
|
88
|
+
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
89
|
+
this.lastMtimeMs = stat.mtimeMs;
|
|
90
|
+
if (probe && (!this.head || this.head.bytes < Math.min(HEAD_BYTES, stat.size))) {
|
|
91
|
+
this.head = fileHeadOf(probe.prefix);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Drops the recorded first bytes (the reader restarted); the next {@link record} with a probe sets them again. */
|
|
95
|
+
discardHead() {
|
|
96
|
+
this.head = null;
|
|
97
|
+
}
|
|
98
|
+
/** Records the file as known from `prefix`, its first `min(HEAD_BYTES, size)` bytes. */
|
|
99
|
+
adoptPrefix(stat, prefix) {
|
|
100
|
+
this.identity = { dev: stat.dev, ino: stat.ino };
|
|
101
|
+
this.lastMtimeMs = stat.mtimeMs;
|
|
102
|
+
this.head = fileHeadOf(prefix);
|
|
103
|
+
}
|
|
104
|
+
/** Forgets the file, so the next {@link check} adopts whatever file has the path. */
|
|
105
|
+
forget() {
|
|
106
|
+
this.identity = null;
|
|
107
|
+
this.head = null;
|
|
108
|
+
this.lastMtimeMs = -1;
|
|
109
|
+
this.checked = null;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Applies the rules to the file open on `fd` (`stat` from `fstat` on that
|
|
113
|
+
* descriptor), reading the bytes they need. `readOffset` is how far the
|
|
114
|
+
* reader has read; `committedOffset` (default `readOffset`) is its
|
|
115
|
+
* resume-safe position just after a newline. Returns why the reader must
|
|
116
|
+
* restart from offset 0, or `null`. Call {@link adopt} with the same
|
|
117
|
+
* `stat` once the reader has acted on the answer.
|
|
118
|
+
*/
|
|
119
|
+
check(fd, stat, readOffset, committedOffset = readOffset) {
|
|
120
|
+
const probe = this.needsProbe(stat, readOffset)
|
|
121
|
+
? probeFileContinuity(fd, stat, committedOffset)
|
|
122
|
+
: null;
|
|
123
|
+
const reason = this.reasonFor(stat, readOffset, probe);
|
|
124
|
+
this.checked = { probe, reset: reason !== null };
|
|
125
|
+
return reason;
|
|
126
|
+
}
|
|
127
|
+
/** Records the file just checked as the one the reader's offsets now refer to. */
|
|
128
|
+
adopt(stat) {
|
|
129
|
+
const checked = this.checked;
|
|
130
|
+
this.checked = null;
|
|
131
|
+
if (checked?.reset)
|
|
132
|
+
this.head = null;
|
|
133
|
+
this.record(stat, checked?.probe ?? null);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Reads the bytes {@link FileContinuity.reasonFor} compares, from the file open on `fd`. */
|
|
137
|
+
export function probeFileContinuity(fd, stat, committedOffset) {
|
|
138
|
+
return {
|
|
139
|
+
prefix: readAt(fd, 0, Math.min(HEAD_BYTES, stat.size)),
|
|
140
|
+
before: committedOffset > 0 && committedOffset <= stat.size
|
|
141
|
+
? readAt(fd, committedOffset - 1, 1)
|
|
142
|
+
: null,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
function readAt(fd, position, length) {
|
|
146
|
+
const buffer = Buffer.alloc(length);
|
|
147
|
+
const count = length > 0 ? fs.readSync(fd, buffer, 0, length, position) : 0;
|
|
148
|
+
return buffer.subarray(0, count);
|
|
149
|
+
}
|
package/dist/esm/index.js
CHANGED
|
@@ -51,6 +51,7 @@ export { ClaudeCodeProvider } from './providers/claudeCode';
|
|
|
51
51
|
export { OpenCodeProvider, getOpenCodeDataDir } from './providers/openCode';
|
|
52
52
|
export { CodexProvider, findCodexRolloutFile } from './providers/codex';
|
|
53
53
|
export { walkRolloutFiles, walkRolloutFilesAsync } from './providers/rolloutWalker';
|
|
54
|
+
export { COMPRESSED_ROLLOUT_SUFFIX, isCompressedRolloutPath, isZstdDecompressionAvailable, readRolloutBuffer, readRolloutText, resolveRolloutPath, } from './providers/compressedRollouts';
|
|
54
55
|
// Parsers — JSONL
|
|
55
56
|
export { JsonlParser, TRUNCATION_PATTERNS } from './parsers/jsonl';
|
|
56
57
|
// Parsers — OpenCode
|
|
@@ -58,6 +59,7 @@ export { normalizeToolName, normalizeToolInput, detectPlanModeFromText, convertO
|
|
|
58
59
|
// Parsers — Codex
|
|
59
60
|
export { CodexRolloutParser, DEFAULT_MAX_TRACKED_TOOL_CALLS, extractPatchFilePaths, normalizeCodexToolName, normalizeCodexToolInput, resolveCodexCallUsage, } from './parsers/codexParser';
|
|
60
61
|
export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
|
|
62
|
+
export { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, describeSubagentActivity, isSubagentActivitySystemEvent, parseCodexAgentMessageText, subagentActivityFromEvent, } from './subagentActivity';
|
|
61
63
|
// Parsers — Subagent scanning
|
|
62
64
|
export { scanSubagentDir, extractTaskInfo, readAgentMeta } from './parsers/subagentScanner';
|
|
63
65
|
// Usage — split-line dedupe, subagent tokens, session totals
|
|
@@ -80,7 +82,7 @@ export { listSessionPreviews, listSessionPreviewsAsync, readSessionPreview, read
|
|
|
80
82
|
export { parseDebugLog, filterByLevel, collapseDuplicates, discoverDebugLogs, } from './parsers/debugLogParser';
|
|
81
83
|
// Database wrappers
|
|
82
84
|
export { OpenCodeDatabase } from './providers/openCodeDatabase';
|
|
83
|
-
export { CodexDatabase } from './providers/codexDatabase';
|
|
85
|
+
export { CodexDatabase, isCodexSubagentThread } from './providers/codexDatabase';
|
|
84
86
|
// Search
|
|
85
87
|
export { searchSessions } from './search/sessionSearch';
|
|
86
88
|
export { FilterEngine } from './search/advancedFilter';
|
|
@@ -88,7 +90,8 @@ export { FilterEngine } from './search/advancedFilter';
|
|
|
88
90
|
export { composeContext } from './context/composer';
|
|
89
91
|
export { buildSessionContextSnapshot, calculateSessionContextPressure, createSessionContextProjector, readSessionContextSnapshot, } from './context/sessionContext';
|
|
90
92
|
// Subagent spawns (`Agent` / `Task`) and background-task notifications
|
|
91
|
-
export { SUBAGENT_SPAWN_TOOL_NAMES, isSubagentSpawnTool, isTaskNotificationSystemEvent, parseTaskNotification, taskNotificationFromEvent, } from './subagentSpawn';
|
|
93
|
+
export { SUBAGENT_SPAWN_TOOL_NAMES, agentsStoppedNoticeFromEvent, isAgentsKilledSystemEvent, isCodexSubagentSpawnTool, isSubagentSpawnTool, isTaskNotificationSystemEvent, isTaskStatusSystemEvent, parseAgentsStoppedNotice, parseCodexSpawnLaunch, parseTaskNotification, taskNotificationFromEvent, taskStatusFromAttachment, taskStatusFromEvent, } from './subagentSpawn';
|
|
94
|
+
export { SUBAGENT_PARENT_IDLE_ENDED_MS, SUBAGENT_UNREPORTED_QUIET_MS, resolveSubagentDisplayState, subagentDisplayLabel, } from './subagentDisplayState';
|
|
92
95
|
// Assistant turn projection
|
|
93
96
|
export { assistantTurnEventsFromSessionEvents, extractTurnSubagents, isAssistantTurnSubagentTool, reasoningSummary, segmentAssistantTurn, } from './turns/assistantTurn';
|
|
94
97
|
// Plan Extraction
|
package/dist/esm/node.js
CHANGED
|
@@ -24,6 +24,7 @@ export { ACTIVE_SESSION_MTIME_THRESHOLD_MS, importSessionHistory, } from './usag
|
|
|
24
24
|
export { bucketUsage, summarizeUsageRows, usageBucketKey, weekKey } from './usage/usageReports';
|
|
25
25
|
export { findCodexRolloutFile } from './providers/codex';
|
|
26
26
|
export { walkRolloutFiles, walkRolloutFilesAsync } from './providers/rolloutWalker';
|
|
27
|
+
export { COMPRESSED_ROLLOUT_SUFFIX, isCompressedRolloutPath, isZstdDecompressionAvailable, readRolloutBuffer, readRolloutText, resolveRolloutPath, } from './providers/compressedRollouts';
|
|
27
28
|
export { readCodexHistory } from './parsers/codexHistory';
|
|
28
29
|
export { listSessionPreviews, listSessionPreviewsAsync, readSessionPreview, readSessionPreviewAsync, } from './sessionPreviews';
|
|
29
30
|
export { ObservedSessionCollector, observedSessionSourceFromProvider, fileFingerprint, fileFingerprintParts, } from './observedSessionCollector';
|
|
@@ -41,6 +42,7 @@ export { parseAuthJson as parseCodexAuthJson, readAuthIdentityFromRaw as readCod
|
|
|
41
42
|
export { atomicWriteJson, atomicWriteJsonSync } from './writers/atomic';
|
|
42
43
|
export { parseClaudeAuthStatus, parseCodexLoginStatus } from './authStatus';
|
|
43
44
|
export { codexRolloutOrigin, codexSpawnParentFromSource } from './parsers/codexRolloutOrigin';
|
|
45
|
+
export { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, describeSubagentActivity, isSubagentActivitySystemEvent, parseCodexAgentMessageText, subagentActivityFromEvent, } from './subagentActivity';
|
|
44
46
|
export { getPricingCatalogInfo, registerModelPricing, clearRegisteredModelPricing, } from './modelInfo';
|
|
45
47
|
export { fetchProviderServiceStatus } from './providerServiceStatus';
|
|
46
48
|
// Tool categories across providers
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
import { isAggregateCodexLimit } from '../types/codex';
|
|
15
15
|
import { normalizeToolName } from './openCodeParser';
|
|
16
16
|
import { normalizeProviderUsage } from '../usageNormalization';
|
|
17
|
+
import { SUBAGENT_ACTIVITY_BLOCK_TYPE, SUBAGENT_ACTIVITY_ORIGIN_KIND, parseCodexAgentMessageText, } from '../subagentActivity';
|
|
17
18
|
function reportedCompaction(tokensBefore, tokensAfter) {
|
|
18
19
|
if (typeof tokensBefore !== 'number' ||
|
|
19
20
|
typeof tokensAfter !== 'number' ||
|
|
@@ -173,6 +174,29 @@ export function extractPatchFilePaths(input) {
|
|
|
173
174
|
export const DEFAULT_MAX_TRACKED_TOOL_CALLS = 4096;
|
|
174
175
|
/** Entries per table {@link CodexRolloutParser.exportResumeState} keeps by default. */
|
|
175
176
|
const DEFAULT_RESUME_STATE_LIMIT = 256;
|
|
177
|
+
/** Raw names of Codex's multi-agent tools (namespace `collaboration`). */
|
|
178
|
+
const COLLABORATION_TOOL_NAMES = new Set([
|
|
179
|
+
'spawn_agent',
|
|
180
|
+
'followup_task',
|
|
181
|
+
'send_message',
|
|
182
|
+
'interrupt_agent',
|
|
183
|
+
'wait_agent',
|
|
184
|
+
'list_agents',
|
|
185
|
+
]);
|
|
186
|
+
const SUBAGENT_ACTIVITY_KINDS = new Set([
|
|
187
|
+
'started',
|
|
188
|
+
'completed',
|
|
189
|
+
'interacted',
|
|
190
|
+
'interrupted',
|
|
191
|
+
]);
|
|
192
|
+
function nonEmptyString(value) {
|
|
193
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
194
|
+
}
|
|
195
|
+
/** `subagent_history_start_ordinal` of a session_meta payload, when valid. */
|
|
196
|
+
function historyStartOrdinalOf(meta) {
|
|
197
|
+
const value = meta?.subagent_history_start_ordinal;
|
|
198
|
+
return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null;
|
|
199
|
+
}
|
|
176
200
|
function isPlainObject(value) {
|
|
177
201
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
178
202
|
}
|
|
@@ -290,6 +314,15 @@ export class CodexRolloutParser {
|
|
|
290
314
|
/** Byte budget for pending calls; `null` = unlimited (no accounting). */
|
|
291
315
|
maxPendingBytes;
|
|
292
316
|
metadataOnly;
|
|
317
|
+
includeSubagentActivity;
|
|
318
|
+
/**
|
|
319
|
+
* Set once a forked child's rollout embeds an ancestor's session_meta: its
|
|
320
|
+
* own `subagent_history_start_ordinal`, below which records are the
|
|
321
|
+
* parent's history and are skipped. `null` while nothing is skipped.
|
|
322
|
+
*/
|
|
323
|
+
historyStartOrdinal = null;
|
|
324
|
+
/** Multi-agent tool calls (call id → raw name), only with `includeSubagentActivity`. */
|
|
325
|
+
collaborationCalls = new Map();
|
|
293
326
|
/** Pending calls in insertion order, for the byte budget. */
|
|
294
327
|
pendingLedger = new Map();
|
|
295
328
|
pendingBytes = 0;
|
|
@@ -305,6 +338,7 @@ export class CodexRolloutParser {
|
|
|
305
338
|
? Math.floor(maxBytes)
|
|
306
339
|
: null;
|
|
307
340
|
this.metadataOnly = options.metadataOnly === true;
|
|
341
|
+
this.includeSubagentActivity = options.includeSubagentActivity === true;
|
|
308
342
|
}
|
|
309
343
|
/** Counters for the pending tool-call state and what the caps evicted. */
|
|
310
344
|
getPendingStats() {
|
|
@@ -337,6 +371,16 @@ export class CodexRolloutParser {
|
|
|
337
371
|
getLastRateLimits() {
|
|
338
372
|
return this.lastRateLimits;
|
|
339
373
|
}
|
|
374
|
+
/**
|
|
375
|
+
* In a forked subagent's rollout, the ordinal of its first own record:
|
|
376
|
+
* records with a lower `ordinal` are the parent history the fork embeds
|
|
377
|
+
* and produce no events. Known once the rollout's second line (an embedded
|
|
378
|
+
* ancestor `session_meta`) was parsed; `null` otherwise, and for every
|
|
379
|
+
* rollout that is not such a fork.
|
|
380
|
+
*/
|
|
381
|
+
getInheritedHistoryEnd() {
|
|
382
|
+
return this.historyStartOrdinal;
|
|
383
|
+
}
|
|
340
384
|
/**
|
|
341
385
|
* The state later lines depend on — session metadata, current model,
|
|
342
386
|
* previous cumulative token total, context window, rate limits, plan mode,
|
|
@@ -358,6 +402,14 @@ export class CodexRolloutParser {
|
|
|
358
402
|
patchExpandedToolUseIds: newest(this.patchExpandedToolUseIds, this.patchExpandedToolUseIds.size, limit),
|
|
359
403
|
pendingExecCommands: newest(this.pendingExecCommands.values(), this.pendingExecCommands.size, limit),
|
|
360
404
|
pendingMcpToolCalls: newest(this.pendingMcpToolCalls.values(), this.pendingMcpToolCalls.size, limit),
|
|
405
|
+
...(this.historyStartOrdinal !== null
|
|
406
|
+
? { inheritedHistoryEnd: this.historyStartOrdinal }
|
|
407
|
+
: {}),
|
|
408
|
+
...(this.collaborationCalls.size > 0
|
|
409
|
+
? {
|
|
410
|
+
collaborationCalls: newest(this.collaborationCalls, this.collaborationCalls.size, limit),
|
|
411
|
+
}
|
|
412
|
+
: {}),
|
|
361
413
|
});
|
|
362
414
|
}
|
|
363
415
|
/**
|
|
@@ -418,6 +470,21 @@ export class CodexRolloutParser {
|
|
|
418
470
|
typeof entry.timestamp === 'string')) {
|
|
419
471
|
return false;
|
|
420
472
|
}
|
|
473
|
+
const inheritedEnd = s.inheritedHistoryEnd;
|
|
474
|
+
if (inheritedEnd !== undefined &&
|
|
475
|
+
inheritedEnd !== null &&
|
|
476
|
+
!(typeof inheritedEnd === 'number' && Number.isInteger(inheritedEnd) && inheritedEnd > 0)) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
const collaborationCalls = s.collaborationCalls;
|
|
480
|
+
if (collaborationCalls !== undefined &&
|
|
481
|
+
!(Array.isArray(collaborationCalls) &&
|
|
482
|
+
collaborationCalls.every((entry) => Array.isArray(entry) &&
|
|
483
|
+
entry.length === 2 &&
|
|
484
|
+
typeof entry[0] === 'string' &&
|
|
485
|
+
typeof entry[1] === 'string'))) {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
421
488
|
const copy = cloneJson({
|
|
422
489
|
sessionMeta: s.sessionMeta,
|
|
423
490
|
lastRateLimits: s.lastRateLimits,
|
|
@@ -427,6 +494,7 @@ export class CodexRolloutParser {
|
|
|
427
494
|
});
|
|
428
495
|
this.reset();
|
|
429
496
|
this.sessionMeta = copy.sessionMeta;
|
|
497
|
+
this.historyStartOrdinal = typeof inheritedEnd === 'number' ? inheritedEnd : null;
|
|
430
498
|
this.currentModel = s.currentModel;
|
|
431
499
|
this.prevTotalUsage = prevTotalUsage;
|
|
432
500
|
this.lastTokenUsage = lastTokenUsage;
|
|
@@ -447,15 +515,28 @@ export class CodexRolloutParser {
|
|
|
447
515
|
for (const pending of copy.mcps) {
|
|
448
516
|
this.putPendingMcp(this.metadataOnly ? { ...pending, arguments: undefined } : pending);
|
|
449
517
|
}
|
|
518
|
+
if (this.includeSubagentActivity && Array.isArray(collaborationCalls)) {
|
|
519
|
+
for (const [callId, name] of collaborationCalls) {
|
|
520
|
+
this.rememberCollaborationCall(callId, name);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
450
523
|
return true;
|
|
451
524
|
}
|
|
452
525
|
/**
|
|
453
526
|
* Converts a single rollout line to zero or more session events.
|
|
454
527
|
*/
|
|
455
528
|
convertLine(line) {
|
|
529
|
+
if (line.type === 'session_meta') {
|
|
530
|
+
return this.handleSessionMeta(line.timestamp, line.payload, line.ordinal);
|
|
531
|
+
}
|
|
532
|
+
// A forked subagent rollout embeds the parent's history before its own
|
|
533
|
+
// records; that history is the parent's, not the child's.
|
|
534
|
+
if (this.historyStartOrdinal !== null &&
|
|
535
|
+
typeof line.ordinal === 'number' &&
|
|
536
|
+
line.ordinal < this.historyStartOrdinal) {
|
|
537
|
+
return [];
|
|
538
|
+
}
|
|
456
539
|
switch (line.type) {
|
|
457
|
-
case 'session_meta':
|
|
458
|
-
return this.handleSessionMeta(line.timestamp, line.payload);
|
|
459
540
|
case 'response_item':
|
|
460
541
|
return this.handleResponseItem(line.timestamp, line.payload);
|
|
461
542
|
case 'compacted':
|
|
@@ -471,6 +552,8 @@ export class CodexRolloutParser {
|
|
|
471
552
|
/** Reset all parser state. */
|
|
472
553
|
reset() {
|
|
473
554
|
this.sessionMeta = null;
|
|
555
|
+
this.historyStartOrdinal = null;
|
|
556
|
+
this.collaborationCalls.clear();
|
|
474
557
|
this.currentModel = null;
|
|
475
558
|
this.pendingExecCommands.clear();
|
|
476
559
|
this.pendingMcpToolCalls.clear();
|
|
@@ -560,7 +643,25 @@ export class CodexRolloutParser {
|
|
|
560
643
|
return this.metadataOnly ? '' : content;
|
|
561
644
|
}
|
|
562
645
|
// --- Handlers ---
|
|
563
|
-
handleSessionMeta(timestamp, payload) {
|
|
646
|
+
handleSessionMeta(timestamp, payload, ordinal) {
|
|
647
|
+
if (this.sessionMeta) {
|
|
648
|
+
// Only the rollout's own (first) session_meta counts. A forked child
|
|
649
|
+
// embeds its parent's right after its own, followed by the parent's
|
|
650
|
+
// history up to the child's `subagent_history_start_ordinal`; from
|
|
651
|
+
// here on, records below that ordinal are skipped. (Children whose
|
|
652
|
+
// rollout has no embedded meta keep every record: in some, Codex
|
|
653
|
+
// recorded a start ordinal past their own records.)
|
|
654
|
+
const start = historyStartOrdinalOf(this.sessionMeta);
|
|
655
|
+
if (this.historyStartOrdinal === null &&
|
|
656
|
+
start !== null &&
|
|
657
|
+
typeof ordinal === 'number' &&
|
|
658
|
+
ordinal < start) {
|
|
659
|
+
this.historyStartOrdinal = start;
|
|
660
|
+
}
|
|
661
|
+
return [];
|
|
662
|
+
}
|
|
663
|
+
if (!payload || typeof payload !== 'object')
|
|
664
|
+
return [];
|
|
564
665
|
this.sessionMeta = payload;
|
|
565
666
|
const text = payload.base_instructions?.text?.trim();
|
|
566
667
|
return [
|
|
@@ -600,10 +701,91 @@ export class CodexRolloutParser {
|
|
|
600
701
|
return this.handleCustomToolCall(timestamp, payload);
|
|
601
702
|
case 'custom_tool_call_output':
|
|
602
703
|
return this.handleCustomToolCallOutput(timestamp, payload);
|
|
704
|
+
case 'agent_message':
|
|
705
|
+
return this.handleAgentMessage(timestamp, payload);
|
|
603
706
|
default:
|
|
604
707
|
return [];
|
|
605
708
|
}
|
|
606
709
|
}
|
|
710
|
+
// --- Subagent activity ---
|
|
711
|
+
rememberCollaborationCall(callId, name) {
|
|
712
|
+
this.collaborationCalls.delete(callId);
|
|
713
|
+
this.collaborationCalls.set(callId, name);
|
|
714
|
+
while (this.collaborationCalls.size > this.maxTrackedToolCalls) {
|
|
715
|
+
this.collaborationCalls.delete(this.collaborationCalls.keys().next().value);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
subagentActivityEvent(timestamp, id, activity) {
|
|
719
|
+
return {
|
|
720
|
+
type: 'system',
|
|
721
|
+
timestamp,
|
|
722
|
+
origin: { kind: SUBAGENT_ACTIVITY_ORIGIN_KIND },
|
|
723
|
+
message: {
|
|
724
|
+
role: 'system',
|
|
725
|
+
id: `subagent-activity:${id}`,
|
|
726
|
+
sourceLabel: 'subagent activity',
|
|
727
|
+
content: [{ type: SUBAGENT_ACTIVITY_BLOCK_TYPE, ...activity }],
|
|
728
|
+
},
|
|
729
|
+
providerMetadata: { providerId: 'codex', source: 'codex-rollout' },
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
/** A child's `FINAL_ANSWER` message to its parent (opt-in). */
|
|
733
|
+
handleAgentMessage(timestamp, item) {
|
|
734
|
+
if (!this.includeSubagentActivity || !Array.isArray(item.content))
|
|
735
|
+
return [];
|
|
736
|
+
const first = item.content.find((part) => !!part && typeof part === 'object' && typeof part.text === 'string');
|
|
737
|
+
const parsed = parseCodexAgentMessageText(first?.text);
|
|
738
|
+
if (!parsed || parsed.messageType !== 'FINAL_ANSWER')
|
|
739
|
+
return [];
|
|
740
|
+
const agentPath = nonEmptyString(item.author) ?? parsed.sender;
|
|
741
|
+
const recipient = nonEmptyString(item.recipient) ?? parsed.taskName;
|
|
742
|
+
const text = this.metadataOnly ? undefined : nonEmptyString(parsed.payload.trim());
|
|
743
|
+
const activity = {
|
|
744
|
+
kind: 'final-answer',
|
|
745
|
+
...(agentPath ? { agentPath } : {}),
|
|
746
|
+
...(recipient ? { recipient } : {}),
|
|
747
|
+
...(text ? { text } : {}),
|
|
748
|
+
};
|
|
749
|
+
return [
|
|
750
|
+
this.subagentActivityEvent(timestamp, nonEmptyString(item.id) ?? `final-answer-${timestamp}-${agentPath ?? ''}`, activity),
|
|
751
|
+
];
|
|
752
|
+
}
|
|
753
|
+
/** A `SubAgentActivity` turn item (opt-in). */
|
|
754
|
+
handleItemCompleted(timestamp, event) {
|
|
755
|
+
if (!this.includeSubagentActivity)
|
|
756
|
+
return [];
|
|
757
|
+
const item = event.item;
|
|
758
|
+
if (!item || item.type !== 'SubAgentActivity')
|
|
759
|
+
return [];
|
|
760
|
+
const kind = typeof item.kind === 'string' ? item.kind : '';
|
|
761
|
+
if (!SUBAGENT_ACTIVITY_KINDS.has(kind))
|
|
762
|
+
return [];
|
|
763
|
+
const itemId = nonEmptyString(item.id);
|
|
764
|
+
const completedTurn = kind === 'completed' && itemId?.startsWith('subagent-completed-')
|
|
765
|
+
? nonEmptyString(itemId.slice('subagent-completed-'.length))
|
|
766
|
+
: undefined;
|
|
767
|
+
const callId = kind === 'completed' ? undefined : itemId;
|
|
768
|
+
const tool = callId ? this.collaborationCalls.get(callId) : undefined;
|
|
769
|
+
const fields = {
|
|
770
|
+
callId,
|
|
771
|
+
tool,
|
|
772
|
+
turnId: completedTurn,
|
|
773
|
+
agentThreadId: nonEmptyString(item.agent_thread_id),
|
|
774
|
+
agentPath: nonEmptyString(item.agent_path),
|
|
775
|
+
model: nonEmptyString(item.model),
|
|
776
|
+
reasoningEffort: nonEmptyString(item.reasoning_effort),
|
|
777
|
+
parentThreadId: nonEmptyString(event.thread_id),
|
|
778
|
+
parentTurnId: nonEmptyString(event.turn_id),
|
|
779
|
+
};
|
|
780
|
+
const activity = { kind: kind };
|
|
781
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
782
|
+
if (value !== undefined)
|
|
783
|
+
activity[key] = value;
|
|
784
|
+
}
|
|
785
|
+
return [
|
|
786
|
+
this.subagentActivityEvent(timestamp, itemId ?? `${kind}-${timestamp}-${activity.agentThreadId ?? ''}`, activity),
|
|
787
|
+
];
|
|
788
|
+
}
|
|
607
789
|
handleMessage(timestamp, item) {
|
|
608
790
|
const text = extractTextFromContent(item.content);
|
|
609
791
|
if (!text)
|
|
@@ -685,6 +867,11 @@ export class CodexRolloutParser {
|
|
|
685
867
|
const canonicalName = normalizeCodexToolName(item.name);
|
|
686
868
|
const normalizedInput = normalizeCodexToolInput(canonicalName, item.name, parsedArgs);
|
|
687
869
|
rememberBounded(this.emittedToolUseIds, item.call_id, this.maxTrackedToolCalls);
|
|
870
|
+
if (this.includeSubagentActivity &&
|
|
871
|
+
typeof item.call_id === 'string' &&
|
|
872
|
+
(item.namespace === 'collaboration' || COLLABORATION_TOOL_NAMES.has(item.name))) {
|
|
873
|
+
this.rememberCollaborationCall(item.call_id, item.name);
|
|
874
|
+
}
|
|
688
875
|
return [
|
|
689
876
|
{
|
|
690
877
|
type: 'assistant',
|
|
@@ -1141,6 +1328,8 @@ export class CodexRolloutParser {
|
|
|
1141
1328
|
timestamp,
|
|
1142
1329
|
},
|
|
1143
1330
|
];
|
|
1331
|
+
case 'item_completed':
|
|
1332
|
+
return this.handleItemCompleted(timestamp, event);
|
|
1144
1333
|
case 'turn_aborted':
|
|
1145
1334
|
case 'agent_reasoning':
|
|
1146
1335
|
case 'agent_message':
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a Claude Code subagent transcript ended cleanly, read line by line.
|
|
3
|
+
*
|
|
4
|
+
* Clean means: the last user or assistant line is an assistant line that
|
|
5
|
+
* stopped with `end_turn` or `stop_sequence`, and every `tool_use` in the
|
|
6
|
+
* transcript has its `tool_result`. A user line after that (a resumed agent's
|
|
7
|
+
* new prompt) makes it unfinished again.
|
|
8
|
+
*
|
|
9
|
+
* Pure. Internal to the subagent parsers.
|
|
10
|
+
*
|
|
11
|
+
* @module parsers/subagentEndState
|
|
12
|
+
*/
|
|
13
|
+
const ENDED_STOP_REASONS = new Set(['end_turn', 'stop_sequence']);
|
|
14
|
+
/** Bounds the unanswered-call set for pathological transcripts. */
|
|
15
|
+
const MAX_PENDING_TOOL_USES = 4096;
|
|
16
|
+
export class SubagentEndTracker {
|
|
17
|
+
unanswered = new Set();
|
|
18
|
+
lastWasCleanAssistant = false;
|
|
19
|
+
/** Feeds one parsed JSONL line, in file order. */
|
|
20
|
+
observe(raw) {
|
|
21
|
+
if (!raw || typeof raw !== 'object')
|
|
22
|
+
return;
|
|
23
|
+
const line = raw;
|
|
24
|
+
if (line.type !== 'assistant' && line.type !== 'user')
|
|
25
|
+
return;
|
|
26
|
+
const message = line.message && typeof line.message === 'object'
|
|
27
|
+
? line.message
|
|
28
|
+
: undefined;
|
|
29
|
+
const content = Array.isArray(message?.content) ? message.content : [];
|
|
30
|
+
if (line.type === 'user') {
|
|
31
|
+
for (const block of content) {
|
|
32
|
+
if (!block || typeof block !== 'object')
|
|
33
|
+
continue;
|
|
34
|
+
const typed = block;
|
|
35
|
+
if (typed.type === 'tool_result' && typeof typed.tool_use_id === 'string') {
|
|
36
|
+
this.unanswered.delete(typed.tool_use_id);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
this.lastWasCleanAssistant = false;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
let callsTool = false;
|
|
43
|
+
for (const block of content) {
|
|
44
|
+
if (!block || typeof block !== 'object')
|
|
45
|
+
continue;
|
|
46
|
+
const typed = block;
|
|
47
|
+
if (typed.type !== 'tool_use')
|
|
48
|
+
continue;
|
|
49
|
+
callsTool = true;
|
|
50
|
+
if (typeof typed.id === 'string' && this.unanswered.size < MAX_PENDING_TOOL_USES) {
|
|
51
|
+
this.unanswered.add(typed.id);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const stop = typeof message?.stop_reason === 'string' ? message.stop_reason : '';
|
|
55
|
+
this.lastWasCleanAssistant = !callsTool && ENDED_STOP_REASONS.has(stop);
|
|
56
|
+
}
|
|
57
|
+
/** Whether the lines so far end cleanly. */
|
|
58
|
+
endedCleanly() {
|
|
59
|
+
return this.lastWasCleanAssistant && this.unanswered.size === 0;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -13,6 +13,7 @@ import { ClaudeUsageDeduper, dedupedRawClaudeUsage } from '../usage/claudeUsageD
|
|
|
13
13
|
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
14
14
|
import { cachedSubagentStats } from './subagentStatsCache';
|
|
15
15
|
import { isSubagentSpawnTool } from '../subagentSpawn';
|
|
16
|
+
import { SubagentEndTracker } from './subagentEndState';
|
|
16
17
|
/**
|
|
17
18
|
* Pattern for matching subagent JSONL files.
|
|
18
19
|
* Files are named like: agent-<hash>.jsonl
|
|
@@ -160,6 +161,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
160
161
|
let description;
|
|
161
162
|
const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
|
|
162
163
|
const usageDeduper = new ClaudeUsageDeduper();
|
|
164
|
+
const endTracker = new SubagentEndTracker();
|
|
163
165
|
let startTime;
|
|
164
166
|
let endTime;
|
|
165
167
|
// Task tracking state
|
|
@@ -176,6 +178,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
176
178
|
if (!startTime)
|
|
177
179
|
startTime = eventTimestamp;
|
|
178
180
|
endTime = eventTimestamp;
|
|
181
|
+
endTracker.observe(event);
|
|
179
182
|
// Extract token usage from assistant messages. Split lines of one
|
|
180
183
|
// response repeat its usage, so count each response once.
|
|
181
184
|
const usage = dedupedRawClaudeUsage(event, usageDeduper);
|
|
@@ -298,6 +301,7 @@ function parseAgentFile(filePath, agentId, log) {
|
|
|
298
301
|
startTime,
|
|
299
302
|
endTime,
|
|
300
303
|
durationMs,
|
|
304
|
+
endedCleanly: endTracker.endedCleanly(),
|
|
301
305
|
};
|
|
302
306
|
}
|
|
303
307
|
return null;
|
|
@@ -22,6 +22,7 @@ import { isHardNoise, classifyMessage, getSoftNoiseReason } from '../formatters/
|
|
|
22
22
|
import { ClaudeUsageDeduper, dedupedRawClaudeUsage } from '../usage/claudeUsageDedupe';
|
|
23
23
|
import { addUsageToSubagent } from '../usage/subagentUsage';
|
|
24
24
|
import { readAgentMeta } from './subagentScanner';
|
|
25
|
+
import { SubagentEndTracker } from './subagentEndState';
|
|
25
26
|
import { isSubagentSpawnTool } from '../subagentSpawn';
|
|
26
27
|
import { ownValue } from '../ownKey';
|
|
27
28
|
// ── Constants ──
|
|
@@ -73,6 +74,7 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
73
74
|
let description;
|
|
74
75
|
const tokens = { agentId, toolCalls: [], inputTokens: 0, outputTokens: 0 };
|
|
75
76
|
const usageDeduper = new ClaudeUsageDeduper();
|
|
77
|
+
const endTracker = new SubagentEndTracker();
|
|
76
78
|
let startTime;
|
|
77
79
|
let endTime;
|
|
78
80
|
for (const line of lines) {
|
|
@@ -82,6 +84,7 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
82
84
|
if (!startTime)
|
|
83
85
|
startTime = timestamp;
|
|
84
86
|
endTime = timestamp;
|
|
87
|
+
endTracker.observe(raw);
|
|
85
88
|
// Build SessionEvent from raw JSONL
|
|
86
89
|
const sessionEvent = rawToSessionEvent(raw);
|
|
87
90
|
if (!sessionEvent)
|
|
@@ -182,6 +185,8 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
182
185
|
...(meta.requestShape ? { background: meta.requestShape === 'background' } : {}),
|
|
183
186
|
...(meta.finished !== undefined ? { finished: meta.finished } : {}),
|
|
184
187
|
...(meta.stoppedByUser !== undefined ? { stoppedByUser: meta.stoppedByUser } : {}),
|
|
188
|
+
...(endTime ? { lastActivityAt: endTime } : {}),
|
|
189
|
+
endedCleanly: endTracker.endedCleanly(),
|
|
185
190
|
events,
|
|
186
191
|
children: [],
|
|
187
192
|
stats: {
|
|
@@ -198,6 +203,7 @@ function parseAgentTrace(filePath, agentId) {
|
|
|
198
203
|
startTime,
|
|
199
204
|
endTime,
|
|
200
205
|
durationMs,
|
|
206
|
+
endedCleanly: endTracker.endedCleanly(),
|
|
201
207
|
},
|
|
202
208
|
};
|
|
203
209
|
}
|