loadtoagent 1.1.0 → 1.3.2

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 (86) hide show
  1. package/README.ko.md +14 -5
  2. package/README.md +7 -0
  3. package/README.zh-CN.md +7 -0
  4. package/docs/PROVIDER-CONTRACTS.md +25 -1
  5. package/docs/UI-AUDIT-100.md +118 -0
  6. package/docs/UI-AUDIT-101-200.md +120 -0
  7. package/docs/UI-AUDIT-201-300.md +120 -0
  8. package/main.js +157 -38
  9. package/package.json +13 -4
  10. package/preload.js +13 -2
  11. package/renderer/app-agent-actions.js +125 -53
  12. package/renderer/app-bootstrap.js +54 -19
  13. package/renderer/app-dashboard.js +196 -55
  14. package/renderer/app-drawer-content.js +48 -44
  15. package/renderer/app-drawer-data.js +26 -13
  16. package/renderer/app-drawer.js +72 -54
  17. package/renderer/app-events-dialogs.js +146 -37
  18. package/renderer/app-events-filters.js +172 -13
  19. package/renderer/app-events-navigation.js +77 -30
  20. package/renderer/app-events-sessions.js +156 -9
  21. package/renderer/app-events.js +2 -1
  22. package/renderer/app-graph-model.js +20 -38
  23. package/renderer/app-graph-orchestration.js +15 -13
  24. package/renderer/app-graph-view.js +228 -113
  25. package/renderer/app-management.js +257 -0
  26. package/renderer/app-provider-visibility.js +124 -0
  27. package/renderer/app-quality.js +568 -0
  28. package/renderer/app-run-modal.js +104 -34
  29. package/renderer/app-runtime-overview.js +312 -0
  30. package/renderer/app-session-render.js +94 -37
  31. package/renderer/app-tmux-render.js +52 -44
  32. package/renderer/app.js +153 -75
  33. package/renderer/i18n-messages.js +867 -37
  34. package/renderer/i18n.js +104 -0
  35. package/renderer/index.html +154 -67
  36. package/renderer/shared.js +17 -0
  37. package/renderer/styles-agent-map.css +16 -10
  38. package/renderer/styles-cards.css +44 -18
  39. package/renderer/styles-collaboration.css +23 -23
  40. package/renderer/styles-components.css +122 -45
  41. package/renderer/styles-management.css +364 -0
  42. package/renderer/styles-onboarding.css +8 -8
  43. package/renderer/styles-overlays.css +13 -7
  44. package/renderer/styles-product.css +48 -40
  45. package/renderer/styles-quality.css +312 -0
  46. package/renderer/styles-readability.css +1040 -0
  47. package/renderer/styles-responsive-product.css +84 -12
  48. package/renderer/styles-responsive-runtime.css +25 -17
  49. package/renderer/styles-responsive-shell.css +44 -48
  50. package/renderer/styles-responsive-workflows.css +39 -5
  51. package/renderer/styles-run-composer.css +28 -23
  52. package/renderer/styles-runtime-overview.css +285 -0
  53. package/renderer/styles-settings.css +176 -16
  54. package/renderer/styles-terminal.css +374 -37
  55. package/renderer/styles-tmux.css +49 -49
  56. package/renderer/styles-workflow-map.css +362 -15
  57. package/renderer/styles-workflows.css +116 -49
  58. package/renderer/styles.css +34 -19
  59. package/renderer/terminal-agent.js +17 -13
  60. package/renderer/terminal-events.js +253 -37
  61. package/renderer/terminal-workbench.js +208 -89
  62. package/renderer/terminal.js +350 -35
  63. package/src/agentMonitor/claudeParser.js +96 -7
  64. package/src/agentMonitor/codexParser.js +59 -4
  65. package/src/agentMonitor/executionActivity.js +282 -0
  66. package/src/agentMonitor/genericParser.js +56 -13
  67. package/src/agentMonitor/hierarchy.js +17 -0
  68. package/src/agentMonitor/responseIntent.js +51 -0
  69. package/src/agentMonitor.js +21 -3
  70. package/src/agentRunner.js +66 -1
  71. package/src/attentionNotifier.js +14 -10
  72. package/src/automationMonitor.js +258 -0
  73. package/src/contracts.js +10 -1
  74. package/src/ipc/registerAgentIpc.js +9 -3
  75. package/src/ipc/registerAppIpc.js +4 -1
  76. package/src/ipc/registerTerminalIpc.js +12 -14
  77. package/src/macUpdateHelper.js +210 -0
  78. package/src/monitorWorker.js +65 -6
  79. package/src/platformPath.js +80 -0
  80. package/src/processMonitor.js +80 -22
  81. package/src/providerVisibilityStore.js +45 -0
  82. package/src/sessionIntelligence.js +261 -0
  83. package/src/terminalHost.js +453 -0
  84. package/src/terminalHostDaemon.js +61 -0
  85. package/src/terminalManager.js +218 -6
  86. package/src/updateInstaller.js +175 -0
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const path = require('path');
4
+ const { createExecutionTracker } = require('./executionActivity');
4
5
 
