loadtoagent 1.3.5 → 1.3.6

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.
@@ -474,14 +474,7 @@ body :is(input, textarea)::placeholder {
474
474
  }
475
475
 
476
476
  .terminal-session-row {
477
- grid-template-columns: minmax(0, 1fr) 44px;
478
- gap: 7px;
479
- }
480
-
481
- .terminal-reorder-actions button {
482
- min-width: 44px;
483
- min-height: 44px;
484
- font-size: 16px;
477
+ grid-template-columns: minmax(0, 1fr);
485
478
  }
486
479
 
487
480
  .terminal-font-controls {
@@ -914,7 +907,6 @@ body :is(
914
907
  .terminal-workspace-actions,
915
908
  .terminal-key-actions,
916
909
  .terminal-tmux-tools,
917
- .terminal-reorder-actions,
918
910
  .tmux-section-actions,
919
911
  .tmux-pane-actions,
920
912
  .run-modal-actions,
@@ -399,10 +399,17 @@
399
399
  }
400
400
 
401
401
  .chat-progress-updates,
402
- .chat-turn-waiting {
402
+ .chat-turn-waiting,
403
+ .subagent-call-moment,
404
+ .subagent-call-event {
403
405
  margin-left: 0;
404
406
  }
405
407
 
408
+ .subagent-call-moment,
409
+ .subagent-call-event {
410
+ width: 100%;
411
+ }
412
+
406
413
  .json-object>div {
407
414
  grid-template-columns: 1fr;
408
415
  }
@@ -475,6 +482,20 @@
475
482
  border-radius: 7px;
476
483
  }
477
484
 
485
+ .subagent-call-event {
486
+ grid-template-columns: 32px minmax(0,1fr);
487
+ }
488
+
489
+ .subagent-call-anchor {
490
+ grid-template-columns: 26px minmax(0,1fr);
491
+ padding: 9px 10px;
492
+ }
493
+
494
+ .subagent-call-action {
495
+ grid-column: 2;
496
+ justify-items: start;
497
+ }
498
+
478
499
  .main-stage {
479
500
  padding-right: 11px;
480
501
  padding-left: 11px;
@@ -421,35 +421,8 @@
421
421
 
422
422
  .terminal-session-row {
423
423
  display: grid;
424
- grid-template-columns: minmax(0,1fr) 26px;
424
+ grid-template-columns: minmax(0,1fr);
425
425
  align-items: stretch;
426
- gap: 4px;
427
- }
428
-
429
- .terminal-reorder-actions {
430
- display: grid;
431
- gap: 4px;
432
- }
433
-
434
- .terminal-reorder-actions button {
435
- min-width: 26px;
436
- padding: 0;
437
- border: 1px solid #243946;
438
- border-radius: 7px;
439
- background: #101a24;
440
- color: #7f91a3;
441
- cursor: pointer;
442
- }
443
-
444
- .terminal-reorder-actions button:hover:not(:disabled),
445
- .terminal-reorder-actions button:focus-visible {
446
- border-color: #397766;
447
- color: #70e6bd;
448
- }
449
-
450
- .terminal-reorder-actions button:disabled {
451
- opacity: .3;
452
- cursor: default;
453
426
  }
454
427
 
455
428
  .terminal-session-item:hover {
@@ -1395,7 +1368,7 @@
1395
1368
  cursor: grab;
1396
1369
  position: relative;
1397
1370
  display: grid;
1398
- grid-template-columns: 38px minmax(0,1fr) 8px;
1371
+ grid-template-columns: 12px 38px minmax(0,1fr) auto;
1399
1372
  gap: 10px;
1400
1373
  min-height: 72px;
1401
1374
  padding: 10px 9px;
@@ -1406,6 +1379,23 @@
1406
1379
  transition: transform var(--motion-fast) var(--motion-ease),background-color var(--motion-fast) ease,border-color var(--motion-fast) ease,color var(--motion-fast) ease,box-shadow var(--motion-medium) ease,opacity var(--motion-fast) ease;
1407
1380
  }
1408
1381
 
1382
+ .terminal-session-drag-handle {
1383
+ width: 3px;
1384
+ height: 3px;
1385
+ justify-self: center;
1386
+ border-radius: 50%;
1387
+ background: #607281;
1388
+ box-shadow: 0 -5px #607281,0 5px #607281;
1389
+ transition: background-color var(--motion-fast) ease,box-shadow var(--motion-fast) ease;
1390
+ }
1391
+
1392
+ .terminal-session-item:hover .terminal-session-drag-handle,
1393
+ .terminal-session-item:focus-visible .terminal-session-drag-handle,
1394
+ .terminal-session-item.dragging .terminal-session-drag-handle {
1395
+ background: #68dcb6;
1396
+ box-shadow: 0 -5px #68dcb6,0 5px #68dcb6;
1397
+ }
1398
+
1409
1399
  .terminal-session-item.dragging {
1410
1400
  opacity: .45;
1411
1401
  cursor: grabbing;
@@ -1468,10 +1458,10 @@ body[data-current-view="terminal"] .topbar h1 {
1468
1458
  .terminal-resource-head>div span { margin-top: 5px; color: #93a6b2; font-size: 12px; }
1469
1459
  .terminal-resource-live { font-size: 12px; }
1470
1460
  .terminal-session-list { gap: 9px; padding: 12px; }
1471
- .terminal-session-row { grid-template-columns: minmax(0,1fr) 28px; gap: 5px; }
1461
+ .terminal-session-row { grid-template-columns: minmax(0,1fr); }
1472
1462
 
1473
1463
  .terminal-session-item {
1474
- grid-template-columns: 42px minmax(0,1fr) auto;
1464
+ grid-template-columns: 12px 42px minmax(0,1fr) auto;
1475
1465
  gap: 11px;
1476
1466
  min-height: 86px;
1477
1467
  padding: 11px 10px;
@@ -610,6 +610,10 @@
610
610
  border-color: rgba(230,104,112,.4);
611
611
  }
612
612
 
613
+ .execution-activity-card.unverified {
614
+ border-color: rgba(229,184,95,.34);
615
+ }
616
+
613
617
  .execution-activity-icon {
614
618
  width: 36px;
615
619
  height: 36px;
@@ -716,6 +720,10 @@
716
720
  background: #e26972;
717
721
  }
718
722
 
723
+ .execution-activity-card.unverified .execution-activity-state>span:first-child i {
724
+ background: #d6b36c;
725
+ }
726
+
719
727
  .execution-activity-state b {
720
728
  color: #c6d2d7;
721
729
  font-size: 12px;
@@ -93,19 +93,6 @@ window.LoadToAgentTerminalEvents = function bindTerminalEvents(context) {
93
93
  });
94
94
  };
95
95
  sessionList.addEventListener('click', event => {
96
- const move = event.target.closest('[data-session-move][data-session-move-id]');
97
- if (move) {
98
- const changed = moveSessionByOffset(move.dataset.sessionMoveId, Number(move.dataset.sessionMove));
99
- if (!changed) return;
100
- renderAll();
101
- requestAnimationFrame(() => {
102
- const next = sessionList.querySelector(`[data-session-move-id="${CSS.escape(move.dataset.sessionMoveId)}"][data-session-move="${CSS.escape(move.dataset.sessionMove)}"]`);
103
- if (next && !next.disabled) next.focus();
104
- else sessionList.querySelector(`[data-terminal-id="${CSS.escape(move.dataset.sessionMoveId)}"]`)?.focus();
105
- });
106
- notice(window.LoadToAgentI18n.t('terminal.reordered'), 'success');
107
- return;
108
- }
109
96
  if (state.sessionDragJustEnded) return;
110
97
  const item = event.target.closest('[data-terminal-id]');
111
98
  if (item) selectSession(item.dataset.terminalId);
@@ -178,14 +178,11 @@ window.LoadToAgentTerminalWorkbench = function createModule(context) {
178
178
  aria-grabbed="false"
179
179
  aria-describedby="terminalReorderHelp"
180
180
  title="${esc(session.cwd || window.LoadToAgentI18n.t('terminal.reorder_hint'))}">
181
+ <span class="terminal-session-drag-handle" aria-hidden="true"></span>
181
182
  <span class="terminal-session-icon">${esc(terminalTypeMark(session))}</span>
182
183
  <span><b>${esc(session.title)}</b><small>${esc(terminalTypeLabel(session))}${session.background ? ` · ${t('terminal.background_kept')}` : ''}${session.recoveredAfterHostRestart ? ` · ${t('terminal.recovered_after_host_restart')}` : ''}</small><em>${esc(session.cwd || session.distro || `PID ${session.pid || '--'}`)}</em><span class="sr-only">${index + 1}/${general.length}</span></span>
183
184
  <span class="terminal-session-status" data-status="${esc(presentation.tone)}"><i></i>${esc(presentation.label)}</span>
184
185
  </button>
185
- <span class="terminal-reorder-actions" aria-label="${esc(window.LoadToAgentI18n.t('terminal.reorder_group', { title: session.title }))}">
186
- <button type="button" data-session-move="-1" data-session-move-id="${esc(session.id)}" aria-label="${esc(window.LoadToAgentI18n.t('terminal.move_up', { title: session.title }))}" ${index === 0 ? 'disabled' : ''}>↑</button>
187
- <button type="button" data-session-move="1" data-session-move-id="${esc(session.id)}" aria-label="${esc(window.LoadToAgentI18n.t('terminal.move_down', { title: session.title }))}" ${index === general.length - 1 ? 'disabled' : ''}>↓</button>
188
- </span>
189
186
  </div>`;
190
187
  }).join('') : `<div class="terminal-resource-empty">${t('terminal.empty.general')}</div>`;
191
188
  }
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
- const { createExecutionTracker } = require('./executionActivity');
4
+ const { createExecutionTracker, reconcileExecutionActivities } = require('./executionActivity');
5
5
 
6
6
  function createClaudeParser(dependencies) {
7
7
  const {
@@ -23,6 +23,34 @@ function createClaudeParser(dependencies) {
23
23
  isUserInputTool,
24
24
  } = dependencies;
25
25
 
26
+ function taskNotification(value) {
27
+ const text = String(value || '');
28
+ if (!/<task-notification\b/i.test(text)) return null;
29
+ const field = name => {
30
+ const match = text.match(new RegExp(`<${name}>([\\s\\S]*?)<\\/${name}>`, 'i'));
31
+ return match ? match[1].trim() : '';
32
+ };
33
+ const taskId = field('task-id');
34
+ const toolUseId = field('tool-use-id');
35
+ const status = field('status').toLowerCase();
36
+ if (!taskId && !toolUseId) return null;
37
+ return { taskId, toolUseId, status, text };
38
+ }
39
+
40
+ function recordTaskNotification(state, value, at) {
41
+ const notification = taskNotification(value);
42
+ if (!notification || !/^(?:completed|failed|error|cancelled)$/.test(notification.status)) return null;
43
+ state.executionTracker.recordOutput({
44
+ name: 'bash',
45
+ callId: notification.toolUseId,
46
+ args: { task_id: notification.taskId },
47
+ output: notification.text,
48
+ at,
49
+ isError: /^(?:failed|error)$/.test(notification.status),
50
+ });
51
+ return notification;
52
+ }
53
+
26
54
  function normalizeUsage(raw = {}) {
27
55
  return finalizeUsage({
28
56
  input: raw.input_tokens,
@@ -53,6 +81,136 @@ function createClaudeParser(dependencies) {
53
81
  return raw;
54
82
  }
55
83
 
84
+ function isClaudeAgentTool(name) {
85
+ return /^(?:Agent|Task)$/i.test(String(name || ''));
86
+ }
87
+
88
+ function addClaudeCommunication(session, event) {
89
+ const communications = session.collaboration.communications;
90
+ const row = {
91
+ id: String(event.id || `claude-communication:${communications.length}`),
92
+ kind: event.kind || 'message',
93
+ label: compactText(event.label || '메시지', 100),
94
+ from: compactText(event.from, 180),
95
+ to: compactText(event.to, 180),
96
+ taskName: compactText(event.taskName, 180),
97
+ childId: compactText(event.childId, 180),
98
+ text: compactText(event.text, 6000),
99
+ protected: false,
100
+ assignmentSource: compactText(event.assignmentSource, 80),
101
+ timestamp: timestamp(event.timestamp, session.updatedAt),
102
+ };
103
+ if (!communications.some(item => item.id === row.id)) communications.push(row);
104
+ }
105
+
106
+ function findClaudeSpawn(session, callId) {
107
+ return session.collaboration.spawns.find(record => record.callId === String(callId || ''));
108
+ }
109
+
110
+ function recordClaudeAgentCall(session, state, row, callId, args) {
111
+ const prompt = compactText(args.prompt || args.message, 6000);
112
+ const taskName = compactText(args.description || args.name || args.subagent_type, 180);
113
+ const record = {
114
+ callId: String(callId || `claude-spawn:${session.collaboration.spawns.length}`),
115
+ taskName,
116
+ agentPath: '',
117
+ childId: '',
118
+ assignment: prompt,
119
+ assignmentObserved: Boolean(prompt),
120
+ assignmentProtected: false,
121
+ assignmentSource: prompt ? 'claude-agent-prompt' : 'unavailable',
122
+ assignmentContext: '',
123
+ sharedGoal: compactText(state.latestUser, 6000),
124
+ status: 'requested',
125
+ startedAt: timestamp(row.timestamp, session.updatedAt),
126
+ completedAt: null,
127
+ result: '',
128
+ agentName: compactText(args.subagent_type, 120),
129
+ currentlyRetained: false,
130
+ };
131
+ session.collaboration.spawns.push(record);
132
+ addClaudeCommunication(session, {
133
+ id: `assign:${record.callId}`,
134
+ kind: 'assignment',
135
+ label: '새 작업 배정',
136
+ from: session.agentPath || session.id,
137
+ to: record.taskName,
138
+ taskName: record.taskName,
139
+ text: record.assignment,
140
+ assignmentSource: record.assignmentSource,
141
+ timestamp: row.timestamp,
142
+ });
143
+ }
144
+
145
+ function updateClaudeSpawn(session, callId, details = {}) {
146
+ const record = findClaudeSpawn(session, callId);
147
+ if (!record) return;
148
+ const childExternalId = compactText(details.childExternalId, 180);
149
+ if (childExternalId) {
150
+ record.childId = `claude:${childExternalId}`;
151
+ record.agentPath = record.childId;
152
+ }
153
+ if (details.status) record.status = details.status;
154
+ if (details.completedAt) record.completedAt = timestamp(details.completedAt, record.completedAt || session.updatedAt);
155
+ if (details.result) record.result = compactText(details.result, 6000);
156
+ for (const communication of session.collaboration.communications) {
157
+ if (communication.taskName !== record.taskName || communication.childId) continue;
158
+ communication.childId = record.childId;
159
+ communication.to = record.agentPath || communication.to;
160
+ }
161
+ if ((record.status === 'running' || record.childId) && !session.collaboration.communications.some(item => item.id === `started:${record.callId}`)) {
162
+ addClaudeCommunication(session, {
163
+ id: `started:${record.callId}`,
164
+ kind: 'started',
165
+ label: '서브에이전트 실행 시작',
166
+ from: 'Claude 런타임',
167
+ to: record.agentPath || record.taskName,
168
+ taskName: record.taskName,
169
+ childId: record.childId,
170
+ timestamp: record.startedAt,
171
+ });
172
+ }
173
+ if (record.completedAt && !session.collaboration.communications.some(item => item.id === `result:${record.callId}`)) {
174
+ addClaudeCommunication(session, {
175
+ id: `result:${record.callId}`,
176
+ kind: 'result',
177
+ label: '결과 반환 확인',
178
+ from: record.agentPath || record.taskName,
179
+ to: session.agentPath || session.id,
180
+ taskName: record.taskName,
181
+ childId: record.childId,
182
+ text: record.result,
183
+ timestamp: record.completedAt,
184
+ });
185
+ }
186
+ }
187
+
188
+ function recordClaudeAgentToolResult(session, state, item, at) {
189
+ const call = state.toolCalls.get(String(item.tool_use_id || ''));
190
+ if (!call || !isClaudeAgentTool(call.name)) return;
191
+ const output = compactText(item.content || item, 12000);
192
+ const agentId = output.match(/\bagentId:\s*([A-Za-z0-9_-]+)/i);
193
+ const launched = /agent launched successfully|working in the background/i.test(output);
194
+ const failed = item.is_error === true;
195
+ updateClaudeSpawn(session, item.tool_use_id, {
196
+ childExternalId: agentId && agentId[1],
197
+ status: failed ? 'failed' : (launched ? 'running' : 'completed'),
198
+ completedAt: launched ? null : at,
199
+ result: launched ? '' : output,
200
+ });
201
+ }
202
+
203
+ function recordClaudeTaskCompletion(session, notification, at) {
204
+ if (!notification) return;
205
+ const resultMatch = notification.text.match(/<result>([\s\S]*?)<\/result>/i);
206
+ updateClaudeSpawn(session, notification.toolUseId, {
207
+ childExternalId: notification.taskId,
208
+ status: notification.status === 'completed' ? 'completed' : notification.status,
209
+ completedAt: at,
210
+ result: resultMatch ? resultMatch[1].trim() : '',
211
+ });
212
+ }
213
+
56
214
  function recordContent(session, state, row, item, index) {
57
215
  const kind = item && item.type;
58
216
  const id = `${row.uuid || row.requestId || session.externalId}:${index}`;
@@ -64,6 +222,7 @@ function createClaudeParser(dependencies) {
64
222
  const callId = item.id || id;
65
223
  const args = item.input && typeof item.input === 'object' ? item.input : {};
66
224
  state.toolCalls.set(String(callId), { name, args });
225
+ if (isClaudeAgentTool(name)) recordClaudeAgentCall(session, state, row, callId, args);
67
226
  state.executionTracker.recordCall({ name, callId, args, rawInput: item.input, at: row.timestamp });
68
227
  addMessage(session, {
69
228
  id,
@@ -93,6 +252,7 @@ function createClaudeParser(dependencies) {
93
252
  at: row.timestamp,
94
253
  isError: item.is_error === true,
95
254
  });
255
+ recordClaudeAgentToolResult(session, state, item, row.timestamp);
96
256
  settleLifecycle(session, item.tool_use_id, item.is_error ? 'failed' : 'done', row.timestamp);
97
257
  addLifecycle(session, {
98
258
  id: `result:${item.tool_use_id || id}`,
@@ -136,11 +296,18 @@ function createClaudeParser(dependencies) {
136
296
  function processMessageRow(session, state, row) {
137
297
  const role = row.message.role === 'assistant' ? 'assistant' : 'user';
138
298
  const internalUserRow = role === 'user' && Boolean(row.isMeta || row.sourceToolUseID);
299
+ state.lastTurnFinished = false;
139
300
  state.lastRole = role;
140
301
  if (row.message.model) session.model = row.message.model;
141
302
  const content = Array.isArray(row.message.content)
142
303
  ? row.message.content
143
304
  : [{ type: 'text', text: row.message.content }];
305
+ content.filter(item => item && (!item.type || item.type === 'text'))
306
+ .forEach((item) => {
307
+ const notification = recordTaskNotification(state, typeof item === 'string' ? item : item.text, row.timestamp);
308
+ recordClaudeTaskCompletion(session, notification, row.timestamp);
309
+ });
310
+ if (session.depth && role === 'user') state.subagentCompletedAt = null;
144
311
  content.forEach((item, index) => {
145
312
  if (internalUserRow && (!item.type || item.type === 'text')) return;
146
313
  recordContent(session, state, row, item, index);
@@ -176,6 +343,10 @@ function createClaudeParser(dependencies) {
176
343
  }
177
344
  content.filter(item => item && item.type === 'tool_use' && isUserInputTool(item.name))
178
345
  .forEach(item => state.pendingUserInputCalls.add(String(item.id || item.name)));
346
+ if (String(row.message.stop_reason || '').toLowerCase() === 'end_turn') {
347
+ state.lastTurnFinished = true;
348
+ if (session.depth) state.subagentCompletedAt = timestamp(row.timestamp, state.latestTs);
349
+ }
179
350
  }
180
351
  if (role === 'assistant' && row.message.usage) {
181
352
  const key = row.requestId || row.message.id || row.uuid;
@@ -198,6 +369,7 @@ function createClaudeParser(dependencies) {
198
369
  executionTracker: createExecutionTracker({ compactText, timestamp }),
199
370
  latestTs: session.updatedAt,
200
371
  lastTurnFinished: false,
372
+ subagentCompletedAt: null,
201
373
  };
202
374
  for (const row of rows) {
203
375
  state.latestTs = timestamp(row.timestamp, state.latestTs);
@@ -206,6 +378,8 @@ function createClaudeParser(dependencies) {
206
378
  if (row.gitBranch) session.branch = row.gitBranch;
207
379
  if (row.agentId && session.depth) session.agentName = row.agentId;
208
380
  if (row.type === 'queue-operation' && row.operation === 'enqueue' && row.content) {
381
+ const notification = recordTaskNotification(state, row.content, row.timestamp);
382
+ recordClaudeTaskCompletion(session, notification, row.timestamp);
209
383
  const detectedUtility = utilityKind(row.content);
210
384
  if (detectedUtility) session.utilityKind = detectedUtility;
211
385
  const visibleUser = visibleUserText(row.content);
@@ -245,7 +419,6 @@ function createClaudeParser(dependencies) {
245
419
  session.updatedAt = state.latestTs;
246
420
  session.startedAt = timestamp(parsed.rows[0].timestamp, session.updatedAt);
247
421
  session.usage = sumUsage([...state.requestUsage.values()]);
248
- session.executions = state.executionTracker.finalize();
249
422
  const utilityTitle = session.utilityKind === 'memory-extraction'
250
423
  ? 'Claude 백그라운드 메모리 추출'
251
424
  : (session.utilityKind === 'authentication-check' ? 'Claude 인증 점검' : '');
@@ -260,10 +433,17 @@ function createClaudeParser(dependencies) {
260
433
  const conversationalInput = state.lastConversationRole === 'assistant'
261
434
  && assistantRequestsUserResponse(state.lastAssistantText)
262
435
  && (state.lastTurnFinished || age >= ACTIVE_THRESHOLD_MS);
263
- if (!session.depth && (pendingUserInput || conversationalInput)) {
436
+ if (session.depth && state.subagentCompletedAt) {
437
+ session.status = 'completed';
438
+ session.statusDetail = '작업 완료';
439
+ session.completedAt = state.subagentCompletedAt;
440
+ session.completionObserved = true;
441
+ session.statusObserved = false;
442
+ session.result = state.lastAssistantText || session.result;
443
+ } else if (!session.depth && (pendingUserInput || conversationalInput)) {
264
444
  session.status = 'waiting';
265
445
  session.statusDetail = pendingUserInput ? '선택 또는 입력 대기' : '답변 또는 선택 대기';
266
- } else if (age < ACTIVE_THRESHOLD_MS && !state.lastTurnFinished) {
446
+ } else if (age < STALE_TURN_THRESHOLD_MS && !state.lastTurnFinished) {
267
447
  session.status = 'running';
268
448
  session.statusDetail = state.lastRole === 'user' ? '응답 생성 중' : '도구 실행 또는 스트리밍 중';
269
449
  } else if (state.lastRole === 'user' && age < STALE_TURN_THRESHOLD_MS) {
@@ -273,6 +453,11 @@ function createClaudeParser(dependencies) {
273
453
  session.status = 'idle';
274
454
  session.statusDetail = state.lastRole === 'user' ? '마지막 응답 기록이 종료됨' : '다음 요청 대기';
275
455
  }
456
+ session.executions = reconcileExecutionActivities(state.executionTracker.finalize(), {
457
+ staleAfterMs: STALE_TURN_THRESHOLD_MS,
458
+ turnFinished: state.lastTurnFinished,
459
+ waitingForUser: session.status === 'waiting',
460
+ });
276
461
  session.statusObserved = age < ACTIVE_THRESHOLD_MS;
277
462
  trimSession(session);
278
463
  return session;
@@ -18,6 +18,7 @@ function createCodexCollaboration(session, dependencies) {
18
18
  assignmentObserved: false,
19
19
  assignmentProtected: false,
20
20
  assignmentSource: 'unavailable',
21
+ assignmentContext: '',
21
22
  sharedGoal: '',
22
23
  status: 'requested',
23
24
  startedAt: null,
@@ -232,20 +232,21 @@ function createCodexParser(dependencies) {
232
232
  : record.agentPath;
233
233
  record.assignmentProtected = messageProtected;
234
234
  const directAssignment = rawMessage && !messageProtected ? rawMessage : '';
235
- record.assignment = directAssignment || (messageProtected ? state.latestDelegationNarration : '');
236
- record.assignmentObserved = Boolean(record.assignment);
237
- record.assignmentSource = directAssignment ? 'spawn-message' : (record.assignment ? 'parent-narration' : 'unavailable');
235
+ record.assignment = directAssignment;
236
+ record.assignmentObserved = Boolean(directAssignment);
237
+ record.assignmentSource = directAssignment ? 'spawn-message' : (messageProtected ? 'protected' : 'unavailable');
238
+ record.assignmentContext = messageProtected ? compactText(state.latestDelegationNarration, 6000) : '';
238
239
  record.sharedGoal = compactText(state.latestUser, 6000);
239
240
  record.startedAt = timestamp(row.timestamp, record.startedAt || session.updatedAt);
240
241
  state.collaboration.addCommunication({
241
242
  id: `assign:${callId}`,
242
243
  kind: 'assignment',
243
- label: record.assignmentSource === 'parent-narration' ? '메인 AI 작업 지시' : '새 작업 배정',
244
+ label: '새 작업 배정',
244
245
  from: session.agentPath || '/root',
245
246
  to: record.agentPath,
246
247
  taskName: record.taskName,
247
248
  text: record.assignment,
248
- protected: record.assignmentProtected && !record.assignment,
249
+ protected: record.assignmentProtected,
249
250
  assignmentSource: record.assignmentSource,
250
251
  timestamp: row.timestamp,
251
252
  });
@@ -156,7 +156,7 @@ function outputStatus(value, explicitError = false) {
156
156
  if (firstLine.startsWith('script running')) return 'running';
157
157
  const code = exitCode(text);
158
158
  if (explicitError || (code != null && code !== 0) || /(?:script|command|process)\s+failed|isError["':\s]+true|fatal error/i.test(text)) return 'failed';
159
- if (/(?:script|command|process)\s+(?:is\s+)?(?:still\s+)?running|running\s+in\s+(?:the\s+)?background|background\s+(?:task|command)|yielded/i.test(text)) return 'running';
159
+ if (/^(?:command|process)\s+(?:is\s+)?(?:still\s+)?running\b|\brunning\s+in\s+(?:the\s+)?background\b|^background\s+(?:task|command)\b.*\brunning\b|\byielded\b/i.test(firstLine)) return 'running';
160
160
  return 'completed';
161
161
  }
162
162
 
@@ -270,6 +270,30 @@ function createExecutionTracker(options = {}) {
270
270
  return { activities, recordCall, recordOutput, finalize };
271
271
  }
272
272
 
273
+ function reconcileExecutionActivities(activities = [], options = {}) {
274
+ const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
275
+ const staleAfterMs = Math.max(0, Number(options.staleAfterMs || 0));
276
+ const turnSettled = Boolean(options.turnFinished || options.waitingForUser);
277
+ return activities.map((activity) => {
278
+ if (!activity || activity.status !== 'running') return activity;
279
+ const observedAt = Date.parse(activity.updatedAt || activity.startedAt || 0);
280
+ const observationAge = Number.isFinite(observedAt) ? Math.max(0, now - observedAt) : Number.POSITIVE_INFINITY;
281
+ const foregroundEnded = activity.mode !== 'background' && turnSettled;
282
+ const backgroundUnobserved = activity.mode === 'background'
283
+ && staleAfterMs > 0
284
+ && observationAge >= staleAfterMs;
285
+ if (!foregroundEnded && !backgroundUnobserved) return activity;
286
+ return {
287
+ ...activity,
288
+ status: 'unverified',
289
+ statusDetail: foregroundEnded
290
+ ? '턴 종료 후 실행 상태 미확인'
291
+ : '최근 실행 신호 없음',
292
+ completedAt: null,
293
+ };
294
+ });
295
+ }
296
+
273
297
  module.exports = {
274
298
  CONTINUATION_TOOLS,
275
299
  SHELL_TOOLS,
@@ -277,6 +301,7 @@ module.exports = {
277
301
  executionInput,
278
302
  outputStatus,
279
303
  outputText,
304
+ reconcileExecutionActivities,
280
305
  runtimeHandle,
281
306
  toolName,
282
307
  };
@@ -45,8 +45,8 @@ function createHierarchyAttacher(dependencies) {
45
45
  }
46
46
 
47
47
  function createHistoryChild(parent, record) {
48
- const externalId = String(record.childId || `${parent.externalId}:spawn:${record.callId}`).replace(/^codex:/, '');
49
- const child = baseSession('codex', externalId, '', {
48
+ const externalId = String(record.childId || `${parent.externalId}:spawn:${record.callId}`).replace(/^(?:codex|claude):/, '');
49
+ const child = baseSession(parent.provider, externalId, '', {
50
50
  mtimeMs: Date.parse(record.completedAt || record.startedAt || parent.updatedAt) || Date.now(),
51
51
  });
52
52
  child.id = record.childId || child.id;
@@ -69,7 +69,7 @@ function createHierarchyAttacher(dependencies) {
69
69
  child.completionObserved = child.status === 'completed';
70
70
  child.result = record.result || '';
71
71
  child.source = 'collaboration-history';
72
- child.sourceLabel = 'Codex 협업 이벤트';
72
+ child.sourceLabel = parent.provider === 'claude' ? 'Claude 협업 이벤트' : 'Codex 협업 이벤트';
73
73
  child.clientKind = parent.clientKind;
74
74
  child.model = parent.model;
75
75
  child.cwd = parent.cwd;
@@ -131,6 +131,7 @@ function createHierarchyAttacher(dependencies) {
131
131
  assignmentObserved: record.assignmentObserved,
132
132
  assignmentProtected: record.assignmentProtected,
133
133
  assignmentSource: record.assignmentSource,
134
+ assignmentContext: record.assignmentContext || '',
134
135
  sharedGoal: record.sharedGoal || child.sharedGoal || parent.title,
135
136
  result: record.result,
136
137
  startedAt: record.startedAt,
@@ -151,6 +152,7 @@ function createHierarchyAttacher(dependencies) {
151
152
  assignmentObserved: false,
152
153
  assignmentProtected: false,
153
154
  assignmentSource: 'unavailable',
155
+ assignmentContext: '',
154
156
  sharedGoal: child.sharedGoal || parent.title,
155
157
  status: child.status === 'completed' ? 'completed' : (child.status === 'running' ? 'running' : 'idle'),
156
158
  startedAt: child.startedAt,
@@ -169,6 +171,7 @@ function createHierarchyAttacher(dependencies) {
169
171
  assignmentObserved: false,
170
172
  assignmentProtected: true,
171
173
  assignmentSource: 'unavailable',
174
+ assignmentContext: '',
172
175
  sharedGoal: child.sharedGoal || parent.title,
173
176
  result: child.result || '',
174
177
  startedAt: child.startedAt,
@@ -492,10 +492,17 @@ class AgentMonitor extends EventEmitter {
492
492
 
493
493
  parseFile(info, parser, variant = '') {
494
494
  const key = `${info.file}|${info.mtimeMs}|${info.size}|${variant}`;
495
- const cached = this.parseCache.get(key);
496
- if (cached) return cached;
495
+ const cachedEntry = this.parseCache.get(key);
496
+ const cached = cachedEntry && cachedEntry.value || cachedEntry;
497
+ const parsedAt = Number(cachedEntry && cachedEntry.parsedAt || 0);
498
+ const timeSensitive = Boolean(cached && (
499
+ cached.status === 'running'
500
+ || cached.status === 'starting'
501
+ || (cached.executions || []).some(execution => execution.status === 'running')
502
+ ));
503
+ if (cached && (!timeSensitive || Date.now() - parsedAt < ACTIVE_THRESHOLD_MS)) return cached;
497
504
  const value = parser(info);
498
- if (value) this.parseCache.set(key, value);
505
+ if (value) this.parseCache.set(key, { value, parsedAt: Date.now() });
499
506
  if (this.parseCache.size > 500) {
500
507
  const keep = [...this.parseCache.entries()].slice(-300);
501
508
  this.parseCache = new Map(keep);
package/src/contracts.js CHANGED
@@ -42,7 +42,7 @@
42
42
  * @property {TokenUsage} usage
43
43
  * @property {Array<Object>} messages
44
44
  * @property {Array<Object>} lifecycle
45
- * @property {Array<{id:string,kind:'shell'|'background',mode:'foreground'|'background',tool:string,runtime:string,label:string,command:string,cwd:string,status:'running'|'completed'|'failed',statusDetail:string,output:string,backgroundId:string,exitCode:number|null,startedAt:string|null,updatedAt:string|null,completedAt:string|null}>} executions Logged regular and background command runs owned by this AI session.
45
+ * @property {Array<{id:string,kind:'shell'|'background',mode:'foreground'|'background',tool:string,runtime:string,label:string,command:string,cwd:string,status:'running'|'completed'|'failed'|'unverified',statusDetail:string,output:string,backgroundId:string,exitCode:number|null,startedAt:string|null,updatedAt:string|null,completedAt:string|null}>} executions Logged regular and background command runs owned by this AI session. `unverified` means the last running observation is no longer fresh enough to claim current liveness.
46
46
  * @property {CollaborationSummary|null} collaboration
47
47
  * @property {{kind:string,iteration:number,phase?:string}|boolean|null} loop Safe execution-loop metadata; internal goal text is never included.
48
48
  * @property {{required:boolean,kind:string,summary:string,requestedAt:string|null,source:string,confidence:string}} attention Actionable reason why the session needs the user.
@@ -107,6 +107,7 @@ function cardCollaboration(value) {
107
107
  assignmentObserved: Boolean(record.assignmentObserved),
108
108
  assignmentProtected: Boolean(record.assignmentProtected),
109
109
  assignmentSource: clip(record.assignmentSource, 80),
110
+ assignmentContext: clip(record.assignmentContext, 1200),
110
111
  sharedGoal: clip(record.sharedGoal, 1200),
111
112
  status: record.status,
112
113
  startedAt: record.startedAt,
@@ -194,6 +195,7 @@ function cardSession(session) {
194
195
  assignmentObserved: Boolean(session.delegation.assignmentObserved),
195
196
  assignmentProtected: Boolean(session.delegation.assignmentProtected),
196
197
  assignmentSource: clip(session.delegation.assignmentSource, 80),
198
+ assignmentContext: clip(session.delegation.assignmentContext, 1200),
197
199
  sharedGoal: clip(session.delegation.sharedGoal, 1200),
198
200
  result: clip(session.delegation.result, 1200),
199
201
  startedAt: session.delegation.startedAt,