loadtoagent 1.0.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.
Files changed (82) hide show
  1. package/README.ko.md +175 -0
  2. package/README.md +175 -0
  3. package/README.zh-CN.md +175 -0
  4. package/bin/loadtoagent.js +171 -0
  5. package/docs/ARCHITECTURE.md +30 -0
  6. package/docs/PROVIDER-CONTRACTS.md +58 -0
  7. package/docs/assets/loadtoagent-dashboard.png +0 -0
  8. package/docs/assets/loadtoagent-demo.gif +0 -0
  9. package/main.js +493 -0
  10. package/package.json +133 -0
  11. package/preload.js +75 -0
  12. package/renderer/app-agent-actions.js +285 -0
  13. package/renderer/app-bootstrap.js +95 -0
  14. package/renderer/app-dashboard.js +361 -0
  15. package/renderer/app-drawer-content.js +203 -0
  16. package/renderer/app-drawer-data.js +41 -0
  17. package/renderer/app-drawer.js +183 -0
  18. package/renderer/app-events-dialogs.js +169 -0
  19. package/renderer/app-events-filters.js +74 -0
  20. package/renderer/app-events-navigation.js +96 -0
  21. package/renderer/app-events-sessions.js +195 -0
  22. package/renderer/app-events.js +16 -0
  23. package/renderer/app-graph-layout.js +71 -0
  24. package/renderer/app-graph-model.js +107 -0
  25. package/renderer/app-graph-orchestration.js +76 -0
  26. package/renderer/app-graph-view.js +544 -0
  27. package/renderer/app-run-modal.js +221 -0
  28. package/renderer/app-session-render.js +243 -0
  29. package/renderer/app-tmux-render.js +247 -0
  30. package/renderer/app.js +608 -0
  31. package/renderer/fonts/geist-ext.woff2 +0 -0
  32. package/renderer/fonts/geist.woff2 +0 -0
  33. package/renderer/i18n-messages.js +355 -0
  34. package/renderer/i18n.js +122 -0
  35. package/renderer/index.html +386 -0
  36. package/renderer/shared.js +26 -0
  37. package/renderer/styles-agent-map.css +401 -0
  38. package/renderer/styles-cards.css +600 -0
  39. package/renderer/styles-collaboration.css +534 -0
  40. package/renderer/styles-components.css +1293 -0
  41. package/renderer/styles-onboarding.css +344 -0
  42. package/renderer/styles-overlays.css +284 -0
  43. package/renderer/styles-product.css +686 -0
  44. package/renderer/styles-responsive-product.css +689 -0
  45. package/renderer/styles-responsive-runtime.css +718 -0
  46. package/renderer/styles-responsive-shell.css +533 -0
  47. package/renderer/styles-responsive-workflows.css +778 -0
  48. package/renderer/styles-run-composer.css +695 -0
  49. package/renderer/styles-settings.css +606 -0
  50. package/renderer/styles-terminal.css +1241 -0
  51. package/renderer/styles-tmux.css +1162 -0
  52. package/renderer/styles-workflow-map.css +513 -0
  53. package/renderer/styles-workflows.css +1217 -0
  54. package/renderer/styles.css +486 -0
  55. package/renderer/terminal-agent.js +152 -0
  56. package/renderer/terminal-events.js +226 -0
  57. package/renderer/terminal-workbench.js +464 -0
  58. package/renderer/terminal.js +497 -0
  59. package/src/agentMonitor/claudeParser.js +197 -0
  60. package/src/agentMonitor/codexCollaboration.js +95 -0
  61. package/src/agentMonitor/codexParser.js +447 -0
  62. package/src/agentMonitor/genericParser.js +244 -0
  63. package/src/agentMonitor/hierarchy.js +248 -0
  64. package/src/agentMonitor/sessionFiles.js +86 -0
  65. package/src/agentMonitor.js +591 -0
  66. package/src/agentRunner.js +465 -0
  67. package/src/bridgeServer.js +246 -0
  68. package/src/contracts.js +71 -0
  69. package/src/diagnostics.js +23 -0
  70. package/src/ipc/registerAgentIpc.js +12 -0
  71. package/src/ipc/registerAppIpc.js +20 -0
  72. package/src/ipc/registerTerminalIpc.js +50 -0
  73. package/src/ipc/registerTmuxIpc.js +30 -0
  74. package/src/ipc/registerWorkspaceIpc.js +14 -0
  75. package/src/monitorWorker.js +273 -0
  76. package/src/processMonitor.js +394 -0
  77. package/src/providerRegistry.js +120 -0
  78. package/src/terminalManager.js +438 -0
  79. package/src/tmuxController.js +183 -0
  80. package/src/tmuxMonitor.js +432 -0
  81. package/src/updateManager.js +339 -0
  82. package/src/workspaceStore.js +77 -0
