@way_marks/server 4.0.2 → 4.3.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.
@@ -0,0 +1,373 @@
1
+ "use strict";
2
+ /**
3
+ * GitHub Copilot CLI session collector.
4
+ *
5
+ * Reads rich session data from ~/.copilot/session-state/<uuid>/:
6
+ * - workspace.yaml — sessionId, cwd, branch, summary (= current task)
7
+ * - inuse.<pid>.lock — active PID
8
+ * - events.jsonl — model, output tokens, tool calls, turn count
9
+ *
10
+ * Event types parsed:
11
+ * session.start → startedAt, initial context
12
+ * session.model_change → current model name
13
+ * session.compaction_complete → preCompactionTokens (context window usage)
14
+ * assistant.message → outputTokens per turn
15
+ * assistant.turn_end → turn count
16
+ * tool.execution_start → tool calls (name, arg)
17
+ * user.message → last user prompt (current task)
18
+ */
19
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ var desc = Object.getOwnPropertyDescriptor(m, k);
22
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
23
+ desc = { enumerable: true, get: function() { return m[k]; } };
24
+ }
25
+ Object.defineProperty(o, k2, desc);
26
+ }) : (function(o, m, k, k2) {
27
+ if (k2 === undefined) k2 = k;
28
+ o[k2] = m[k];
29
+ }));
30
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
31
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
32
+ }) : function(o, v) {
33
+ o["default"] = v;
34
+ });
35
+ var __importStar = (this && this.__importStar) || (function () {
36
+ var ownKeys = function(o) {
37
+ ownKeys = Object.getOwnPropertyNames || function (o) {
38
+ var ar = [];
39
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
40
+ return ar;
41
+ };
42
+ return ownKeys(o);
43
+ };
44
+ return function (mod) {
45
+ if (mod && mod.__esModule) return mod;
46
+ var result = {};
47
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
48
+ __setModuleDefault(result, mod);
49
+ return result;
50
+ };
51
+ })();
52
+ Object.defineProperty(exports, "__esModule", { value: true });
53
+ exports.CopilotCollector = void 0;
54
+ const fs = __importStar(require("fs"));
55
+ const os = __importStar(require("os"));
56
+ const path = __importStar(require("path"));
57
+ const types_1 = require("./types");
58
+ const process_1 = require("./process");
59
+ /** Minimal flat-YAML parser for Copilot CLI workspace.yaml. */
60
+ function parseWorkspaceYaml(filePath) {
61
+ try {
62
+ const text = fs.readFileSync(filePath, 'utf8');
63
+ const result = {};
64
+ for (const line of text.split('\n')) {
65
+ const idx = line.indexOf(': ');
66
+ if (idx === -1)
67
+ continue;
68
+ result[line.slice(0, idx).trim()] = line.slice(idx + 2).trim();
69
+ }
70
+ if (!result['id'] || !result['cwd'])
71
+ return null;
72
+ return result;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ }
78
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
79
+ function applyEvents(acc, events) {
80
+ for (const e of events) {
81
+ const ts = e.timestamp ? new Date(e.timestamp).getTime() : 0;
82
+ const data = e.data ?? {};
83
+ switch (e.type) {
84
+ case 'session.start':
85
+ if (acc.startedAt === 0) {
86
+ acc.startedAt = new Date(data.startTime ?? e.timestamp ?? Date.now()).getTime();
87
+ }
88
+ break;
89
+ case 'session.model_change':
90
+ if (data.newModel)
91
+ acc.model = data.newModel;
92
+ break;
93
+ case 'session.compaction_complete':
94
+ if (data.preCompactionTokens) {
95
+ acc.contextTokens = data.preCompactionTokens;
96
+ acc.compactionCount++;
97
+ }
98
+ break;
99
+ case 'assistant.message':
100
+ if (data.outputTokens)
101
+ acc.totalOutputTokens += data.outputTokens;
102
+ if (ts) {
103
+ acc.lastAssistantMsgMs = ts;
104
+ acc.lastActivityMs = ts;
105
+ }
106
+ acc.pendingToolCallId = null;
107
+ break;
108
+ case 'assistant.turn_end':
109
+ acc.turnCount++;
110
+ if (ts)
111
+ acc.lastActivityMs = ts;
112
+ break;
113
+ case 'tool.execution_start': {
114
+ const rawName = data.toolName ?? '';
115
+ const toolName = rawName.includes('.') ? rawName.split('.').pop() ?? rawName : rawName;
116
+ const args = data.arguments ?? {};
117
+ const rawArg = args.path ?? args.command ?? args.pattern ?? args.query ?? args.url ?? '';
118
+ const tc = {
119
+ name: toolName || 'unknown',
120
+ arg: typeof rawArg === 'string' ? rawArg.slice(0, 120) : '',
121
+ durationMs: 0,
122
+ };
123
+ acc.toolCalls.push(tc);
124
+ if (acc.toolCalls.length > 200)
125
+ acc.toolCalls = acc.toolCalls.slice(-200);
126
+ if (ts)
127
+ acc.lastActivityMs = ts;
128
+ acc.pendingToolCallId = data.toolCallId ?? null;
129
+ break;
130
+ }
131
+ case 'tool.execution_complete':
132
+ if (ts)
133
+ acc.lastActivityMs = ts;
134
+ acc.pendingToolCallId = null;
135
+ break;
136
+ case 'user.message': {
137
+ const content = data.content ?? '';
138
+ if (content.trim())
139
+ acc.currentTask = content.trim().slice(0, 200);
140
+ if (ts) {
141
+ acc.lastUserMsgMs = ts;
142
+ acc.lastActivityMs = ts;
143
+ }
144
+ break;
145
+ }
146
+ }
147
+ }
148
+ }
149
+ /** Read new lines from a JSONL file starting at startOffset. */
150
+ function readEventsFrom(filePath, startOffset) {
151
+ try {
152
+ const stat = fs.statSync(filePath);
153
+ if (stat.size <= startOffset)
154
+ return { events: [], newOffset: startOffset };
155
+ const fd = fs.openSync(filePath, 'r');
156
+ const bufSize = stat.size - startOffset;
157
+ const buf = Buffer.alloc(bufSize);
158
+ const bytesRead = fs.readSync(fd, buf, 0, bufSize, startOffset);
159
+ fs.closeSync(fd);
160
+ const text = buf.slice(0, bytesRead).toString('utf8');
161
+ const lastNl = text.lastIndexOf('\n');
162
+ if (lastNl === -1)
163
+ return { events: [], newOffset: startOffset };
164
+ const chunk = text.slice(0, lastNl);
165
+ const newOffset = startOffset + Buffer.byteLength(chunk + '\n', 'utf8');
166
+ const events = [];
167
+ for (const line of chunk.split('\n')) {
168
+ if (!line.trim())
169
+ continue;
170
+ try {
171
+ events.push(JSON.parse(line));
172
+ }
173
+ catch { /* skip malformed */ }
174
+ }
175
+ return { events, newOffset };
176
+ }
177
+ catch {
178
+ return { events: [], newOffset: startOffset };
179
+ }
180
+ }
181
+ // ─── Collector class ──────────────────────────────────────────────────────────
182
+ class CopilotCollector {
183
+ /**
184
+ * @param sessionsDir Directory holding `<uuid>/` per-session subdirs.
185
+ * Defaults to `~/.copilot/session-state` (Copilot CLI's
186
+ * real location); tests can pass a fixture path.
187
+ */
188
+ constructor(sessionsDir = path.join(os.homedir(), '.copilot', 'session-state')) {
189
+ this.sessionsDir = sessionsDir;
190
+ this.eventsCache = {};
191
+ }
192
+ /**
193
+ * Collect all live GitHub Copilot CLI sessions.
194
+ *
195
+ * Discovery uses lock files at <sessionsDir>/{uuid}/inuse.{PID}.lock rather
196
+ * than scanning the ps snapshot, giving us access to rich session data.
197
+ *
198
+ * @param processInfo Current ps snapshot (pid to ProcInfo)
199
+ * @param childrenMap Parent to children adjacency
200
+ * @param ports pid to listening port list
201
+ * @param gitMap cwd to {added, modified}
202
+ */
203
+ collect(processInfo, childrenMap, ports, gitMap) {
204
+ const sessionsDir = this.sessionsDir;
205
+ if (!fs.existsSync(sessionsDir))
206
+ return [];
207
+ const sessions = [];
208
+ let sessionDirs;
209
+ try {
210
+ sessionDirs = fs.readdirSync(sessionsDir);
211
+ }
212
+ catch {
213
+ return [];
214
+ }
215
+ for (const dirName of sessionDirs) {
216
+ const sessionDir = path.join(sessionsDir, dirName);
217
+ const session = this.loadSession(sessionDir, processInfo, childrenMap, ports, gitMap);
218
+ if (session)
219
+ sessions.push(session);
220
+ }
221
+ this.evictStaleCache(sessions);
222
+ sessions.sort((a, b) => b.startedAt - a.startedAt);
223
+ return sessions;
224
+ }
225
+ evictStaleCache(sessions) {
226
+ const activeIds = new Set(sessions.map((s) => s.sessionId));
227
+ for (const sid of Object.keys(this.eventsCache)) {
228
+ if (!activeIds.has(sid))
229
+ delete this.eventsCache[sid];
230
+ }
231
+ }
232
+ loadSession(sessionDir, processInfo, childrenMap, ports, gitMap) {
233
+ // Require a workspace.yaml
234
+ const workspacePath = path.join(sessionDir, 'workspace.yaml');
235
+ if (!fs.existsSync(workspacePath))
236
+ return null;
237
+ const meta = parseWorkspaceYaml(workspacePath);
238
+ if (!meta)
239
+ return null;
240
+ // Find any lock file: inuse.<pid>.lock → extract PID
241
+ let activePid = null;
242
+ try {
243
+ for (const f of fs.readdirSync(sessionDir)) {
244
+ const m = f.match(/^inuse\.(\d+)\.lock$/);
245
+ if (m) {
246
+ activePid = parseInt(m[1], 10);
247
+ break;
248
+ }
249
+ }
250
+ }
251
+ catch {
252
+ return null;
253
+ }
254
+ // Skip sessions with no lock file (completed, not currently running)
255
+ if (activePid === null)
256
+ return null;
257
+ // Confirm the PID is actually alive in the ps snapshot
258
+ const proc = processInfo.get(activePid);
259
+ const pidAlive = proc != null;
260
+ // Parse events.jsonl incrementally
261
+ const eventsPath = path.join(sessionDir, 'events.jsonl');
262
+ const sessionId = meta.id;
263
+ let cached = this.eventsCache[sessionId];
264
+ if (!cached) {
265
+ cached = {
266
+ model: '',
267
+ startedAt: 0,
268
+ totalOutputTokens: 0,
269
+ contextTokens: 0,
270
+ compactionCount: 0,
271
+ turnCount: 0,
272
+ toolCalls: [],
273
+ currentTask: '',
274
+ lastActivityMs: 0,
275
+ lastUserMsgMs: 0,
276
+ lastAssistantMsgMs: 0,
277
+ pendingToolCallId: null,
278
+ newOffset: 0,
279
+ };
280
+ }
281
+ if (fs.existsSync(eventsPath)) {
282
+ const { events, newOffset } = readEventsFrom(eventsPath, cached.newOffset);
283
+ if (events.length > 0) {
284
+ applyEvents(cached, events);
285
+ cached.newOffset = newOffset;
286
+ }
287
+ }
288
+ this.eventsCache[sessionId] = cached;
289
+ // Determine status
290
+ const now = Date.now();
291
+ const idleSecs = (now - cached.lastActivityMs) / 1000;
292
+ let status;
293
+ if (!pidAlive) {
294
+ status = 'done';
295
+ }
296
+ else if (cached.pendingToolCallId) {
297
+ status = 'executing';
298
+ }
299
+ else if (cached.lastUserMsgMs > cached.lastAssistantMsgMs && idleSecs < 60) {
300
+ status = 'thinking';
301
+ }
302
+ else if (idleSecs > 30) {
303
+ status = 'waiting';
304
+ }
305
+ else {
306
+ status = 'thinking';
307
+ }
308
+ // Resolve context window + percent
309
+ const contextWindow = (0, types_1.contextWindowForModel)(cached.model, cached.contextTokens);
310
+ const contextPercent = cached.contextTokens > 0
311
+ ? Math.min(100, Math.round((cached.contextTokens / contextWindow) * 100))
312
+ : 0;
313
+ // Current task: prefer workspace summary (set after compaction), else last user message
314
+ const taskStr = meta.summary || cached.currentTask || '';
315
+ const currentTasks = taskStr ? [taskStr] : [];
316
+ // Child processes
317
+ const childPids = (proc ? childrenMap.get(activePid) ?? [] : []);
318
+ const children = childPids
319
+ .map((cpid) => {
320
+ const cp = processInfo.get(cpid);
321
+ if (!cp)
322
+ return null;
323
+ return {
324
+ pid: cpid,
325
+ command: cp.command.split(/\s+/)[0].split('/').pop() ?? cp.command,
326
+ memKb: cp.rssKb,
327
+ port: (ports.get(cpid) ?? [])[0],
328
+ };
329
+ })
330
+ .filter((c) => c !== null);
331
+ // Git stats
332
+ const cwd = meta.cwd;
333
+ const git = gitMap.get(cwd) ?? (0, process_1.collectGitStats)(cwd);
334
+ return {
335
+ agentCli: 'copilot',
336
+ pid: activePid,
337
+ sessionId,
338
+ cwd,
339
+ projectName: path.basename(cwd) || cwd,
340
+ startedAt: cached.startedAt || new Date(meta.created_at || Date.now()).getTime(),
341
+ status,
342
+ model: cached.model || 'github-copilot',
343
+ effort: '',
344
+ contextPercent,
345
+ totalInputTokens: 0,
346
+ totalOutputTokens: cached.totalOutputTokens,
347
+ totalCacheRead: 0,
348
+ totalCacheCreate: 0,
349
+ turnCount: cached.turnCount,
350
+ currentTasks,
351
+ memMb: proc ? Math.round(proc.rssKb / 1024) : 0,
352
+ version: '',
353
+ gitBranch: meta.branch || '',
354
+ gitAdded: git.added,
355
+ gitModified: git.modified,
356
+ tokenHistory: [],
357
+ contextHistory: [],
358
+ compactionCount: cached.compactionCount,
359
+ contextWindow,
360
+ subagents: [],
361
+ memFileCount: 0,
362
+ memLineCount: 0,
363
+ children,
364
+ initialPrompt: currentTasks[0] ?? '',
365
+ firstAssistantText: '',
366
+ toolCalls: cached.toolCalls,
367
+ pendingSinceMs: cached.pendingToolCallId ? cached.lastActivityMs : 0,
368
+ thinkingSinceMs: cached.lastUserMsgMs > cached.lastAssistantMsgMs ? cached.lastUserMsgMs : 0,
369
+ fileAccesses: [],
370
+ };
371
+ }
372
+ }
373
+ exports.CopilotCollector = CopilotCollector;
@@ -0,0 +1,221 @@
1
+ "use strict";
2
+ /**
3
+ * Multi-collector orchestrator.
4
+ *
5
+ * Mirrors abtop/src/collector/mod.rs `MultiCollector`.
6
+ * Runs Claude and Codex collectors on every tick, staggered so the
7
+ * expensive operations (lsof, git) only run on slow ticks.
8
+ *
9
+ * Usage:
10
+ * const mc = new MultiCollector();
11
+ * setInterval(() => {
12
+ * const snapshot = mc.tick();
13
+ * // use snapshot.sessions, snapshot.rateLimits, snapshot.orphanPorts
14
+ * }, 2000);
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.MultiCollector = void 0;
18
+ const process_1 = require("./process");
19
+ const claude_1 = require("./claude");
20
+ const codex_1 = require("./codex");
21
+ const copilot_1 = require("./copilot");
22
+ const rate_limit_1 = require("./rate-limit");
23
+ const secrets_1 = require("./secrets");
24
+ class MultiCollector {
25
+ constructor() {
26
+ this.claude = new claude_1.ClaudeCollector();
27
+ this.codex = new codex_1.CodexCollector();
28
+ this.copilot = new copilot_1.CopilotCollector();
29
+ this.tickCount = 0;
30
+ /** Ports seen in the previous tick that had a live session behind them. */
31
+ this.prevSessionPids = new Set();
32
+ /** Known orphan ports (pid → port[]) persisted across ticks. */
33
+ this.orphanPortMap = new Map();
34
+ /** Polling interval config (mirrors abtop defaults). */
35
+ this.SLOW_TICK_EVERY = 5; // slow tick every 5 fast ticks (= every 10s at 2s interval)
36
+ }
37
+ tick() {
38
+ this.tickCount++;
39
+ const slowTick = this.tickCount % this.SLOW_TICK_EVERY === 0;
40
+ // ── Process table (fast tick) ───────────────────────────────────────────
41
+ const processInfo = (0, process_1.getProcessInfo)();
42
+ const childrenMap = (0, process_1.getChildrenMap)(processInfo);
43
+ // ── Ports + git (slow tick only) ─────────────────────────────────────────
44
+ let ports = new Map();
45
+ let gitMap = new Map();
46
+ if (slowTick) {
47
+ ports = (0, process_1.getListeningPorts)();
48
+ }
49
+ // ── Collect sessions ──────────────────────────────────────────────────────
50
+ const claudeSessions = this.claude.collect(processInfo, childrenMap, ports, gitMap);
51
+ const codexSessions = this.codex.collect(processInfo, childrenMap, ports, gitMap);
52
+ const copilotSessions = this.copilot.collect(processInfo, childrenMap, ports, gitMap);
53
+ const sessions = [...claudeSessions, ...codexSessions, ...copilotSessions];
54
+ // ── Git stats (slow tick, after session list is known) ────────────────────
55
+ if (slowTick) {
56
+ const cwds = new Set(sessions.map((s) => s.cwd));
57
+ for (const cwd of cwds) {
58
+ gitMap.set(cwd, (0, process_1.collectGitStats)(cwd));
59
+ }
60
+ // Backfill git stats into sessions
61
+ for (const s of sessions) {
62
+ const g = gitMap.get(s.cwd);
63
+ if (g) {
64
+ s.gitAdded = g.added;
65
+ s.gitModified = g.modified;
66
+ }
67
+ }
68
+ }
69
+ // ── Rate limits ───────────────────────────────────────────────────────────
70
+ const rateLimits = [];
71
+ if (slowTick) {
72
+ rateLimits.push(...(0, rate_limit_1.readClaudeRateLimits)());
73
+ }
74
+ const codexRl = this.codex.lastRateLimit ?? (0, rate_limit_1.readCodexRateLimitCache)();
75
+ if (codexRl)
76
+ rateLimits.push(codexRl);
77
+ // ── Orphan port detection ─────────────────────────────────────────────────
78
+ const orphanPorts = this.detectOrphanPorts(sessions, processInfo, ports, slowTick);
79
+ return {
80
+ sessions: sessions.map(normalizeSession),
81
+ rateLimits,
82
+ orphanPorts,
83
+ collectedAt: Date.now(),
84
+ };
85
+ }
86
+ detectOrphanPorts(sessions, processInfo, ports, slowTick) {
87
+ if (!slowTick)
88
+ return [...this.flattenOrphans()];
89
+ const sessionPids = new Set(sessions.map((s) => s.pid));
90
+ const sessionChildPids = new Set();
91
+ for (const s of sessions) {
92
+ for (const c of s.children)
93
+ sessionChildPids.add(c.pid);
94
+ }
95
+ // Any port-holding PID that is NOT under a live session → orphan candidate
96
+ for (const [pid, pidPorts] of ports) {
97
+ if (sessionPids.has(pid) || sessionChildPids.has(pid)) {
98
+ // Still alive under a session — remove from orphan map if present
99
+ this.orphanPortMap.delete(pid);
100
+ continue;
101
+ }
102
+ // Port-holding PID with no parent session
103
+ const proc = processInfo.get(pid);
104
+ if (!proc) {
105
+ this.orphanPortMap.delete(pid);
106
+ continue;
107
+ }
108
+ this.orphanPortMap.set(pid, pidPorts.map((port) => ({
109
+ port,
110
+ command: proc.command,
111
+ projectName: '?',
112
+ })));
113
+ }
114
+ // Remove orphan entries whose PID has died
115
+ for (const pid of this.orphanPortMap.keys()) {
116
+ if (!processInfo.has(pid))
117
+ this.orphanPortMap.delete(pid);
118
+ }
119
+ return [...this.flattenOrphans()];
120
+ }
121
+ flattenOrphans() {
122
+ const result = [];
123
+ for (const [pid, entries] of this.orphanPortMap) {
124
+ for (const e of entries) {
125
+ result.push({ pid, port: e.port, command: e.command, projectName: e.projectName });
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+ }
131
+ exports.MultiCollector = MultiCollector;
132
+ // ─── Snapshot normalization ───────────────────────────────────────────────────
133
+ const VALID_STATUSES = new Set([
134
+ 'thinking',
135
+ 'executing',
136
+ 'waiting',
137
+ 'rateLimited',
138
+ 'done',
139
+ ]);
140
+ function num(n) {
141
+ return typeof n === 'number' && Number.isFinite(n) ? n : 0;
142
+ }
143
+ function str(s) {
144
+ return typeof s === 'string' ? s : '';
145
+ }
146
+ function arr(a) {
147
+ return Array.isArray(a) ? a : [];
148
+ }
149
+ /**
150
+ * Single source of truth for the snapshot wire shape. Every `AgentSession` that
151
+ * leaves `tick()` is run through this so:
152
+ *
153
+ * • numeric fields are real numbers (never null / undefined / NaN);
154
+ * • array fields are arrays (never null / undefined);
155
+ * • status is a known `SessionStatus` (any unrecognized value defaults to
156
+ * 'waiting' — keeps the front end's filter sets honest);
157
+ * • free-text fields that originate from agent-controlled content
158
+ * (`currentTasks`, `initialPrompt`, `firstAssistantText`, `toolCalls[].arg`,
159
+ * `fileAccesses[].path`) are run through `redactSecrets()` so a pasted
160
+ * `sk-ant-…` doesn't end up rendered in the dashboard.
161
+ *
162
+ * Front-end null-coalescing guards (e.g. `value ?? 0`) become defence-in-depth
163
+ * after this runs server-side.
164
+ */
165
+ function normalizeSession(s) {
166
+ const status = VALID_STATUSES.has(s.status) ? s.status : 'waiting';
167
+ return {
168
+ agentCli: str(s.agentCli),
169
+ pid: num(s.pid),
170
+ sessionId: str(s.sessionId),
171
+ cwd: str(s.cwd),
172
+ projectName: str(s.projectName),
173
+ startedAt: num(s.startedAt),
174
+ status,
175
+ model: str(s.model),
176
+ effort: str(s.effort),
177
+ contextPercent: num(s.contextPercent),
178
+ totalInputTokens: num(s.totalInputTokens),
179
+ totalOutputTokens: num(s.totalOutputTokens),
180
+ totalCacheRead: num(s.totalCacheRead),
181
+ totalCacheCreate: num(s.totalCacheCreate),
182
+ turnCount: num(s.turnCount),
183
+ currentTasks: arr(s.currentTasks).map((t) => (0, secrets_1.redactSecrets)(str(t))),
184
+ memMb: num(s.memMb),
185
+ version: str(s.version),
186
+ gitBranch: str(s.gitBranch),
187
+ gitAdded: num(s.gitAdded),
188
+ gitModified: num(s.gitModified),
189
+ tokenHistory: arr(s.tokenHistory).map(num),
190
+ contextHistory: arr(s.contextHistory).map(num),
191
+ compactionCount: num(s.compactionCount),
192
+ contextWindow: num(s.contextWindow) || 200000,
193
+ subagents: arr(s.subagents).map((sa) => ({
194
+ name: str(sa?.name),
195
+ status: sa?.status === 'done' ? 'done' : 'working',
196
+ tokens: num(sa?.tokens),
197
+ })),
198
+ memFileCount: num(s.memFileCount),
199
+ memLineCount: num(s.memLineCount),
200
+ children: arr(s.children).map((c) => ({
201
+ pid: num(c?.pid),
202
+ command: str(c?.command),
203
+ memKb: num(c?.memKb),
204
+ port: typeof c?.port === 'number' ? c.port : undefined,
205
+ })),
206
+ initialPrompt: (0, secrets_1.redactSecrets)(str(s.initialPrompt)),
207
+ firstAssistantText: (0, secrets_1.redactSecrets)(str(s.firstAssistantText)),
208
+ toolCalls: arr(s.toolCalls).map((tc) => ({
209
+ name: str(tc?.name),
210
+ arg: (0, secrets_1.redactSecrets)(str(tc?.arg)),
211
+ durationMs: num(tc?.durationMs),
212
+ })),
213
+ pendingSinceMs: num(s.pendingSinceMs),
214
+ thinkingSinceMs: num(s.thinkingSinceMs),
215
+ fileAccesses: arr(s.fileAccesses).map((fa) => ({
216
+ path: (0, secrets_1.redactSecrets)(str(fa?.path)),
217
+ operation: fa?.operation === 'Write' || fa?.operation === 'Edit' ? fa.operation : 'Read',
218
+ turnIndex: num(fa?.turnIndex),
219
+ })),
220
+ };
221
+ }