5
6
  const TOOL_START_PATTERN = /tool_use|tool-call|tool_start/;
6
7
  const TOOL_END_PATTERN = /tool_result|tool-result|tool_end/;
@@ -22,6 +23,8 @@ function createGenericParser(dependencies) {
22
23
  sumUsage,
23
24
  timestamp,
24
25
  trimSession,
26
+ assistantRequestsUserResponse,
27
+ isUserInputTool,
25
28
  } = dependencies;
26
29
 
27
30
  function normalizeUsage(raw = {}) {
@@ -72,6 +75,7 @@ function createGenericParser(dependencies) {
72
75
  const session = baseSession(provider, externalId, fileInfo.file, fileInfo);
73
76
  session.truncated = parsed.truncated;
74
77
  session.cwd = root.cwd || root.projectPath || root.project_path || '';
78
+ session.originCwd = session.cwd;
75
79
  session.model = root.model || root.modelId || '';
76
80
  session.startedAt = timestamp(root.startTime || root.startedAt || root.created_at, session.updatedAt);
77
81
  session.parentId = root.parent_session_id ? `${provider}:${root.parent_session_id}` : null;
@@ -79,8 +83,12 @@ function createGenericParser(dependencies) {
79
83
  return { rows, root, session };
80
84
  }
81
85
 
82
- function recordToolStart(session, event) {
86
+ function recordToolStart(session, state, event) {
83
87
  const tool = event.tool_name || event.name || event.tool || 'tool';
88
+ const callId = event.tool_call_id || event.tool_use_id || event.id;
89
+ const args = event.parameters || event.args || event.input || {};
90
+ state.toolCalls.set(String(callId || ''), { name: tool, args });
91
+ state.executionTracker.recordCall({ name: tool, callId, args, rawInput: args, at: event.timestamp });
84
92
  addMessage(session, {
85
93
  id: event.id,
86
94
  role: 'tool',
@@ -99,8 +107,17 @@ function createGenericParser(dependencies) {
99
107
  });
100
108
  }
101
109
 
102
- function recordToolEnd(session, event) {
110
+ function recordToolEnd(session, state, event) {
103
111
  const callId = event.tool_call_id || event.tool_use_id || event.id;
112
+ const call = state.toolCalls.get(String(callId || ''));
113
+ state.executionTracker.recordOutput({
114
+ name: call && call.name,
115
+ callId,
116
+ args: call && call.args || {},
117
+ output: event.output || event.result || event.content || event,
118
+ at: event.timestamp,
119
+ isError: Boolean(event.error),
120
+ });
104
121
  settleLifecycle(session, callId, event.error ? 'failed' : 'done', event.timestamp);
105
122
  addLifecycle(session, {
106
123
  id: `result:${event.id || event.tool_call_id}`,
@@ -112,7 +129,13 @@ function createGenericParser(dependencies) {
112
129
  }
113
130
 
114
131
  function processEvents(session, events) {
115
- const state = { running: false, failed: false };
132
+ const state = {
133
+ running: false,
134
+ failed: false,
135
+ pendingUserInputCalls: new Set(),
136
+ toolCalls: new Map(),
137
+ executionTracker: createExecutionTracker({ compactText, timestamp }),
138
+ };
116
139
  for (const event of events) {
117
140
  const type = String(event.type || event.event || event.kind || '').toLowerCase();
118
141
  if (type === 'init') {
@@ -127,10 +150,15 @@ function createGenericParser(dependencies) {
127
150
  });
128
151
  }
129
152
  if (TOOL_START_PATTERN.test(type)) {
130
- recordToolStart(session, event);
153
+ recordToolStart(session, state, event);
131
154
  state.running = true;
155
+ const toolName = event.tool_name || event.name || event.tool;
156
+ if (isUserInputTool(toolName)) state.pendingUserInputCalls.add(String(event.id || toolName));
157
+ }
158
+ if (TOOL_END_PATTERN.test(type)) {
159
+ recordToolEnd(session, state, event);
160
+ state.pendingUserInputCalls.delete(String(event.tool_call_id || event.tool_use_id || event.id || ''));
132
161
  }
133
- if (TOOL_END_PATTERN.test(type)) recordToolEnd(session, event);
134
162
  if (type === 'result' || /session_end|completed/.test(type)) state.running = false;
135
163
  if (type === 'error' || event.error) state.failed = true;
136
164
  const usage = normalizeUsage(event);
@@ -194,10 +222,17 @@ function createGenericParser(dependencies) {
194
222
  usages.push(usage);
195
223
  }
196
224
  }
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 };
225
+ const orderedMessages = [...messages.values()]
226
+ .sort((a, b) => Date.parse(a.timestamp) - Date.parse(b.timestamp) || a.order - b.order);
227
+ orderedMessages.forEach(message => addMessage(session, message));
228
+ const lastConversation = [...orderedMessages].reverse()
229
+ .find(message => message.role === 'assistant' || message.role === 'user');
230
+ return {
231
+ firstUser,
232
+ usages,
233
+ lastConversationRole: lastConversation && lastConversation.role || '',
234
+ lastAssistantText: lastConversation && lastConversation.role === 'assistant' ? lastConversation.text : '',
235
+ };
201
236
  }
202
237
 
203
238
  function finalizeSession(session, provider, root, eventState, messageState, fileInfo) {
@@ -208,15 +243,23 @@ function createGenericParser(dependencies) {
208
243
  const context = modelContextWindow(provider, session.model, root.context_window || root.contextWindow);
209
244
  session.context = contextInfo(session.turnUsage.total || session.usage.total, context);
210
245
  const age = Date.now() - fileInfo.mtimeMs;
246
+ const pendingUserInput = eventState.pendingUserInputCalls.size > 0;
247
+ const conversationalInput = messageState.lastConversationRole === 'assistant'
248
+ && assistantRequestsUserResponse(messageState.lastAssistantText);
211
249
  session.status = eventState.failed
212
250
  ? 'failed'
213
- : ((eventState.running && age < STALE_TURN_THRESHOLD_MS) || age < ACTIVE_THRESHOLD_MS
251
+ : (pendingUserInput || (!eventState.running && conversationalInput)
252
+ ? 'waiting'
253
+ : ((eventState.running && age < STALE_TURN_THRESHOLD_MS) || age < ACTIVE_THRESHOLD_MS
214
254
  ? 'running'
215
- : 'idle');
255
+ : 'idle'));
216
256
  session.statusDetail = eventState.failed
217
257
  ? '오류 발생'
218
- : (session.status === 'running' ? '실시간 이벤트 수신 중' : '다음 요청 대기');
219
- session.statusObserved = eventState.running;
258
+ : (session.status === 'waiting'
259
+ ? (pendingUserInput ? '선택 또는 입력 대기' : '답변 또는 선택 대기')
260
+ : (session.status === 'running' ? '실시간 이벤트 수신 중' : '다음 요청 대기'));
261
+ session.statusObserved = eventState.running || session.status === 'waiting';
262
+ session.executions = eventState.executionTracker.finalize();
220
263
  trimSession(session);
221
264
  return session;
222
265
  }
@@ -18,6 +18,22 @@ function createHierarchyAttacher(dependencies) {
18
18
  const parent = byId.get(session.parentId);
19
19
  if (parent && !parent.childIds.includes(session.id)) parent.childIds.push(session.id);
20
20
  }
21
+ const inheritOrigin = (session, seen = new Set()) => {
22
+ if (!session || !session.parentId || seen.has(session.id)) return;
23
+ seen.add(session.id);
24
+ const parent = byId.get(session.parentId);
25
+ if (!parent) return;
26
+ inheritOrigin(parent, seen);
27
+ const hadOrigin = Boolean(session.originCwd || session.cwd);
28
+ if (!session.cwd) session.cwd = parent.cwd;
29
+ if (!session.originCwd) session.originCwd = parent.originCwd || parent.cwd;
30
+ if (!hadOrigin) {
31
+ session.workspace = parent.workspace;
32
+ session.projectless = parent.projectless;
33
+ }
34
+ if (!session.environment) session.environment = parent.environment;
35
+ };
36
+ for (const session of sessions) inheritOrigin(session);
21
37
  return byId;
22
38
  }
23
39
 
@@ -57,6 +73,7 @@ function createHierarchyAttacher(dependencies) {
57
73
  child.clientKind = parent.clientKind;
58
74
  child.model = parent.model;
59
75
  child.cwd = parent.cwd;
76
+ child.originCwd = parent.originCwd || parent.cwd;
60
77
  child.workspace = parent.workspace;
61
78
  child.projectless = parent.projectless;
62
79
  child.environment = parent.environment;
@@ -0,0 +1,51 @@
1
+ 'use strict';
2
+
3
+ const USER_INPUT_TOOL_PATTERN = /^(?:request_user_input|ask_user_question|askuserquestion|request_input|get_user_input)$/i;
4
+
5
+ function isUserInputTool(name) {
6
+ return USER_INPUT_TOOL_PATTERN.test(String(name || '').trim());
7
+ }
8
+
9
+ function conversationalTail(value) {
10
+ const text = String(value || '')
11
+ .replace(/```[\s\S]*?```/g, ' ')
12
+ .replace(/~~~[\s\S]*?~~~/g, ' ')
13
+ .replace(/`[^`\r\n]*`/g, ' ')
14
+ .replace(/^\s*>.*$/gm, ' ')
15
+ .replace(/!?(?:\[[^\]]*\])\([^\s)]+(?:\s+"[^"]*")?\)/g, '$1')
16
+ .replace(/https?:\/\/\S+/g, ' ')
17
+ .replace(/[ \t]+/g, ' ')
18
+ .replace(/\n{3,}/g, '\n\n')
19
+ .trim();
20
+ return text.slice(-1600).trim();
21
+ }
22
+
23
+ function assistantRequestsUserResponse(value) {
24
+ const tail = conversationalTail(value);
25
+ if (!tail) return false;
26
+
27
+ // A question at the end of the final prose is the strongest provider-neutral signal.
28
+ if (/[??]\s*(?:["')\]}>*_~]|&gt;)*\s*$/.test(tail)) return true;
29
+
30
+ const koreanRequest = /(?:선택|골라|알려|말씀|답변|확인|결정|지정|입력|보내|첨부|업로드|제공)(?:해|하여|해서|해\s*)?(?:주세요|주십시오|주실래요|바랍니다)(?:[.!::]|\s|$)/.test(tail);
31
+ const englishRequest = [
32
+ // A polite request may follow introductory prose, so "please"/"kindly"
33
+ // is a strong signal wherever it appears.
34
+ /\b(?:please|kindly)\s+(?:choose|select|confirm|tell\s+me|let\s+me\s+know|provide|send|share|enter|upload|attach)\b/i,
35
+ // Bare imperatives are requests only at the beginning of prose or a new
36
+ // sentence. This avoids treating technical words such as "resend" or a
37
+ // Korean completion report containing "attach" as user-input requests.
38
+ /(?:^|[\r\n]+|[.!?]\s+)\s*(?:choose|select|confirm|tell\s+me|let\s+me\s+know|provide|send|share|enter|upload|attach)\b/i,
39
+ /(?:^|[\r\n]+|[.!?]\s+)\s*(?:can|could|would|will)\s+you\s+(?:choose|select|confirm|provide|send|share|enter|upload|attach)\b/i,
40
+ ].some(pattern => pattern.test(tail));
41
+ const chineseRequest = /(?:请选择|请确认|请提供|请告诉|请回复|请上传)/.test(tail);
42
+ const directRequest = koreanRequest || englishRequest || chineseRequest;
43
+ if (!directRequest) return false;
44
+
45
+ // Ignore common courtesy offers that do not block the task.
46
+ if (/(?:궁금한|추가 질문|도움이 필요).{0,30}(?:알려|말씀).{0,12}주세요[.!]?$/i.test(tail)) return false;
47
+ if (/let me know if (?:you have|there are) (?:any )?(?:questions|issues)[.!]?$/i.test(tail)) return false;
48
+ return true;
49
+ }
50
+
51
+ module.exports = { assistantRequestsUserResponse, isUserInputTool };
@@ -14,6 +14,7 @@ const { createCodexParser } = require('./agentMonitor/codexParser');
14
14
  const { createClaudeParser } = require('./agentMonitor/claudeParser');
15
15
  const { createGenericParser } = require('./agentMonitor/genericParser');
16
16
  const { createHierarchyAttacher } = require('./agentMonitor/hierarchy');
17
+ const { assistantRequestsUserResponse, isUserInputTool } = require('./agentMonitor/responseIntent');
17
18
  const {
18
19
  MAX_FILES_PER_PROVIDER,
19
20
  readJson,
@@ -178,10 +179,12 @@ function baseSession(provider, externalId, file, stat) {
178
179
  title: '제목을 불러오는 중',
179
180
  model: '',
180
181
  cwd: '',
182
+ originCwd: '',
181
183
  branch: '',
182
184
  source: 'local-history',
183
185
  sourceLabel: '로컬 세션',
184
186
  clientKind: '',
187
+ utilityKind: '',
185
188
  status: 'idle',
186
189
  statusDetail: '',
187
190
  statusObserved: false,
@@ -198,6 +201,7 @@ function baseSession(provider, externalId, file, stat) {
198
201
  context: { used: 0, window: 0, percent: 0, source: 'unknown' },
199
202
  messages: [],
200
203
  lifecycle: [],
204
+ executions: [],
201
205
  childIds: [],
202
206
  collaboration: {
203
207
  capacity: { totalThreads: 0, subagents: 0, source: 'unknown' },
@@ -207,6 +211,7 @@ function baseSession(provider, externalId, file, stat) {
207
211
  retainedObserved: false,
208
212
  metrics: null,
209
213
  },
214
+ loop: null,
210
215
  };
211
216
  }
212
217
 
@@ -234,6 +239,8 @@ const parseClaude = createClaudeParser({
234
239
  sumUsage,
235
240
  timestamp,
236
241
  trimSession,
242
+ assistantRequestsUserResponse,
243
+ isUserInputTool,
237
244
  });
238
245
 
239
246
  function codexUsage(raw = {}) {
@@ -258,6 +265,7 @@ function codexVisibleUserText(value) {
258
265
  if (objective) return compactText(objective[1], 6000);
259
266
  const desktopRequest = raw.match(/##\s*My request for Codex:\s*([\s\S]*?)(?:\n{2,}<image\b|$)/i);
260
267
  if (desktopRequest) return compactText(desktopRequest[1], 6000);
268
+ if (/^<(?:subagent_notification|task_notification|task-notification|agent_notification|collaboration_notification)(?:\s|>)/i.test(raw)) return '';
261
269
  if (/^<(?:permissions instructions|app-context|environment_context|skills_instructions|plugins_instructions|apps_instructions|multi_agent_mode|collaboration_mode)>/i.test(raw)) return '';
262
270
  if (/^<skill(?:\s|>)/i.test(raw)) return '';
263
271
  if (/^#\s*Codex desktop context/i.test(raw)) return '';
@@ -302,6 +310,7 @@ const parseCodex = createCodexParser({
302
310
  textOps: {
303
311
  agentEnvelope, codexContentText, codexVisibleUserText,
304
312
  compactText, encryptedCollaborationText, jsonObject,
313
+ assistantRequestsUserResponse, isUserInputTool,
305
314
  },
306
315
  collaborationOps: {
307
316
  collaborationCapacity, collaborationTaskName,
@@ -328,6 +337,8 @@ const parseGeneric = createGenericParser({
328
337
  sumUsage,
329
338
  timestamp,
330
339
  trimSession,
340
+ assistantRequestsUserResponse,
341
+ isUserInputTool,
331
342
  });
332
343
 
333
344
  function contextInfo(used, windowInfo) {
@@ -363,6 +374,7 @@ function parseManagedSession(runDir) {
363
374
  sourceLabel: 'LoadToAgent 실행',
364
375
  statusObserved: true,
365
376
  };
377
+ session.originCwd = session.originCwd || meta.cwd || session.cwd || '';
366
378
  session.usage = finalizeUsage(session.usage);
367
379
  session.turnUsage = finalizeUsage(session.turnUsage);
368
380
  const window = modelContextWindow(session.provider, session.model, session.context && session.context.window);
@@ -378,8 +390,9 @@ function workspaceLabel(cwd) {
378
390
  }
379
391
 
380
392
  function isProjectlessSession(session) {
381
- if (!session || !session.cwd) return true;
382
- const normalized = String(session.cwd).replace(/\\/g, '/').replace(/\/+$/, '');
393
+ const cwd = session && (session.originCwd || session.cwd);
394
+ if (!cwd) return true;
395
+ const normalized = String(cwd).replace(/\\/g, '/').replace(/\/+$/, '');
383
396
  return session.provider === 'codex'
384
397
  && session.clientKind === 'codex-desktop'
385
398
  && /(?:^|\/)Documents\/Codex\/\d{4}-\d{2}-\d{2}\/new-chat$/i.test(normalized);
@@ -528,6 +541,10 @@ class AgentMonitor extends EventEmitter {
528
541
  const value = this.parseFile(info, parser);
529
542
  if (!value) continue;
530
543
  const copy = structuredClone(value);
544
+ // Provider health checks and detached memory extraction turns are
545
+ // implementation details, not user work. Keep them out of the
546
+ // dashboard and runtime-link candidate pool entirely.
547
+ if (copy.utilityKind) continue;
531
548
  copy.environment = { kind: history.kind, distro: history.distro, label: history.label, home: history.home };
532
549
  if (history.kind === 'wsl') copy.sourceLabel = history.label;
533
550
  sessions.push(copy);
@@ -548,8 +565,9 @@ class AgentMonitor extends EventEmitter {
548
565
  for (const session of managed) byId.set(session.id, session);
549
566
  const merged = [...byId.values()]
550
567
  .map(session => {
568
+ const originCwd = session.originCwd || session.cwd || '';
551
569
  const projectless = isProjectlessSession(session);
552
- return { ...session, projectless, workspace: projectless ? '프로젝트 없음' : workspaceLabel(session.cwd) };
570
+ return { ...session, originCwd, projectless, workspace: projectless ? '프로젝트 없음' : workspaceLabel(originCwd) };
553
571
  })
554
572
  .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt));
555
573
  attachHierarchy(merged);
@@ -385,7 +385,7 @@ class AgentRunner extends EventEmitter {
385
385
  if (run.stopping) {
386
386
  state.status = 'cancelled';
387
387
  state.statusDetail = '사용자가 중지함';
388
- } else if (state.status === 'running' || state.status === 'starting') {
388
+ } else if (state.status === 'running' || state.status === 'starting' || state.status === 'paused') {
389
389
  state.status = code === 0 ? 'completed' : 'failed';
390
390
  state.statusDetail = code === 0 ? '작업 완료' : `CLI 종료 코드 ${code}${signal ? ` · ${signal}` : ''}`;
391
391
  }
@@ -450,10 +450,75 @@ class AgentRunner extends EventEmitter {
450
450
  if (process.platform === 'win32') {
451
451
  execFile('taskkill', ['/PID', String(run.child.pid), '/T', '/F'], { windowsHide: true }, () => {});
452
452
  } else {
453
+ if (run.state.status === 'paused') runBestEffort('runner-stop-resume', () => process.kill(run.child.pid, 'SIGCONT'));
453
454
  runBestEffort('runner-stop', () => run.child.kill('SIGTERM'));
454
455
  }
455
456
  return { ok: true };
456
457
  }
458
+
459
+ retry(id) {
460
+ const runIdValue = String(id || '');
461
+ if (!/^[a-z0-9-]{4,120}$/i.test(runIdValue)) return { ok: false, error: '다시 실행할 작업 ID가 올바르지 않습니다.' };
462
+ if (this.active.has(runIdValue)) return { ok: false, error: '아직 실행 중인 작업은 다시 실행할 수 없습니다.' };
463
+ const file = path.join(this.runsDir, runIdValue, 'meta.json');
464
+ let meta;
465
+ try {
466
+ meta = JSON.parse(fs.readFileSync(file, 'utf8'));
467
+ } catch (_unavailableRunMetadata) {
468
+ return { ok: false, error: '이전 실행 설정을 찾을 수 없습니다.' };
469
+ }
470
+ const result = this.start({
471
+ provider: meta.provider,
472
+ prompt: meta.prompt,
473
+ cwd: meta.cwd,
474
+ model: meta.model,
475
+ allowWrites: Boolean(meta.allowWrites),
476
+ });
477
+ return result && result.ok ? { ...result, retriedFrom: runIdValue } : result;
478
+ }
479
+
480
+ setPaused(id, paused) {
481
+ const run = this.active.get(String(id || ''));
482
+ if (!run) return Promise.resolve({ ok: false, error: '실행 중인 작업을 찾을 수 없습니다.' });
483
+ if (paused && run.state.status === 'paused') return Promise.resolve({ ok: true, status: 'paused' });
484
+ if (!paused && run.state.status !== 'paused') return Promise.resolve({ ok: true, status: run.state.status });
485
+ const applyState = () => {
486
+ run.state.status = paused ? 'paused' : 'running';
487
+ run.state.statusDetail = paused ? '사용자가 실행을 일시정지함' : '사용자가 실행을 다시 시작함';
488
+ addLifecycle(run.state, paused ? 'process-pause' : 'process-resume', paused ? '실행 일시정지' : '실행 다시 시작', {
489
+ id: `${paused ? 'pause' : 'resume'}:${Date.now()}`,
490
+ status: paused ? 'done' : 'running',
491
+ });
492
+ run.state.updatedAt = new Date().toISOString();
493
+ this.persist(run);
494
+ this.emit('changed', { runId: run.id, state: run.state });
495
+ return { ok: true, status: run.state.status };
496
+ };
497
+ if (process.platform !== 'win32') {
498
+ try {
499
+ process.kill(run.child.pid, paused ? 'SIGSTOP' : 'SIGCONT');
500
+ return Promise.resolve(applyState());
501
+ } catch (error) {
502
+ return Promise.resolve({ ok: false, error: error.message });
503
+ }
504
+ }
505
+ const systemRoot = process.env.SystemRoot || 'C:\\Windows';
506
+ const powershell = path.join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
507
+ const command = `${paused ? 'Suspend' : 'Resume'}-Process -Id ${Number(run.child.pid)} -ErrorAction Stop`;
508
+ return new Promise(resolve => {
509
+ execFile(powershell, ['-NoProfile', '-NonInteractive', '-Command', command], { windowsHide: true }, error => {
510
+ resolve(error ? { ok: false, error: error.message } : applyState());
511
+ });
512
+ });
513
+ }
514
+
515
+ pause(id) {
516
+ return this.setPaused(id, true);
517
+ }
518
+
519
+ resume(id) {
520
+ return this.setPaused(id, false);
521
+ }
457
522
  }
458
523
 
459
524
  module.exports = {
@@ -7,24 +7,28 @@ class AttentionNotifier {
7
7
  this.copy = options.copy || (() => ({ title: '내 확인이 필요합니다', body: '응답이나 선택을 기다리는 AI 세션이 있습니다.' }));
8
8
  this.onOpen = options.onOpen || (() => {});
9
9
  this.onFallback = options.onFallback || (() => {});
10
- this.waitingIds = null;
10
+ this.attentionIds = null;
11
11
  this.notifications = new Set();
12
12
  }
13
13
 
14
14
  sync(snapshot) {
15
- const waiting = (snapshot && Array.isArray(snapshot.sessions) ? snapshot.sessions : [])
16
- .filter(session => session && session.id && session.status === 'waiting');
17
- const nextIds = new Set(waiting.map(session => String(session.id)));
15
+ const needsAttention = (snapshot && Array.isArray(snapshot.sessions) ? snapshot.sessions : [])
16
+ .filter(session => session && session.id && (
17
+ session.attention?.required
18
+ || session.health?.level === 'critical'
19
+ || session.status === 'waiting'
20
+ ));
21
+ const nextIds = new Set(needsAttention.map(session => String(session.id)));
18
22
 
19
- if (this.waitingIds === null) {
20
- this.waitingIds = nextIds;
23
+ if (this.attentionIds === null) {
24
+ this.attentionIds = nextIds;
21
25
  return [];
22
26
  }
23
27
 
24
- const newlyWaiting = waiting.filter(session => !this.waitingIds.has(String(session.id)));
25
- this.waitingIds = nextIds;
26
- newlyWaiting.forEach(session => this.notify(session));
27
- return newlyWaiting.map(session => String(session.id));
28
+ const newlyActionable = needsAttention.filter(session => !this.attentionIds.has(String(session.id)));
29
+ this.attentionIds = nextIds;
30
+ newlyActionable.forEach(session => this.notify(session));
31
+ return newlyActionable.map(session => String(session.id));
28
32
  }
29
33
 
30
34
  notify(session) {