@@ -0,0 +1,244 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const TOOL_START_PATTERN = /tool_use|tool-call|tool_start/;
6
+ const TOOL_END_PATTERN = /tool_result|tool-result|tool_end/;
7
+
8
+ function createGenericParser(dependencies) {
9
+ const {
10
+ ACTIVE_THRESHOLD_MS,
11
+ STALE_TURN_THRESHOLD_MS,
12
+ addLifecycle,
13
+ addMessage,
14
+ baseSession,
15
+ compactText,
16
+ contextInfo,
17
+ finalizeUsage,
18
+ modelContextWindow,
19
+ readJson,
20
+ readJsonLines,
21
+ settleLifecycle,
22
+ sumUsage,
23
+ timestamp,
24
+ trimSession,
25
+ } = dependencies;
26
+
27
+ function normalizeUsage(raw = {}) {
28
+ const usage = raw.usageMetadata || raw.usage_metadata || raw.usage
29
+ || raw.stats || raw.tokens || raw;
30
+ return finalizeUsage({
31
+ input: usage.input_tokens || usage.inputTokenCount || usage.prompt_tokens || usage.promptTokenCount,
32
+ cachedInput: usage.cached_input_tokens || usage.cachedContentTokenCount || usage.cached_tokens,
33
+ cacheWrite: usage.cache_creation_input_tokens,
34
+ output: usage.output_tokens || usage.candidatesTokenCount
35
+ || usage.completion_tokens || usage.response_tokens,
36
+ reasoning: usage.reasoning_tokens || usage.thoughtsTokenCount,
37
+ total: usage.total_tokens || usage.totalTokenCount || usage.total_token_count,
38
+ });
39
+ }
40
+
41
+ function flattenRows(value, out = [], depth = 0, seen = new Set()) {
42
+ if (depth > 6 || value == null) return out;
43
+ if (Array.isArray(value)) {
44
+ for (const item of value) flattenRows(item, out, depth + 1, seen);
45
+ return out;
46
+ }
47
+ if (typeof value !== 'object' || seen.has(value)) return out;
48
+ seen.add(value);
49
+ const role = value.role || value.author || value.sender;
50
+ const deltaText = typeof value.delta === 'string' ? value.delta : '';
51
+ const text = compactText(value.text || value.content || value.message
52
+ || value.response || value.prompt || deltaText);
53
+ if (role && text) out.push({ value, order: out.length });
54
+ for (const key of ['messages', 'history', 'turns', 'events', 'conversation']) {
55
+ if (value[key]) flattenRows(value[key], out, depth + 1, seen);
56
+ }
57
+ return out;
58
+ }
59
+
60
+ function readSessionFile(fileInfo) {
61
+ const isJsonl = /\.jsonl$/i.test(fileInfo.file);
62
+ return isJsonl
63
+ ? readJsonLines(fileInfo.file)
64
+ : { rows: [readJson(fileInfo.file, {})], truncated: false };
65
+ }
66
+
67
+ function initializeSession(fileInfo, provider, parsed) {
68
+ const rows = parsed.rows.filter(Boolean);
69
+ const root = rows.length === 1 ? rows[0] : { events: rows };
70
+ const externalId = root.session_id || root.sessionId || root.id
71
+ || path.basename(fileInfo.file).replace(/\.(jsonl?|ndjson)$/i, '');
72
+ const session = baseSession(provider, externalId, fileInfo.file, fileInfo);
73
+ session.truncated = parsed.truncated;
74
+ session.cwd = root.cwd || root.projectPath || root.project_path || '';
75
+ session.model = root.model || root.modelId || '';
76
+ session.startedAt = timestamp(root.startTime || root.startedAt || root.created_at, session.updatedAt);
77
+ session.parentId = root.parent_session_id ? `${provider}:${root.parent_session_id}` : null;
78
+ session.depth = session.parentId ? 1 : 0;
79
+ return { rows, root, session };
80
+ }
81
+
82
+ function recordToolStart(session, event) {
83
+ const tool = event.tool_name || event.name || event.tool || 'tool';
84
+ addMessage(session, {
85
+ id: event.id,
86
+ role: 'tool',
87
+ type: 'tool',
88
+ title: tool,
89
+ text: compactText(event.parameters || event.args || event.input, 1000),
90
+ status: 'started',
91
+ timestamp: event.timestamp,
92
+ });
93
+ addLifecycle(session, {
94
+ id: event.id,
95
+ type: 'tool',
96
+ label: tool,
97
+ status: 'running',
98
+ timestamp: event.timestamp,
99
+ });
100
+ }
101
+
102
+ function recordToolEnd(session, event) {
103
+ const callId = event.tool_call_id || event.tool_use_id || event.id;
104
+ settleLifecycle(session, callId, event.error ? 'failed' : 'done', event.timestamp);
105
+ addLifecycle(session, {
106
+ id: `result:${event.id || event.tool_call_id}`,
107
+ type: 'tool-result',
108
+ label: '도구 완료',
109
+ status: event.error ? 'failed' : 'done',
110
+ timestamp: event.timestamp,
111
+ });
112
+ }
113
+
114
+ function processEvents(session, events) {
115
+ const state = { running: false, failed: false };
116
+ for (const event of events) {
117
+ const type = String(event.type || event.event || event.kind || '').toLowerCase();
118
+ if (type === 'init') {
119
+ session.model = event.model || session.model;
120
+ session.externalId = event.session_id || event.sessionId || session.externalId;
121
+ addLifecycle(session, {
122
+ id: `init:${event.timestamp || 0}`,
123
+ type: 'session-start',
124
+ label: '세션 시작',
125
+ status: 'done',
126
+ timestamp: event.timestamp,
127
+ });
128
+ }
129
+ if (TOOL_START_PATTERN.test(type)) {
130
+ recordToolStart(session, event);
131
+ state.running = true;
132
+ }
133
+ if (TOOL_END_PATTERN.test(type)) recordToolEnd(session, event);
134
+ if (type === 'result' || /session_end|completed/.test(type)) state.running = false;
135
+ if (type === 'error' || event.error) state.failed = true;
136
+ const usage = normalizeUsage(event);
137
+ if (usage.total) session.usage = usage;
138
+ }
139
+ return state;
140
+ }
141
+
142
+ function normalizedMessage(row, session) {
143
+ const item = row.value;
144
+ const eventType = String(item.type || item.event || item.kind || '').toLowerCase();
145
+ if (TOOL_START_PATTERN.test(eventType) || TOOL_END_PATTERN.test(eventType)) return null;
146
+ const rawRole = String(item.role || item.author || item.sender || '').toLowerCase();
147
+ const role = /assistant|model|agent/.test(rawRole)
148
+ ? 'assistant'
149
+ : (rawRole === 'user' ? 'user' : 'system');
150
+ const deltaText = typeof item.delta === 'string' ? item.delta : '';
151
+ const text = compactText(item.text || item.content || item.message
152
+ || item.response || item.prompt || deltaText);
153
+ const id = item.id || item.uuid || '';
154
+ const recordedAt = timestamp(item.timestamp || item.created_at, session.updatedAt);
155
+ const isDelta = item.is_delta === true || item.delta === true
156
+ || typeof item.delta === 'string'
157
+ || /(?:^|[_-])delta(?:$|[_-])/.test(eventType);
158
+ return {
159
+ item,
160
+ role,
161
+ text,
162
+ id,
163
+ recordedAt,
164
+ isDelta,
165
+ key: id ? `${role}:${id}` : `${role}:${text}:${recordedAt}`,
166
+ order: row.order,
167
+ };
168
+ }
169
+
170
+ function processMessages(session, root) {
171
+ const messages = new Map();
172
+ const usages = [];
173
+ let firstUser = '';
174
+ for (const row of flattenRows(root)) {
175
+ const message = normalizedMessage(row, session);
176
+ if (!message) continue;
177
+ const previous = messages.get(message.key);
178
+ const mergedText = previous && message.isDelta
179
+ ? `${previous.text}${message.text}`
180
+ : message.text;
181
+ if (!previous || message.isDelta || message.text.length >= previous.text.length) {
182
+ messages.set(message.key, {
183
+ id: message.id,
184
+ role: message.role,
185
+ text: mergedText,
186
+ timestamp: message.recordedAt,
187
+ order: previous ? previous.order : message.order,
188
+ });
189
+ }
190
+ if (message.role === 'user' && !firstUser) firstUser = message.text;
191
+ const usage = normalizeUsage(message.item);
192
+ if (usage.total) {
193
+ session.turnUsage = usage;
194
+ usages.push(usage);
195
+ }
196
+ }
197
+ [...messages.values()]
198
+ .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp) || a.order - b.order)
199
+ .forEach(message => addMessage(session, message));
200
+ return { firstUser, usages };
201
+ }
202
+
203
+ function finalizeSession(session, provider, root, eventState, messageState, fileInfo) {
204
+ if (!session.usage.total && messageState.usages.length) {
205
+ session.usage = sumUsage(messageState.usages);
206
+ }
207
+ session.title = messageState.firstUser || `${provider === 'gemini' ? 'Gemini' : 'Grok'} 세션`;
208
+ const context = modelContextWindow(provider, session.model, root.context_window || root.contextWindow);
209
+ session.context = contextInfo(session.turnUsage.total || session.usage.total, context);
210
+ const age = Date.now() - fileInfo.mtimeMs;
211
+ session.status = eventState.failed
212
+ ? 'failed'
213
+ : ((eventState.running && age < STALE_TURN_THRESHOLD_MS) || age < ACTIVE_THRESHOLD_MS
214
+ ? 'running'
215
+ : 'idle');
216
+ session.statusDetail = eventState.failed
217
+ ? '오류 발생'
218
+ : (session.status === 'running' ? '실시간 이벤트 수신 중' : '다음 요청 대기');
219
+ session.statusObserved = eventState.running;
220
+ trimSession(session);
221
+ return session;
222
+ }
223
+
224
+ return function parseGeneric(fileInfo, provider) {
225
+ const parsed = readSessionFile(fileInfo);
226
+ const initialized = initializeSession(fileInfo, provider, parsed);
227
+ if (!initialized.rows.length) return null;
228
+ const events = initialized.rows.length === 1 && Array.isArray(initialized.root.events)
229
+ ? initialized.root.events
230
+ : initialized.rows;
231
+ const eventState = processEvents(initialized.session, events);
232
+ const messageState = processMessages(initialized.session, initialized.root);
233
+ return finalizeSession(
234
+ initialized.session,
235
+ provider,
236
+ initialized.root,
237
+ eventState,
238
+ messageState,
239
+ fileInfo,
240
+ );
241
+ };
242
+ }
243
+
244
+ module.exports = { createGenericParser };
@@ -0,0 +1,248 @@
1
+ 'use strict';
2
+
3
+ function createHierarchyAttacher(dependencies) {
4
+ const {
5
+ addMessage,
6
+ baseSession,
7
+ collaborationTaskName,
8
+ compactText,
9
+ timestamp,
10
+ trimSession,
11
+ } = dependencies;
12
+
13
+ function indexExistingRelationships(sessions) {
14
+ const byId = new Map(sessions.map(session => [session.id, session]));
15
+ for (const session of sessions) session.childIds = [];
16
+ for (const session of sessions) {
17
+ if (!session.parentId) continue;
18
+ const parent = byId.get(session.parentId);
19
+ if (parent && !parent.childIds.includes(session.id)) parent.childIds.push(session.id);
20
+ }
21
+ return byId;
22
+ }
23
+
24
+ function findRecordedChild(sessions, byId, parent, record) {
25
+ if (record.childId && byId.has(record.childId)) return byId.get(record.childId);
26
+ return sessions.find(candidate => candidate.parentId === parent.id
27
+ && ((record.agentPath && candidate.agentPath === record.agentPath)
28
+ || (record.taskName && candidate.taskName === record.taskName)));
29
+ }
30
+
31
+ function createHistoryChild(parent, record) {
32
+ const externalId = String(record.childId || `${parent.externalId}:spawn:${record.callId}`).replace(/^codex:/, '');
33
+ const child = baseSession('codex', externalId, '', {
34
+ mtimeMs: Date.parse(record.completedAt || record.startedAt || parent.updatedAt) || Date.now(),
35
+ });
36
+ child.id = record.childId || child.id;
37
+ child.parentId = parent.id;
38
+ child.depth = Number(parent.depth || 0) + 1;
39
+ child.agentPath = record.agentPath;
40
+ child.taskName = record.taskName;
41
+ child.agentName = record.agentName || record.taskName || 'subagent';
42
+ child.title = record.assignmentObserved
43
+ ? compactText(record.assignment, 180)
44
+ : (record.taskName || '서브에이전트 작업');
45
+ child.sharedGoal = record.sharedGoal || parent.title;
46
+ child.status = record.status === 'completed' ? 'completed' : (record.status === 'running' ? 'running' : 'idle');
47
+ child.statusDetail = child.status === 'completed'
48
+ ? '작업 완료 기록'
49
+ : (child.status === 'running' ? '실행 시작 관측' : '상태 기록만 확인됨');
50
+ child.startedAt = timestamp(record.startedAt, parent.startedAt);
51
+ child.updatedAt = timestamp(record.completedAt || record.startedAt, parent.updatedAt);
52
+ child.completedAt = timestamp(record.completedAt, null);
53
+ child.completionObserved = child.status === 'completed';
54
+ child.result = record.result || '';
55
+ child.source = 'collaboration-history';
56
+ child.sourceLabel = 'Codex 협업 이벤트';
57
+ child.clientKind = parent.clientKind;
58
+ child.model = parent.model;
59
+ child.cwd = parent.cwd;
60
+ child.workspace = parent.workspace;
61
+ child.projectless = parent.projectless;
62
+ child.environment = parent.environment;
63
+ if (child.result) {
64
+ addMessage(child, {
65
+ id: `${child.id}:result`,
66
+ role: 'assistant',
67
+ text: child.result,
68
+ timestamp: child.completedAt || child.updatedAt,
69
+ });
70
+ }
71
+ trimSession(child);
72
+ return child;
73
+ }
74
+
75
+ function resolveChild(sessions, byId, parent, record) {
76
+ const existing = findRecordedChild(sessions, byId, parent, record);
77
+ if (existing) return existing;
78
+ const child = createHistoryChild(parent, record);
79
+ sessions.push(child);
80
+ byId.set(child.id, child);
81
+ return child;
82
+ }
83
+
84
+ function connectCommunications(collaboration, record, child) {
85
+ for (const communication of collaboration.communications || []) {
86
+ if (communication.childId) continue;
87
+ if ((record.agentPath && (communication.from === record.agentPath || communication.to === record.agentPath))
88
+ || (record.taskName && communication.taskName === record.taskName)) {
89
+ communication.childId = child.id;
90
+ }
91
+ }
92
+ }
93
+
94
+ function synchronizeSpawn(parent, collaboration, retainedByTask, record, child) {
95
+ record.childId = child.id;
96
+ record.agentPath = record.agentPath || child.agentPath;
97
+ record.taskName = record.taskName || child.taskName || collaborationTaskName(child.agentPath);
98
+ record.agentName = child.agentName || record.agentName;
99
+ record.result = record.result || child.result || '';
100
+ if (child.status === 'running' || child.status === 'starting') record.status = 'running';
101
+ else if (child.status === 'completed' || child.completionObserved || record.result) record.status = 'completed';
102
+ if (!record.completedAt && child.completedAt) record.completedAt = child.completedAt;
103
+
104
+ const retained = retainedByTask.get(record.taskName);
105
+ record.currentlyRetained = Boolean(retained);
106
+ if (retained && retained.name && (!child.agentName || child.agentName === child.taskName)) {
107
+ child.agentName = retained.name;
108
+ }
109
+ child.taskName = child.taskName || record.taskName;
110
+ child.sharedGoal = child.sharedGoal || record.sharedGoal || parent.title;
111
+ child.delegation = {
112
+ taskName: record.taskName,
113
+ assignment: record.assignment,
114
+ assignmentObserved: record.assignmentObserved,
115
+ assignmentProtected: record.assignmentProtected,
116
+ assignmentSource: record.assignmentSource,
117
+ sharedGoal: record.sharedGoal || child.sharedGoal || parent.title,
118
+ result: record.result,
119
+ startedAt: record.startedAt,
120
+ completedAt: record.completedAt,
121
+ currentlyRetained: record.currentlyRetained,
122
+ };
123
+ if (!parent.childIds.includes(child.id)) parent.childIds.push(child.id);
124
+ connectCommunications(collaboration, record, child);
125
+ }
126
+
127
+ function inferredSpawn(parent, child) {
128
+ return {
129
+ callId: `inferred:${child.id}`,
130
+ taskName: child.taskName || collaborationTaskName(child.agentPath) || child.agentName,
131
+ agentPath: child.agentPath,
132
+ childId: child.id,
133
+ assignment: '',
134
+ assignmentObserved: false,
135
+ assignmentProtected: false,
136
+ assignmentSource: 'unavailable',
137
+ sharedGoal: child.sharedGoal || parent.title,
138
+ status: child.status === 'completed' ? 'completed' : (child.status === 'running' ? 'running' : 'idle'),
139
+ startedAt: child.startedAt,
140
+ completedAt: child.completedAt,
141
+ result: child.result || '',
142
+ agentName: child.agentName,
143
+ currentlyRetained: false,
144
+ inferred: true,
145
+ };
146
+ }
147
+
148
+ function attachInferredDelegation(parent, child, record) {
149
+ child.delegation = {
150
+ taskName: record.taskName,
151
+ assignment: '',
152
+ assignmentObserved: false,
153
+ assignmentProtected: true,
154
+ assignmentSource: 'unavailable',
155
+ sharedGoal: child.sharedGoal || parent.title,
156
+ result: child.result || '',
157
+ startedAt: child.startedAt,
158
+ completedAt: child.completedAt,
159
+ currentlyRetained: false,
160
+ };
161
+ }
162
+
163
+ function appendInferredCommunications(parent, collaboration, child, record) {
164
+ const communications = collaboration.communications || (collaboration.communications = []);
165
+ if (communications.some(event => event.childId === child.id)) return;
166
+ communications.push({
167
+ id: `inferred-assignment:${child.id}`,
168
+ kind: 'assignment',
169
+ label: '작업 배정 확인',
170
+ from: parent.agentPath || '/root',
171
+ to: child.agentPath || child.id,
172
+ taskName: record.taskName,
173
+ childId: child.id,
174
+ text: '',
175
+ protected: true,
176
+ timestamp: child.startedAt || parent.updatedAt,
177
+ });
178
+ communications.push({
179
+ id: `inferred-started:${child.id}`,
180
+ kind: 'started',
181
+ label: '서브에이전트 실행 시작',
182
+ from: 'Codex 런타임',
183
+ to: child.agentPath || child.id,
184
+ taskName: record.taskName,
185
+ childId: child.id,
186
+ text: '',
187
+ protected: false,
188
+ timestamp: child.startedAt || parent.updatedAt,
189
+ });
190
+ if (!child.result && child.status !== 'completed') return;
191
+ communications.push({
192
+ id: `inferred-result:${child.id}`,
193
+ kind: 'result',
194
+ label: '결과 반환 확인',
195
+ from: child.agentPath || child.id,
196
+ to: parent.agentPath || '/root',
197
+ taskName: record.taskName,
198
+ childId: child.id,
199
+ text: child.result || '작업을 완료하고 메인 AI에 결과를 반환했습니다.',
200
+ protected: false,
201
+ timestamp: child.completedAt || child.updatedAt,
202
+ });
203
+ }
204
+
205
+ function inferUnrecordedChildren(parent, collaboration, byId) {
206
+ for (const childId of parent.childIds) {
207
+ const child = byId.get(childId);
208
+ if (!child || collaboration.spawns.some(record => record.childId === child.id)) continue;
209
+ const record = inferredSpawn(parent, child);
210
+ collaboration.spawns.push(record);
211
+ attachInferredDelegation(parent, child, record);
212
+ appendInferredCommunications(parent, collaboration, child, record);
213
+ }
214
+ }
215
+
216
+ function updateMetrics(collaboration) {
217
+ const spawns = collaboration.spawns;
218
+ collaboration.metrics = {
219
+ cumulativeCreated: spawns.length,
220
+ simultaneousCapacity: Number(collaboration.capacity && collaboration.capacity.subagents || 0),
221
+ currentlyRunning: spawns.filter(record => record.status === 'running').length,
222
+ completedRecords: spawns.filter(record => record.status === 'completed').length,
223
+ retainedCount: collaboration.retainedObserved ? (collaboration.retainedAgents || []).length : null,
224
+ capacitySource: collaboration.capacity && collaboration.capacity.source || 'unknown',
225
+ cumulativeSource: spawns.some(record => !record.inferred) ? 'spawn-events' : 'child-sessions',
226
+ };
227
+ }
228
+
229
+ return function attachHierarchy(sessions) {
230
+ const byId = indexExistingRelationships(sessions);
231
+ const parents = sessions.filter(session => session.collaboration
232
+ && ((Array.isArray(session.collaboration.spawns) && session.collaboration.spawns.length)
233
+ || (Array.isArray(session.childIds) && session.childIds.length)));
234
+
235
+ for (const parent of parents) {
236
+ const collaboration = parent.collaboration;
237
+ const retainedByTask = new Map((collaboration.retainedAgents || []).map(agent => [agent.taskName, agent]));
238
+ for (const record of collaboration.spawns) {
239
+ const child = resolveChild(sessions, byId, parent, record);
240
+ synchronizeSpawn(parent, collaboration, retainedByTask, record, child);
241
+ }
242
+ inferUnrecordedChildren(parent, collaboration, byId);
243
+ updateMetrics(collaboration);
244
+ }
245
+ };
246
+ }
247
+
248
+ module.exports = { createHierarchyAttacher };
@@ -0,0 +1,86 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const MAX_FILES_PER_PROVIDER = 80;
7
+ const MAX_JSONL_BYTES = 12 * 1024 * 1024;
8
+
9
+ function safeStat(file) {
10
+ try { return fs.statSync(file); } catch (_missingOrUnreadableFile) { return null; }
11
+ }
12
+
13
+ function readJson(file, fallback = null) {
14
+ try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch (_missingOrPartialJson) { return fallback; }
15
+ }
16
+
17
+ function parseJsonText(text) {
18
+ try { return JSON.parse(text); } catch (_plainTextPayload) { return null; }
19
+ }
20
+
21
+ function readJsonLines(file, maxBytes = MAX_JSONL_BYTES) {
22
+ const stat = safeStat(file);
23
+ if (!stat || !stat.isFile()) return { rows: [], truncated: false };
24
+ const start = Math.max(0, stat.size - maxBytes);
25
+ const length = stat.size - start;
26
+ const fd = fs.openSync(file, 'r');
27
+ const buffer = Buffer.alloc(length);
28
+ let headerLine = '';
29
+ try {
30
+ fs.readSync(fd, buffer, 0, length, start);
31
+ if (start > 0) {
32
+ const headLength = Math.min(stat.size, 2 * 1024 * 1024);
33
+ const head = Buffer.alloc(headLength);
34
+ fs.readSync(fd, head, 0, headLength, 0);
35
+ const newline = head.indexOf(10);
36
+ if (newline >= 0) {
37
+ headerLine = head.subarray(0, newline).toString('utf8').replace(/\r$/, '');
38
+ }
39
+ }
40
+ } finally {
41
+ fs.closeSync(fd);
42
+ }
43
+ let text = buffer.toString('utf8');
44
+ if (start > 0) {
45
+ const newline = text.indexOf('\n');
46
+ text = newline >= 0 ? text.slice(newline + 1) : '';
47
+ }
48
+ const rows = [];
49
+ for (const line of text.split(/\r?\n/)) {
50
+ if (!line.trim()) continue;
51
+ const row = parseJsonText(line);
52
+ if (row) rows.push(row);
53
+ }
54
+ if (headerLine) {
55
+ const header = parseJsonText(headerLine);
56
+ if (header && header.type === 'session_meta') rows.unshift(header);
57
+ }
58
+ return { rows, truncated: start > 0 };
59
+ }
60
+
61
+ function walkRecent(root, predicate, max = MAX_FILES_PER_PROVIDER, maxDepth = 6) {
62
+ if (!root || !fs.existsSync(root)) return [];
63
+ const out = [];
64
+ const stack = [{ dir: root, depth: 0 }];
65
+ while (stack.length) {
66
+ const { dir, depth } = stack.pop();
67
+ let entries = [];
68
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_unreadableDirectory) { continue; }
69
+ for (const entry of entries) {
70
+ const full = path.join(dir, entry.name);
71
+ if (entry.isDirectory() && depth < maxDepth) stack.push({ dir: full, depth: depth + 1 });
72
+ if (!entry.isFile() || !predicate(full, entry.name)) continue;
73
+ const stat = safeStat(full);
74
+ if (stat) out.push({ file: full, mtimeMs: stat.mtimeMs, size: stat.size });
75
+ }
76
+ }
77
+ return out.sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, max);
78
+ }
79
+
80
+ module.exports = {
81
+ MAX_FILES_PER_PROVIDER,
82
+ readJson,
83
+ readJsonLines,
84
+ safeStat,
85
+ walkRecent,
86
+ };