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,95 @@
1
+ 'use strict';
2
+
3
+ function createCodexCollaboration(session, dependencies) {
4
+ const { compactText, timestamp } = dependencies;
5
+ const calls = new Map();
6
+ const spawns = new Map();
7
+ const communications = [];
8
+
9
+ function ensureSpawn(callId) {
10
+ const key = String(callId || `spawn:${spawns.size}`);
11
+ if (!spawns.has(key)) {
12
+ spawns.set(key, {
13
+ callId: key,
14
+ taskName: '',
15
+ agentPath: '',
16
+ childId: '',
17
+ assignment: '',
18
+ assignmentObserved: false,
19
+ assignmentProtected: false,
20
+ assignmentSource: 'unavailable',
21
+ sharedGoal: '',
22
+ status: 'requested',
23
+ startedAt: null,
24
+ completedAt: null,
25
+ result: '',
26
+ agentName: '',
27
+ currentlyRetained: false,
28
+ });
29
+ }
30
+ return spawns.get(key);
31
+ }
32
+
33
+ function addCommunication(event) {
34
+ const row = {
35
+ id: String(event.id || `communication:${communications.length}`),
36
+ kind: event.kind || 'message',
37
+ label: compactText(event.label || '메시지', 100),
38
+ from: compactText(event.from, 180),
39
+ to: compactText(event.to, 180),
40
+ taskName: compactText(event.taskName, 180),
41
+ childId: compactText(event.childId, 180),
42
+ text: compactText(event.text, 6000),
43
+ protected: Boolean(event.protected),
44
+ assignmentSource: compactText(event.assignmentSource, 80),
45
+ timestamp: timestamp(event.timestamp, session.updatedAt),
46
+ };
47
+ if (!communications.some(item => item.id === row.id)) communications.push(row);
48
+ return row;
49
+ }
50
+
51
+ function findSpawn({ childId, agentPath, taskName } = {}) {
52
+ return [...spawns.values()].find(record => (childId && record.childId === childId)
53
+ || (agentPath && record.agentPath === agentPath)
54
+ || (taskName && record.taskName === taskName));
55
+ }
56
+
57
+ function linkCommunicationChildIds() {
58
+ for (const record of spawns.values()) {
59
+ for (const communication of communications) {
60
+ if (communication.childId) continue;
61
+ if ((record.agentPath && (communication.from === record.agentPath || communication.to === record.agentPath))
62
+ || (record.taskName && communication.taskName === record.taskName)) {
63
+ communication.childId = record.childId;
64
+ }
65
+ }
66
+ }
67
+ }
68
+
69
+ function finalize(retainedAgents) {
70
+ const retainedByTask = new Map((retainedAgents || []).map(agent => [agent.taskName, agent]));
71
+ for (const record of spawns.values()) {
72
+ const retained = retainedByTask.get(record.taskName);
73
+ if (!retained) continue;
74
+ record.currentlyRetained = true;
75
+ record.agentName = retained.name || record.agentName;
76
+ }
77
+ linkCommunicationChildIds();
78
+ return {
79
+ spawns: [...spawns.values()].sort((a, b) => Date.parse(a.startedAt || 0) - Date.parse(b.startedAt || 0)),
80
+ communications: communications.slice(-240),
81
+ };
82
+ }
83
+
84
+ return {
85
+ calls,
86
+ spawns,
87
+ communications,
88
+ ensureSpawn,
89
+ addCommunication,
90
+ findSpawn,
91
+ finalize,
92
+ };
93
+ }
94
+
95
+ module.exports = { createCodexCollaboration };
@@ -0,0 +1,447 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const { createCodexCollaboration } = require('./codexCollaboration');
5
+
6
+ const COLLABORATION_TOOLS = new Set([
7
+ 'spawn_agent',
8
+ 'followup_task',
9
+ 'send_message',
10
+ 'interrupt_agent',
11
+ 'wait_agent',
12
+ 'list_agents',
13
+ ]);
14
+
15
+ function createCodexParser(dependencies) {
16
+ const {
17
+ thresholds: { ACTIVE_THRESHOLD_MS, STALE_TURN_THRESHOLD_MS },
18
+ sessionOps: {
19
+ addCodexMessage,
20
+ addLifecycle,
21
+ addMessage,
22
+ baseSession,
23
+ settleLifecycle,
24
+ settleRunningLifecycle,
25
+ trimSession,
26
+ },
27
+ textOps: {
28
+ agentEnvelope,
29
+ codexContentText,
30
+ codexVisibleUserText,
31
+ compactText,
32
+ encryptedCollaborationText,
33
+ jsonObject,
34
+ },
35
+ collaborationOps: {
36
+ collaborationCapacity,
37
+ collaborationTaskName,
38
+ retainedAgentsFromValue,
39
+ retainedAgentsFromWorldState,
40
+ },
41
+ usageOps: { codexUsage, contextInfo, modelContextWindow },
42
+ storageOps: { readJsonLines },
43
+ timeOps: { timestamp },
44
+ } = dependencies;
45
+
46
+ function initializeSession(fileInfo, parsed) {
47
+ const metaRow = parsed.rows.find(row => row.type === 'session_meta');
48
+ const meta = (metaRow && metaRow.payload) || {};
49
+ const externalId = meta.id
50
+ || meta.session_id
51
+ || path.basename(fileInfo.file, '.jsonl').split('-').slice(-5).join('-');
52
+ const session = baseSession('codex', externalId, fileInfo.file, fileInfo);
53
+ session.truncated = parsed.truncated;
54
+ session.cwd = meta.cwd || '';
55
+ session.model = meta.model || '';
56
+ session.branch = meta.git && meta.git.branch || '';
57
+ session.startedAt = timestamp(meta.timestamp || (metaRow && metaRow.timestamp), session.updatedAt);
58
+
59
+ if (/codex desktop/i.test(String(meta.originator || ''))) {
60
+ session.clientKind = 'codex-desktop';
61
+ session.sourceLabel = 'Codex 데스크톱 앱';
62
+ } else if (/vscode|ide/i.test(String(meta.source || ''))) {
63
+ session.clientKind = 'codex-ide';
64
+ session.sourceLabel = 'Codex IDE';
65
+ } else {
66
+ session.clientKind = 'codex-cli';
67
+ }
68
+
69
+ const spawn = meta.source && meta.source.subagent && meta.source.subagent.thread_spawn;
70
+ if (spawn && spawn.parent_thread_id) {
71
+ session.parentId = `codex:${spawn.parent_thread_id}`;
72
+ session.depth = Number(spawn.depth || 1);
73
+ session.agentName = spawn.agent_nickname || spawn.agent_role || 'subagent';
74
+ session.agentRole = spawn.agent_role || '';
75
+ session.agentPath = spawn.agent_path || meta.agent_path || '';
76
+ session.taskName = collaborationTaskName(session.agentPath);
77
+ }
78
+ return { session, meta };
79
+ }
80
+
81
+ function createParseState(session, meta) {
82
+ return {
83
+ latestUser: '',
84
+ latestInternalGoal: '',
85
+ latestDelegationNarration: '',
86
+ activeTurn: false,
87
+ lastTurnCompleted: false,
88
+ lastFinalAnswer: '',
89
+ latestTs: session.updatedAt,
90
+ observedWindow: Number(meta.context_window || 0),
91
+ messageObservations: new Map(),
92
+ collaboration: createCodexCollaboration(session, { compactText, timestamp }),
93
+ sessionStartedMs: Date.parse(session.startedAt || '') || 0,
94
+ };
95
+ }
96
+
97
+ function recordSubagentActivity(session, state, payload, row, timing) {
98
+ const activityMs = Date.parse(timestamp(payload.occurred_at_ms || row.timestamp, session.startedAt)) || timing.rowMs;
99
+ if (!timing.ownCollaborationRow || (session.depth && state.sessionStartedMs && activityMs < state.sessionStartedMs)) return;
100
+ if (session.depth && session.agentPath) {
101
+ const activityPath = String(payload.agent_path || '').replace(/\/$/, '');
102
+ const nestedPrefix = `${String(session.agentPath).replace(/\/$/, '')}/`;
103
+ if (!activityPath.startsWith(nestedPrefix)) return;
104
+ }
105
+
106
+ const childId = payload.agent_thread_id ? `codex:${payload.agent_thread_id}` : '';
107
+ const agentPath = payload.agent_path || '';
108
+ let record = state.collaboration.findSpawn({ childId, agentPath });
109
+ if (!record) {
110
+ const fallbackId = `activity:${payload.agent_thread_id || payload.agent_path || payload.event_id}`;
111
+ record = state.collaboration.ensureSpawn(payload.kind === 'started'
112
+ ? (payload.event_id || payload.agent_thread_id)
113
+ : fallbackId);
114
+ }
115
+ record.childId = childId || record.childId;
116
+ record.agentPath = payload.agent_path || record.agentPath;
117
+ record.taskName = record.taskName || collaborationTaskName(record.agentPath);
118
+ record.status = payload.kind === 'started' ? 'running' : (payload.kind || record.status);
119
+ if (payload.kind === 'started' || !record.startedAt) {
120
+ record.startedAt = timestamp(payload.occurred_at_ms || row.timestamp, record.startedAt || session.updatedAt);
121
+ }
122
+ if (payload.kind === 'completed' || payload.kind === 'interrupted') {
123
+ record.completedAt = timestamp(payload.occurred_at_ms || row.timestamp, record.completedAt || session.updatedAt);
124
+ }
125
+ state.collaboration.addCommunication({
126
+ id: `activity:${payload.event_id || payload.agent_thread_id}:${payload.kind}`,
127
+ kind: payload.kind === 'started' ? 'started' : 'status',
128
+ label: payload.kind === 'started' ? '서브에이전트 실행 시작' : '서브에이전트 상태 변경',
129
+ from: 'Codex 런타임',
130
+ to: record.agentPath,
131
+ taskName: record.taskName,
132
+ childId: record.childId,
133
+ text: payload.kind || '',
134
+ timestamp: payload.occurred_at_ms || row.timestamp,
135
+ });
136
+ }
137
+
138
+ function processEventMessage(session, state, row, payload, timing) {
139
+ if (payload.type === 'task_started') {
140
+ state.activeTurn = true;
141
+ state.lastTurnCompleted = false;
142
+ state.latestDelegationNarration = '';
143
+ addLifecycle(session, {
144
+ id: payload.turn_id,
145
+ type: 'turn-start',
146
+ label: '턴 시작',
147
+ status: 'running',
148
+ timestamp: payload.started_at || row.timestamp,
149
+ });
150
+ } else if (payload.type === 'task_complete') {
151
+ state.activeTurn = false;
152
+ state.lastTurnCompleted = true;
153
+ settleRunningLifecycle(session, payload.completed_at || row.timestamp);
154
+ session.completedAt = timestamp(payload.completed_at || row.timestamp, session.updatedAt);
155
+ session.completionObserved = true;
156
+ if (payload.last_agent_message) state.lastFinalAnswer = compactText(payload.last_agent_message, 6000);
157
+ addLifecycle(session, {
158
+ id: payload.turn_id,
159
+ type: 'turn-complete',
160
+ label: '턴 완료',
161
+ detail: payload.duration_ms ? `${payload.duration_ms} ms` : '',
162
+ status: 'done',
163
+ timestamp: payload.completed_at || row.timestamp,
164
+ });
165
+ } else if (payload.type === 'sub_agent_activity') {
166
+ recordSubagentActivity(session, state, payload, row, timing);
167
+ } else if (payload.type === 'user_message') {
168
+ const rawUser = compactText(payload.message || payload.text_elements, 12000);
169
+ const text = codexVisibleUserText(rawUser);
170
+ if (!text) return;
171
+ if (/<codex_internal_context(?:\s|>)/i.test(rawUser)) {
172
+ state.latestInternalGoal = text;
173
+ return;
174
+ }
175
+ state.latestUser = text;
176
+ const key = `u:${payload.client_id || row.timestamp}:${text}`;
177
+ addCodexMessage(session, state.messageObservations, { id: key, role: 'user', text, timestamp: row.timestamp }, 'event');
178
+ } else if (payload.type === 'agent_message') {
179
+ const text = compactText(payload.message);
180
+ if (text) state.latestDelegationNarration = text;
181
+ if (payload.phase === 'final_answer') state.lastFinalAnswer = text;
182
+ const key = `a:${row.timestamp}:${text}`;
183
+ addCodexMessage(session, state.messageObservations, { id: key, role: 'assistant', text, timestamp: row.timestamp }, 'event');
184
+ } else if (payload.type === 'agent_reasoning') {
185
+ addLifecycle(session, {
186
+ id: `r:${row.timestamp}`,
187
+ type: 'reasoning',
188
+ label: '판단 중',
189
+ detail: '다음 작업을 판단하고 결과를 정리하는 중',
190
+ status: 'running',
191
+ timestamp: row.timestamp,
192
+ });
193
+ } else if (payload.type === 'token_count' && payload.info) {
194
+ session.usage = codexUsage(payload.info.total_token_usage);
195
+ session.turnUsage = codexUsage(payload.info.last_token_usage);
196
+ state.observedWindow = Number(payload.info.model_context_window || state.observedWindow);
197
+ } else if (/failed|error/.test(String(payload.type || ''))) {
198
+ session.status = 'failed';
199
+ session.statusDetail = compactText(payload.message || payload.error || payload.type, 240);
200
+ state.activeTurn = false;
201
+ }
202
+ }
203
+
204
+ function recordCollaborationToolCall(session, state, row, name, callId, args) {
205
+ const rawMessage = compactText(args.message, 6000);
206
+ const messageProtected = encryptedCollaborationText(rawMessage);
207
+ if (name === 'spawn_agent') {
208
+ const record = state.collaboration.ensureSpawn(callId);
209
+ record.taskName = compactText(args.task_name, 180);
210
+ record.agentPath = record.taskName
211
+ ? `${session.agentPath || '/root'}/${record.taskName}`.replace(/\/+/g, '/')
212
+ : record.agentPath;
213
+ record.assignmentProtected = messageProtected;
214
+ const directAssignment = rawMessage && !messageProtected ? rawMessage : '';
215
+ record.assignment = directAssignment || (messageProtected ? state.latestDelegationNarration : '');
216
+ record.assignmentObserved = Boolean(record.assignment);
217
+ record.assignmentSource = directAssignment ? 'spawn-message' : (record.assignment ? 'parent-narration' : 'unavailable');
218
+ record.sharedGoal = compactText(state.latestUser, 6000);
219
+ record.startedAt = timestamp(row.timestamp, record.startedAt || session.updatedAt);
220
+ state.collaboration.addCommunication({
221
+ id: `assign:${callId}`,
222
+ kind: 'assignment',
223
+ label: record.assignmentSource === 'parent-narration' ? '메인 AI 작업 지시' : '새 작업 배정',
224
+ from: session.agentPath || '/root',
225
+ to: record.agentPath,
226
+ taskName: record.taskName,
227
+ text: record.assignment,
228
+ protected: record.assignmentProtected && !record.assignment,
229
+ assignmentSource: record.assignmentSource,
230
+ timestamp: row.timestamp,
231
+ });
232
+ } else if (name === 'send_message' || name === 'followup_task') {
233
+ const target = compactText(args.target, 180);
234
+ state.collaboration.addCommunication({
235
+ id: `${name}:${callId}`,
236
+ kind: name === 'followup_task' ? 'followup' : 'message',
237
+ label: name === 'followup_task' ? '추가 작업 지시' : '메시지 전달',
238
+ from: session.agentPath || '/root',
239
+ to: target,
240
+ taskName: collaborationTaskName(target),
241
+ text: messageProtected ? '' : rawMessage,
242
+ protected: messageProtected,
243
+ timestamp: row.timestamp,
244
+ });
245
+ } else if (name === 'interrupt_agent') {
246
+ const target = compactText(args.target, 180);
247
+ state.collaboration.addCommunication({
248
+ id: `interrupt:${callId}`,
249
+ kind: 'interrupt',
250
+ label: '작업 중단 요청',
251
+ from: session.agentPath || '/root',
252
+ to: target,
253
+ taskName: collaborationTaskName(target),
254
+ timestamp: row.timestamp,
255
+ });
256
+ }
257
+ return messageProtected;
258
+ }
259
+
260
+ function processToolCall(session, state, row, payload, timing) {
261
+ const name = payload.name || 'tool';
262
+ const callId = payload.call_id || payload.id;
263
+ const args = jsonObject(payload.arguments || payload.input);
264
+ const collaborationTool = payload.namespace === 'collaboration' || COLLABORATION_TOOLS.has(name);
265
+ if (collaborationTool && !timing.ownCollaborationRow) return;
266
+ state.collaboration.calls.set(String(callId || ''), { name, args, timestamp: row.timestamp });
267
+
268
+ let collaborationMessageProtected = false;
269
+ if (collaborationTool) {
270
+ collaborationMessageProtected = recordCollaborationToolCall(session, state, row, name, callId, args);
271
+ }
272
+ const protectedToolText = name === 'followup_task' ? '보호된 추가 작업 지시' : '보호된 메시지 전달';
273
+ const toolText = collaborationTool
274
+ ? (name === 'spawn_agent'
275
+ ? `담당 작업: ${args.task_name || '이름 미상'}`
276
+ : (collaborationMessageProtected
277
+ ? protectedToolText
278
+ : compactText(args.message || args.target || '', 1000)))
279
+ : compactText(payload.arguments || payload.input, 1000);
280
+ addMessage(session, {
281
+ id: callId,
282
+ role: 'tool',
283
+ type: 'tool',
284
+ title: name,
285
+ text: toolText,
286
+ status: payload.status || 'started',
287
+ timestamp: row.timestamp,
288
+ });
289
+ addLifecycle(session, {
290
+ id: callId,
291
+ type: collaborationTool ? 'collaboration' : 'tool',
292
+ label: name,
293
+ detail: toolText,
294
+ status: payload.status === 'completed' ? 'done' : 'running',
295
+ timestamp: row.timestamp,
296
+ });
297
+ }
298
+
299
+ function processToolOutput(session, state, row, payload) {
300
+ settleLifecycle(session, payload.call_id, 'done', row.timestamp);
301
+ const call = state.collaboration.calls.get(String(payload.call_id || ''));
302
+ const output = jsonObject(payload.output);
303
+ if (call && call.name === 'spawn_agent') {
304
+ const record = state.collaboration.ensureSpawn(payload.call_id);
305
+ record.agentPath = compactText(output.task_name, 180) || record.agentPath;
306
+ record.taskName = record.taskName || collaborationTaskName(record.agentPath);
307
+ }
308
+ if (call && call.name === 'list_agents') {
309
+ session.collaboration.retainedAgents = retainedAgentsFromValue(payload.output)
310
+ .map(agent => ({ ...agent, observedAt: timestamp(row.timestamp, session.updatedAt) }));
311
+ session.collaboration.retainedObserved = true;
312
+ }
313
+ addLifecycle(session, { id: `out:${payload.call_id}`, type: 'tool-result', label: '도구 완료', status: 'done', timestamp: row.timestamp });
314
+ }
315
+
316
+ function processAgentMessage(session, state, row, payload, timing) {
317
+ if (!timing.ownCollaborationRow) return;
318
+ const rawText = codexContentText(payload.content);
319
+ const envelope = agentEnvelope(rawText);
320
+ const from = compactText(payload.author || envelope.sender, 180);
321
+ const to = compactText(payload.recipient || envelope.task, 180);
322
+ const taskName = collaborationTaskName(from === (session.agentPath || '/root') ? to : from);
323
+ const kind = envelope.type === 'FINAL_ANSWER' ? 'result' : (envelope.type === 'NEW_TASK' ? 'assignment' : 'message');
324
+ const text = envelope.payload || (envelope.type ? '' : rawText);
325
+ const communication = state.collaboration.addCommunication({
326
+ id: payload.id || `agent-message:${row.timestamp}:${from}:${to}`,
327
+ kind,
328
+ label: kind === 'result' ? '결과 반환' : (kind === 'assignment' ? '작업 전달' : '에이전트 메시지'),
329
+ from,
330
+ to,
331
+ taskName,
332
+ text,
333
+ protected: !text && /encrypted_content/.test(JSON.stringify(payload.content || [])),
334
+ timestamp: row.timestamp,
335
+ });
336
+ if (kind !== 'result') return;
337
+ const record = [...state.collaboration.spawns.values()]
338
+ .find(item => item.agentPath === from || item.taskName === taskName);
339
+ if (!record) return;
340
+ record.status = 'completed';
341
+ record.completedAt = timestamp(row.timestamp, record.completedAt || session.updatedAt);
342
+ record.result = text;
343
+ communication.childId = record.childId;
344
+ }
345
+
346
+ function processConversationMessage(session, state, row, payload) {
347
+ if (payload.role === 'developer') {
348
+ const capacity = collaborationCapacity(codexContentText(payload.content));
349
+ if (capacity) session.collaboration.capacity = capacity;
350
+ return;
351
+ }
352
+ if (payload.role !== 'assistant' && payload.role !== 'user') return;
353
+ const role = payload.role;
354
+ const rawText = codexContentText(payload.content);
355
+ const text = role === 'user' ? codexVisibleUserText(rawText) : rawText;
356
+ if (!text) return;
357
+ if (role === 'user' && /<codex_internal_context(?:\s|>)/i.test(rawText)) {
358
+ state.latestInternalGoal = text;
359
+ return;
360
+ }
361
+ if (role === 'user') state.latestUser = text;
362
+ if (role === 'assistant') state.latestDelegationNarration = text;
363
+ const key = payload.id || `message:${role}:${row.timestamp}:${text.slice(0, 80)}`;
364
+ addCodexMessage(session, state.messageObservations, { id: key, role, text, timestamp: row.timestamp }, 'response');
365
+ }
366
+
367
+ function processResponseItem(session, state, row, payload, timing) {
368
+ if (payload.type === 'function_call' || payload.type === 'custom_tool_call') {
369
+ processToolCall(session, state, row, payload, timing);
370
+ } else if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
371
+ processToolOutput(session, state, row, payload);
372
+ } else if (payload.type === 'agent_message') {
373
+ processAgentMessage(session, state, row, payload, timing);
374
+ } else if (payload.type === 'message') {
375
+ processConversationMessage(session, state, row, payload);
376
+ }
377
+ }
378
+
379
+ function processRow(session, state, row) {
380
+ state.latestTs = timestamp(row.timestamp, state.latestTs);
381
+ const payload = row.payload || {};
382
+ const rowMs = Date.parse(timestamp(row.timestamp, session.startedAt)) || 0;
383
+ const timing = {
384
+ rowMs,
385
+ ownCollaborationRow: !session.depth || !state.sessionStartedMs || !rowMs || rowMs >= state.sessionStartedMs,
386
+ };
387
+ if (row.type === 'turn_context') {
388
+ session.model = payload.model || session.model;
389
+ session.cwd = payload.cwd || session.cwd;
390
+ } else if (row.type === 'world_state') {
391
+ const retainedText = payload.state && payload.state.environments && payload.state.environments.subagents;
392
+ const retained = retainedAgentsFromWorldState(retainedText, row.timestamp);
393
+ if (retainedText !== undefined) {
394
+ session.collaboration.retainedAgents = retained;
395
+ session.collaboration.retainedObserved = true;
396
+ }
397
+ } else if (row.type === 'event_msg') {
398
+ processEventMessage(session, state, row, payload, timing);
399
+ } else if (row.type === 'response_item') {
400
+ processResponseItem(session, state, row, payload, timing);
401
+ }
402
+ }
403
+
404
+ function finalizeSession(session, state, fileInfo) {
405
+ session.updatedAt = state.latestTs;
406
+ session.sharedGoal = session.depth ? compactText(state.latestUser, 6000) : '';
407
+ session.title = session.depth
408
+ ? (session.taskName || `${session.agentName} 서브에이전트`)
409
+ : (compactText(state.latestUser || state.latestInternalGoal, 180) || 'GPT 작업 세션');
410
+ session.result = state.lastFinalAnswer;
411
+ const collaboration = state.collaboration.finalize(session.collaboration.retainedAgents);
412
+ session.collaboration.spawns = collaboration.spawns;
413
+ session.collaboration.communications = collaboration.communications;
414
+ const windowInfo = modelContextWindow('codex', session.model, state.observedWindow);
415
+ session.context = contextInfo(session.turnUsage.total || session.turnUsage.input, windowInfo);
416
+
417
+ if (session.status !== 'failed') {
418
+ const turnAge = Date.now() - fileInfo.mtimeMs;
419
+ if (state.activeTurn && turnAge < STALE_TURN_THRESHOLD_MS) {
420
+ session.status = 'running';
421
+ session.statusDetail = '턴 실행 중';
422
+ session.statusObserved = true;
423
+ } else if (session.depth && state.lastTurnCompleted) {
424
+ session.status = 'completed';
425
+ session.statusDetail = '작업 완료';
426
+ session.statusObserved = false;
427
+ } else {
428
+ session.status = 'idle';
429
+ session.statusDetail = state.activeTurn ? '마지막 턴 기록이 종료됨' : '다음 요청 대기';
430
+ session.statusObserved = Date.now() - fileInfo.mtimeMs < ACTIVE_THRESHOLD_MS;
431
+ }
432
+ }
433
+ trimSession(session);
434
+ return session;
435
+ }
436
+
437
+ return function parseCodex(fileInfo) {
438
+ const parsed = readJsonLines(fileInfo.file);
439
+ if (!parsed.rows.length) return null;
440
+ const { session, meta } = initializeSession(fileInfo, parsed);
441
+ const state = createParseState(session, meta);
442
+ for (const row of parsed.rows) processRow(session, state, row);
443
+ return finalizeSession(session, state, fileInfo);
444
+ };
445
+ }
446
+
447
+ module.exports = { createCodexParser };