tide-commander 1.136.0 → 1.136.1
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.
- package/dist/assets/{BossLogsModal-B5wtQ9Fh.js → BossLogsModal-ed7L3S97.js} +1 -1
- package/dist/assets/{BossSpawnModal-B8twkNNt.js → BossSpawnModal-C-KkKRgI.js} +1 -1
- package/dist/assets/{ControlsModal-BKUVlO45.js → ControlsModal-DBJPuS6z.js} +1 -1
- package/dist/assets/{DockerLogsModal-Oa0WYexL.js → DockerLogsModal-D8lPR5BA.js} +1 -1
- package/dist/assets/{EmbeddedEditor-Cnkfr9vJ.js → EmbeddedEditor-D5opmQLi.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-Cfsgqs5D.js → GmailOAuthSetup-CGhj8CNw.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-BZCY7oIc.js → GoogleOAuthSetup-Br7IfgCD.js} +1 -1
- package/dist/assets/{IframeModal-BVaOFzOL.js → IframeModal-EOV4pjTD.js} +1 -1
- package/dist/assets/{IntegrationsPanel-CdcwTSOY.js → IntegrationsPanel-BJED0ToF.js} +2 -2
- package/dist/assets/{LogViewerModal-U0o-ixDu.js → LogViewerModal-BY1nfMIb.js} +1 -1
- package/dist/assets/{MonitoringModal-CdYBNEBK.js → MonitoringModal-C88vTxw-.js} +1 -1
- package/dist/assets/{PM2LogsModal-cxSSfOTz.js → PM2LogsModal-CVbr9x3j.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-BkzbecLN.js → RestoreArchivedAreaModal-Du4e4ebk.js} +1 -1
- package/dist/assets/{Scene2DCanvas-GGc__FVu.js → Scene2DCanvas-DW6-BjZg.js} +1 -1
- package/dist/assets/{SceneManager-BWyie0tH.js → SceneManager-DFaHLv0b.js} +1 -1
- package/dist/assets/{SkillsPanel-DS7QNt0-.js → SkillsPanel-R6txFGjx.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-CjHaygiR.js → SlackMultiInstanceSetup-Cd7eklY5.js} +1 -1
- package/dist/assets/{SpawnModal-MWySBWkA.js → SpawnModal-qJUC6gUU.js} +1 -1
- package/dist/assets/{StatisticsModal-D08htYhH.js → StatisticsModal-BVacMzYw.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-DULM1xpx.js → SubordinateAssignmentModal-BM4JvNyT.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-nB6R7Nk4.js → TriggerManagerPanel-CHhTlUF3.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-CKgW8DhB.js → WorkflowEditorPanel-C_V9Xmy8.js} +1 -1
- package/dist/assets/{area-logos-CuU1qdkU.js → area-logos-CcnztkrB.js} +1 -1
- package/dist/assets/{index-Cc_Ys4_9.js → index-47gQB8Y5.js} +1 -1
- package/dist/assets/{index-Csnk6THj.js → index-Bd05QMt_.js} +1 -1
- package/dist/assets/{index-CLmNQzTF.js → index-BvfNZfX7.js} +2 -2
- package/dist/assets/{index-B6TRH1E2.js → index-BvlPsF-d.js} +1 -1
- package/dist/assets/{index-ih8yuWhL.js → index-CF55uz6s.js} +1 -1
- package/dist/assets/{index-CWiGs6nn.js → index-CMP6SUq-.js} +1 -1
- package/dist/assets/{index-CQPpjjTd.js → index-DKbbtwcq.js} +1 -1
- package/dist/assets/{index-8DzLSviG.js → index-DXbpyN-P.js} +1 -1
- package/dist/assets/{index-CWYcGGCJ.js → index-DwcZd659.js} +3 -3
- package/dist/assets/main-Ba3wdQK_.css +1 -0
- package/dist/assets/{main-CHE4wC7P.js → main-Db4or1Jx.js} +42 -42
- package/dist/assets/{web-dmnfXY_q.js → web-BahzYw5F.js} +1 -1
- package/dist/assets/{web-D1Ur_Mtd.js → web-BnBWe9ee.js} +1 -1
- package/dist/assets/{web-C4yrL2Hp.js → web-BsXkVdvj.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/src/packages/server/claude/backend.js +56 -6
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +4 -0
- package/dist/src/packages/server/claude/runner/watchdog.js +34 -0
- package/dist/src/packages/server/services/runtime-events.js +76 -9
- package/dist/src/packages/server/services/runtime-subagents.js +42 -0
- package/package.json +1 -1
- package/dist/assets/main-DM0iS4EN.css +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cO as s}from"./main-
|
|
1
|
+
import{cO as s}from"./main-Db4or1Jx.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class l extends s{constructor(){super(...arguments),this.pending=[],this.deliveredNotifications=[],this.hasNotificationSupport=()=>{if(!("Notification"in window)||!Notification.requestPermission)return!1;if(Notification.permission!=="granted")try{new Notification("")}catch(i){if(i instanceof Error&&i.name==="TypeError")return!1}return!0}}async getDeliveredNotifications(){const i=[];for(const t of this.deliveredNotifications){const e={title:t.title,id:parseInt(t.tag),body:t.body};i.push(e)}return{notifications:i}}async removeDeliveredNotifications(i){for(const t of i.notifications){const e=this.deliveredNotifications.find(n=>n.tag===String(t.id));e==null||e.close(),this.deliveredNotifications=this.deliveredNotifications.filter(()=>!e)}}async removeAllDeliveredNotifications(){for(const i of this.deliveredNotifications)i.close();this.deliveredNotifications=[]}async createChannel(){throw this.unimplemented("Not implemented on web.")}async deleteChannel(){throw this.unimplemented("Not implemented on web.")}async listChannels(){throw this.unimplemented("Not implemented on web.")}async schedule(i){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");for(const t of i.notifications)this.sendNotification(t);return{notifications:i.notifications.map(t=>({id:t.id}))}}async getPending(){return{notifications:this.pending}}async registerActionTypes(){throw this.unimplemented("Not implemented on web.")}async cancel(i){this.pending=this.pending.filter(t=>!i.notifications.find(e=>e.id===t.id))}async areEnabled(){const{display:i}=await this.checkPermissions();return{value:i==="granted"}}async changeExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async checkExactNotificationSetting(){throw this.unimplemented("Not implemented on web.")}async requestPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(await Notification.requestPermission())}}async checkPermissions(){if(!this.hasNotificationSupport())throw this.unavailable("Notifications not supported in this browser.");return{display:this.transformNotificationPermission(Notification.permission)}}transformNotificationPermission(i){switch(i){case"granted":return"granted";case"denied":return"denied";default:return"prompt"}}sendPending(){var i;const t=[],e=new Date().getTime();for(const n of this.pending)!((i=n.schedule)===null||i===void 0)&&i.at&&n.schedule.at.getTime()<=e&&(this.buildNotification(n),t.push(n));this.pending=this.pending.filter(n=>!t.find(o=>o===n))}sendNotification(i){var t;if(!((t=i.schedule)===null||t===void 0)&&t.at){const e=i.schedule.at.getTime()-new Date().getTime();this.pending.push(i),setTimeout(()=>{this.sendPending()},e);return}this.buildNotification(i)}buildNotification(i){const t=new Notification(i.title,{body:i.body,tag:String(i.id)});return t.addEventListener("click",this.onClick.bind(this,i),!1),t.addEventListener("show",this.onShow.bind(this,i),!1),t.addEventListener("close",()=>{this.deliveredNotifications=this.deliveredNotifications.filter(()=>!this)},!1),this.deliveredNotifications.push(t),t}onClick(i){const t={actionId:"tap",notification:i};this.notifyListeners("localNotificationActionPerformed",t)}onShow(i){this.notifyListeners("localNotificationReceived",i)}}export{l as LocalNotificationsWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cO as t}from"./main-
|
|
1
|
+
import{cO as t}from"./main-Db4or1Jx.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class o extends t{constructor(){super(),this.handleVisibilityChange=()=>{const e={isActive:document.hidden!==!0};this.notifyListeners("appStateChange",e),document.hidden?this.notifyListeners("pause",null):this.notifyListeners("resume",null)},document.addEventListener("visibilitychange",this.handleVisibilityChange,!1)}exitApp(){throw this.unimplemented("Not implemented on web.")}async getInfo(){throw this.unimplemented("Not implemented on web.")}async getLaunchUrl(){return{url:""}}async getState(){return{isActive:document.hidden!==!0}}async minimizeApp(){throw this.unimplemented("Not implemented on web.")}async toggleBackButtonHandler(){throw this.unimplemented("Not implemented on web.")}async getAppLanguage(){return{value:navigator.language.split("-")[0].toLowerCase()}}}export{o as AppWeb};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{cO as a}from"./main-
|
|
1
|
+
import{cO as a}from"./main-Db4or1Jx.js";import{ImpactStyle as i,NotificationType as r}from"./index-BvfNZfX7.js";import"./vendor-react--Eh9ivFN.js";import"./vendor-three-Chj50gSY.js";class h extends a{constructor(){super(...arguments),this.selectionStarted=!1}async impact(t){const e=this.patternForImpact(t==null?void 0:t.style);this.vibrateWithPattern(e)}async notification(t){const e=this.patternForNotification(t==null?void 0:t.type);this.vibrateWithPattern(e)}async vibrate(t){const e=(t==null?void 0:t.duration)||300;this.vibrateWithPattern([e])}async selectionStart(){this.selectionStarted=!0}async selectionChanged(){this.selectionStarted&&this.vibrateWithPattern([70])}async selectionEnd(){this.selectionStarted=!1}patternForImpact(t=i.Heavy){return t===i.Medium?[43]:t===i.Light?[20]:[61]}patternForNotification(t=r.Success){return t===r.Warning?[30,40,30,50,60]:t===r.Error?[27,45,50]:[35,65,21]}vibrateWithPattern(t){if(navigator.vibrate)navigator.vibrate(t);else throw this.unavailable("Browser does not support the vibrate API")}}export{h as HapticsWeb};
|
package/dist/index.html
CHANGED
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-Db4or1Jx.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react--Eh9ivFN.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-Chj50gSY.js">
|
|
28
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
28
|
+
<link rel="stylesheet" crossorigin href="/assets/main-Ba3wdQK_.css">
|
|
29
29
|
</head>
|
|
30
30
|
<body>
|
|
31
31
|
<div id="app"></div>
|
|
@@ -15,6 +15,12 @@ const log = createLogger('Backend');
|
|
|
15
15
|
// Track tool_use_id to tool_name mapping for matching tool_result events
|
|
16
16
|
// This is a module-level map that persists across parseEvent calls
|
|
17
17
|
const toolUseIdToName = new Map();
|
|
18
|
+
// Marker text of the immediate tool_result the CLI returns when a Task/Agent tool
|
|
19
|
+
// launches in the background (the real result arrives later as a <task-notification>)
|
|
20
|
+
function isBackgroundLaunchStub(content) {
|
|
21
|
+
return content.includes('Async agent launched successfully')
|
|
22
|
+
|| content.includes('The agent is working in the background');
|
|
23
|
+
}
|
|
18
24
|
/**
|
|
19
25
|
* Write prompt content to a temp file for use with --system-prompt-file / --append-system-prompt-file
|
|
20
26
|
* This avoids issues with multiline prompts and shell escaping
|
|
@@ -268,7 +274,7 @@ export class ClaudeBackend {
|
|
|
268
274
|
const message = event.message;
|
|
269
275
|
// Handle array content (tool_result blocks)
|
|
270
276
|
if (Array.isArray(message?.content)) {
|
|
271
|
-
const
|
|
277
|
+
const events = [];
|
|
272
278
|
for (const block of message.content) {
|
|
273
279
|
if (block.type === 'tool_result' && block.tool_use_id) {
|
|
274
280
|
// Prefer tool_use_result.stdout (raw output) over block.content (may be truncated)
|
|
@@ -288,6 +294,16 @@ export class ClaudeBackend {
|
|
|
288
294
|
// Look up the tool name from the tool_use_id mapping
|
|
289
295
|
const toolName = toolUseIdToName.get(block.tool_use_id) || 'unknown';
|
|
290
296
|
log.log(`parseUserEvent: Found tool_result for tool_use_id=${block.tool_use_id}, toolName=${toolName}, content length=${content?.length || 0}, hasToolUseResult=${!!event.tool_use_result}`);
|
|
297
|
+
// Background Task launch stub: the CLI answers the tool_use immediately with
|
|
298
|
+
// "Async agent launched..." while the subagent keeps running. Reclassify as
|
|
299
|
+
// task_started so downstream doesn't complete the subagent or idle the agent —
|
|
300
|
+
// the real completion arrives later as a <task-notification> user message.
|
|
301
|
+
if ((toolName === 'Task' || toolName === 'Agent') && isBackgroundLaunchStub(content)) {
|
|
302
|
+
log.log(`parseUserEvent: background launch stub for tool_use_id=${block.tool_use_id} — task still running`);
|
|
303
|
+
events.push({ type: 'task_started', toolUseId: block.tool_use_id });
|
|
304
|
+
toolUseIdToName.delete(block.tool_use_id);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
291
307
|
const toolResult = {
|
|
292
308
|
type: 'tool_result',
|
|
293
309
|
toolName,
|
|
@@ -310,14 +326,29 @@ export class ClaudeBackend {
|
|
|
310
326
|
if (event.parent_tool_use_id) {
|
|
311
327
|
toolResult.parentToolUseId = event.parent_tool_use_id;
|
|
312
328
|
}
|
|
313
|
-
|
|
329
|
+
events.push(toolResult);
|
|
314
330
|
// Clean up the mapping after use (tool_use_id is unique per invocation)
|
|
315
331
|
toolUseIdToName.delete(block.tool_use_id);
|
|
316
332
|
}
|
|
317
333
|
}
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
334
|
+
// Background task completions: the CLI wakes the model with a user message
|
|
335
|
+
// containing <task-notification> blocks (one per finished background task).
|
|
336
|
+
for (const block of message.content) {
|
|
337
|
+
if (block.type !== 'text' || typeof block.text !== 'string' || !block.text.includes('<task-notification>')) {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
for (const chunk of block.text.match(/<task-notification>[\s\S]*?(?:<\/task-notification>|$)/g) ?? []) {
|
|
341
|
+
const toolUseId = /<tool-use-id>([^<]+)<\/tool-use-id>/.exec(chunk)?.[1];
|
|
342
|
+
const taskId = /<task-id>([^<]+)<\/task-id>/.exec(chunk)?.[1];
|
|
343
|
+
if (toolUseId) {
|
|
344
|
+
log.log(`parseUserEvent: task_notification for tool_use_id=${toolUseId} (task_id=${taskId || 'unknown'})`);
|
|
345
|
+
events.push({ type: 'task_notification', toolUseId, taskId });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
if (events.length > 0) {
|
|
350
|
+
log.log(`parseUserEvent: Extracted ${events.length} event(s)`);
|
|
351
|
+
return events.length === 1 ? events[0] : events;
|
|
321
352
|
}
|
|
322
353
|
}
|
|
323
354
|
// Check for local-command-stdout (from /context, /cost, /usage etc. commands)
|
|
@@ -354,9 +385,28 @@ export class ClaudeBackend {
|
|
|
354
385
|
errorMessage: event.error,
|
|
355
386
|
};
|
|
356
387
|
}
|
|
357
|
-
//
|
|
388
|
+
// Background Task launched (links task_id to the Task tool_use_id). The tool_result
|
|
389
|
+
// that follows immediately is only a launch stub — the task keeps running until a
|
|
390
|
+
// <task-notification> user message arrives.
|
|
358
391
|
if (event.subtype === 'task_started' && event.task_id) {
|
|
359
392
|
log.log(`parseSystemEvent: task_started - task_id=${event.task_id}, tool_use_id=${event.tool_use_id}`);
|
|
393
|
+
if (event.tool_use_id) {
|
|
394
|
+
return {
|
|
395
|
+
type: 'task_started',
|
|
396
|
+
taskId: event.task_id,
|
|
397
|
+
toolUseId: event.tool_use_id,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
// Background task finished (fires for async Task/Agent launches and for slow Bash
|
|
402
|
+
// commands the CLI promotes to tasks) — resolves pending-task tracking.
|
|
403
|
+
if (event.subtype === 'task_notification' && (event.tool_use_id || event.task_id)) {
|
|
404
|
+
log.log(`parseSystemEvent: task_notification - task_id=${event.task_id}, tool_use_id=${event.tool_use_id}`);
|
|
405
|
+
return {
|
|
406
|
+
type: 'task_notification',
|
|
407
|
+
taskId: event.task_id,
|
|
408
|
+
toolUseId: event.tool_use_id,
|
|
409
|
+
};
|
|
360
410
|
}
|
|
361
411
|
// Context compaction status
|
|
362
412
|
if (event.subtype === 'status' && event.status === 'compacting') {
|
|
@@ -85,6 +85,10 @@ export class RunnerStdoutPipeline {
|
|
|
85
85
|
}
|
|
86
86
|
const eventOrEvents = this.backend.parseEvent(rawEvent);
|
|
87
87
|
if (!eventOrEvents) {
|
|
88
|
+
// Even unmapped events (task_progress, thinking_tokens, ...) prove the CLI is
|
|
89
|
+
// alive and mid-work — keep the watchdog's activity clock fresh so a session
|
|
90
|
+
// waiting on background tasks isn't idle-killed or flagged as stuck.
|
|
91
|
+
this.bus.emit({ type: 'runner.activity', agentId, timestamp: Date.now() });
|
|
88
92
|
return;
|
|
89
93
|
}
|
|
90
94
|
if (Array.isArray(eventOrEvents)) {
|
|
@@ -2,6 +2,7 @@ import { createLogger } from '../../utils/logger.js';
|
|
|
2
2
|
import { isProcessRunning } from '../../data/index.js';
|
|
3
3
|
import { hasTmuxSession, isTmuxPaneCommandAlive, killTmuxSession } from './tmux-helper.js';
|
|
4
4
|
import * as agentService from '../../services/agent-service.js';
|
|
5
|
+
import { clearPendingBackgroundTasks } from '../../services/runtime-subagents.js';
|
|
5
6
|
import { getTmuxIdleTimeoutMs } from '../../services/system-prompt-service.js';
|
|
6
7
|
const log = createLogger('Runner');
|
|
7
8
|
const MAX_DEATH_HISTORY = 50;
|
|
@@ -10,6 +11,13 @@ const MAX_DEATH_HISTORY = 50;
|
|
|
10
11
|
// can hang for many minutes without an exit signal) and SIGKILL it so the normal
|
|
11
12
|
// process_closed → maybeAutoRestart path can respawn with session resume.
|
|
12
13
|
const IDLE_RESPAWN_MS = Number.parseInt(process.env.TIDE_IDLE_RESPAWN_MS ?? '', 10) || 180_000;
|
|
14
|
+
// A 'working' agent whose turn already ended (waiting_for_input) and whose stream has
|
|
15
|
+
// been silent this long is stuck — a background-task completion signal we missed
|
|
16
|
+
// (killed task, CLI wording change) or a stale status restored across a server
|
|
17
|
+
// restart. Reconcile to idle. Live background tasks emit task_progress lines that
|
|
18
|
+
// count as activity, and message sends flip turnState to 'processing', so neither
|
|
19
|
+
// can trip this.
|
|
20
|
+
const STUCK_WORKING_RECONCILE_MS = Number.parseInt(process.env.TIDE_STUCK_WORKING_RECONCILE_MS ?? '', 10) || 180_000;
|
|
13
21
|
export class RunnerWatchdog {
|
|
14
22
|
activeProcesses;
|
|
15
23
|
lastStderr;
|
|
@@ -28,6 +36,7 @@ export class RunnerWatchdog {
|
|
|
28
36
|
const idleTimeoutMs = getTmuxIdleTimeoutMs();
|
|
29
37
|
const now = Date.now();
|
|
30
38
|
for (const [agentId, activeProcess] of this.activeProcesses) {
|
|
39
|
+
this.reconcileStuckWorking(agentId, activeProcess, now);
|
|
31
40
|
// tmux mode: the launcher PID exits immediately — check the tmux session instead
|
|
32
41
|
if (activeProcess.tmuxSession) {
|
|
33
42
|
const sessionAlive = hasTmuxSession(agentId);
|
|
@@ -146,6 +155,31 @@ export class RunnerWatchdog {
|
|
|
146
155
|
}
|
|
147
156
|
}
|
|
148
157
|
}
|
|
158
|
+
// Safety net for stuck 'working' status: the turn ended, no events are flowing,
|
|
159
|
+
// yet the agent still shows working. Flip it to idle and drop any pending
|
|
160
|
+
// background-task tracking so the tmux idle-timeout can later reclaim the session.
|
|
161
|
+
reconcileStuckWorking(agentId, activeProcess, now) {
|
|
162
|
+
if (activeProcess.turnState !== 'waiting_for_input') {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const lastActivity = activeProcess.lastActivityTime ?? activeProcess.startTime;
|
|
166
|
+
if (now - lastActivity < STUCK_WORKING_RECONCILE_MS) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const agent = agentService.getAgent(agentId);
|
|
170
|
+
if (agent?.status !== 'working') {
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const silentSec = Math.round((now - lastActivity) / 1000);
|
|
174
|
+
log.error(`🐕 [WATCHDOG] Agent ${agentId}: status 'working' but turn ended and stream silent for ${silentSec}s `
|
|
175
|
+
+ `(threshold=${STUCK_WORKING_RECONCILE_MS / 1000}s) — reconciling to idle and clearing pending background tasks`);
|
|
176
|
+
clearPendingBackgroundTasks(agentId);
|
|
177
|
+
agentService.updateAgent(agentId, {
|
|
178
|
+
status: 'idle',
|
|
179
|
+
currentTask: undefined,
|
|
180
|
+
currentTool: undefined,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
149
183
|
recordDeath(info) {
|
|
150
184
|
this.recentDeaths.unshift(info);
|
|
151
185
|
if (this.recentDeaths.length > MAX_DEATH_HISTORY) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { loadSession } from '../claude/session-loader.js';
|
|
2
2
|
import * as agentService from './agent-service.js';
|
|
3
3
|
import { clearPendingSilentContextRefresh, consumeStepCompleteReceived, markStepCompleteReceived, } from './runtime-watchdog.js';
|
|
4
|
-
import { handleTaskToolResult, handleTaskToolStart } from './runtime-subagents.js';
|
|
4
|
+
import { addPendingBackgroundTask, clearPendingBackgroundTasks, getActiveSubagentByToolUseId, handleTaskToolResult, handleTaskToolStart, hasPendingBackgroundTasks, resolvePendingBackgroundTask, resolvePendingBackgroundTaskByTaskId, } from './runtime-subagents.js';
|
|
5
5
|
const DEFAULT_CLAUDE_CONTEXT_WINDOW = 200000;
|
|
6
6
|
const DEFAULT_CODEX_CONTEXT_WINDOW = 258400;
|
|
7
7
|
const CODEX_ROLLING_CONTEXT_TURNS = 40;
|
|
@@ -173,8 +173,56 @@ export function createRuntimeEventHandlers(deps) {
|
|
|
173
173
|
break;
|
|
174
174
|
case 'tool_result':
|
|
175
175
|
handleTaskToolResult(agentId, event, log);
|
|
176
|
+
if (event.toolUseId) {
|
|
177
|
+
// Any tool_result for a tracked toolUseId means that task delivered its
|
|
178
|
+
// result. The CLI assigns task_ids to slow Bash commands too, so resolution
|
|
179
|
+
// must NOT be limited to Task/Agent tools — that gap pinned agents 'working'.
|
|
180
|
+
resolvePendingBackgroundTask(agentId, event.toolUseId);
|
|
181
|
+
}
|
|
176
182
|
agentService.updateAgent(agentId, { currentTool: undefined });
|
|
177
183
|
break;
|
|
184
|
+
case 'task_started':
|
|
185
|
+
// Tool promoted to a background task (async Task/Agent, slow Bash): it can
|
|
186
|
+
// outlive the parent turn, so step_complete must not flip the agent idle
|
|
187
|
+
// while it runs.
|
|
188
|
+
if (event.toolUseId) {
|
|
189
|
+
addPendingBackgroundTask(agentId, event.toolUseId, event.taskId);
|
|
190
|
+
log.log(`[task] Background task pending for ${agentId}: toolUseId=${event.toolUseId}${event.taskId ? `, taskId=${event.taskId}` : ''}`);
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case 'task_notification': {
|
|
194
|
+
// A background task finished and the CLI is waking the model with its result.
|
|
195
|
+
if (!event.toolUseId && event.taskId) {
|
|
196
|
+
resolvePendingBackgroundTaskByTaskId(agentId, event.taskId);
|
|
197
|
+
}
|
|
198
|
+
if (event.toolUseId) {
|
|
199
|
+
resolvePendingBackgroundTask(agentId, event.toolUseId);
|
|
200
|
+
// Complete the subagent now (its launch-stub tool_result was suppressed).
|
|
201
|
+
if (getActiveSubagentByToolUseId(event.toolUseId)) {
|
|
202
|
+
const completion = {
|
|
203
|
+
type: 'tool_result',
|
|
204
|
+
toolName: 'Agent',
|
|
205
|
+
toolUseId: event.toolUseId,
|
|
206
|
+
};
|
|
207
|
+
handleTaskToolResult(agentId, completion, log);
|
|
208
|
+
emitEvent(agentId, completion);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (processActive && agent.status === 'idle') {
|
|
212
|
+
agentService.updateAgent(agentId, { status: 'working' });
|
|
213
|
+
}
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
case 'text':
|
|
217
|
+
case 'thinking':
|
|
218
|
+
// The model produced output — it's working, even right after a step_complete
|
|
219
|
+
// (background-task wakes often start with text/thinking, not a tool call).
|
|
220
|
+
// Subagent-bridged events (parentToolUseId set) don't count: the JSONL watcher
|
|
221
|
+
// can drain trailing lines after the parent is genuinely done.
|
|
222
|
+
if (processActive && agent.status === 'idle' && !event.parentToolUseId) {
|
|
223
|
+
agentService.updateAgent(agentId, { status: 'working' });
|
|
224
|
+
}
|
|
225
|
+
break;
|
|
178
226
|
case 'usage_snapshot': {
|
|
179
227
|
// Skip subagent events — their token counts reflect the subagent's context,
|
|
180
228
|
// not the parent's. Updating the parent's contextUsed here would corrupt it.
|
|
@@ -356,14 +404,26 @@ export function createRuntimeEventHandlers(deps) {
|
|
|
356
404
|
}
|
|
357
405
|
agentService.updateAgent(agentId, updates);
|
|
358
406
|
if (!isCodexProvider && !isOpencodeProvider) {
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
407
|
+
if (hasPendingBackgroundTasks(agentId)) {
|
|
408
|
+
// The turn ended but background tasks are still running — the CLI will wake
|
|
409
|
+
// the model with a <task-notification> when they finish. Not idle yet.
|
|
410
|
+
log.log(`[step_complete] Agent ${agentId} has pending background task(s) — keeping status '${agent.status}'`);
|
|
411
|
+
agentService.updateAgent(agentId, { currentTool: undefined });
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
setTimeout(() => {
|
|
415
|
+
if (hasPendingBackgroundTasks(agentId)) {
|
|
416
|
+
log.log(`[step_complete] Agent ${agentId} gained pending background task(s) — skipping idle`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
log.log(`[step_complete] Setting status to idle for agent ${agentId} (lastTask: ${agent.lastAssignedTask})`);
|
|
420
|
+
agentService.updateAgent(agentId, {
|
|
421
|
+
status: 'idle',
|
|
422
|
+
currentTask: undefined,
|
|
423
|
+
currentTool: undefined,
|
|
424
|
+
});
|
|
425
|
+
}, 200);
|
|
426
|
+
}
|
|
367
427
|
}
|
|
368
428
|
else {
|
|
369
429
|
log.log(`[step_complete] ${isCodexProvider ? 'Codex' : 'OpenCode'} agent ${agentId} will be set idle on process completion`);
|
|
@@ -374,6 +434,9 @@ export function createRuntimeEventHandlers(deps) {
|
|
|
374
434
|
break;
|
|
375
435
|
}
|
|
376
436
|
case 'error':
|
|
437
|
+
// Background tasks are children of the CLI process — an errored run means
|
|
438
|
+
// they're gone; a stale pending set would pin the agent 'working' forever.
|
|
439
|
+
clearPendingBackgroundTasks(agentId);
|
|
377
440
|
agentService.updateAgent(agentId, { status: 'error' });
|
|
378
441
|
break;
|
|
379
442
|
case 'context_stats':
|
|
@@ -401,6 +464,8 @@ export function createRuntimeEventHandlers(deps) {
|
|
|
401
464
|
}
|
|
402
465
|
}
|
|
403
466
|
function handleComplete(agentId, success) {
|
|
467
|
+
// Process closed — any background tasks died with it.
|
|
468
|
+
clearPendingBackgroundTasks(agentId);
|
|
404
469
|
const receivedStepComplete = consumeStepCompleteReceived(agentId);
|
|
405
470
|
const agent = agentService.getAgent(agentId);
|
|
406
471
|
const isCodexProvider = (agent?.provider ?? 'claude') === 'codex';
|
|
@@ -438,6 +503,8 @@ export function createRuntimeEventHandlers(deps) {
|
|
|
438
503
|
}
|
|
439
504
|
}
|
|
440
505
|
function handleError(agentId, error) {
|
|
506
|
+
// Same as 'error' events: a failed run takes its background tasks with it.
|
|
507
|
+
clearPendingBackgroundTasks(agentId);
|
|
441
508
|
const agent = agentService.getAgent(agentId);
|
|
442
509
|
const timestamp = new Date().toISOString();
|
|
443
510
|
const isCodexProvider = (agent?.provider ?? 'claude') === 'codex';
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
let subagentCounter = 0;
|
|
2
2
|
const activeSubagents = new Map();
|
|
3
3
|
const subagentIdToToolUseId = new Map();
|
|
4
|
+
// Background tasks: the CLI assigns task_ids to async Task/Agent launches AND to slow
|
|
5
|
+
// Bash commands. The parent turn can end while these still run — the CLI wakes the
|
|
6
|
+
// model with a task-notification when each finishes. While any are pending the agent
|
|
7
|
+
// must NOT be flipped to idle.
|
|
8
|
+
const pendingBackgroundTasks = new Map(); // parentAgentId -> toolUseId -> taskId
|
|
4
9
|
function generateSubagentId() {
|
|
5
10
|
return `sub_${Date.now().toString(36)}_${(subagentCounter++).toString(36)}`;
|
|
6
11
|
}
|
|
@@ -41,6 +46,42 @@ export function handleTaskToolResult(agentId, event, log) {
|
|
|
41
46
|
activeSubagents.delete(event.toolUseId);
|
|
42
47
|
subagentIdToToolUseId.delete(subagent.id);
|
|
43
48
|
}
|
|
49
|
+
export function addPendingBackgroundTask(agentId, toolUseId, taskId) {
|
|
50
|
+
let tasks = pendingBackgroundTasks.get(agentId);
|
|
51
|
+
if (!tasks) {
|
|
52
|
+
tasks = new Map();
|
|
53
|
+
pendingBackgroundTasks.set(agentId, tasks);
|
|
54
|
+
}
|
|
55
|
+
tasks.set(toolUseId, taskId);
|
|
56
|
+
}
|
|
57
|
+
export function resolvePendingBackgroundTask(agentId, toolUseId) {
|
|
58
|
+
const tasks = pendingBackgroundTasks.get(agentId);
|
|
59
|
+
if (!tasks?.delete(toolUseId)) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (tasks.size === 0) {
|
|
63
|
+
pendingBackgroundTasks.delete(agentId);
|
|
64
|
+
}
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
export function resolvePendingBackgroundTaskByTaskId(agentId, taskId) {
|
|
68
|
+
const tasks = pendingBackgroundTasks.get(agentId);
|
|
69
|
+
if (!tasks) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
for (const [toolUseId, pendingTaskId] of tasks) {
|
|
73
|
+
if (pendingTaskId === taskId) {
|
|
74
|
+
return resolvePendingBackgroundTask(agentId, toolUseId);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
export function hasPendingBackgroundTasks(agentId) {
|
|
80
|
+
return (pendingBackgroundTasks.get(agentId)?.size ?? 0) > 0;
|
|
81
|
+
}
|
|
82
|
+
export function clearPendingBackgroundTasks(agentId) {
|
|
83
|
+
pendingBackgroundTasks.delete(agentId);
|
|
84
|
+
}
|
|
44
85
|
export function getActiveSubagentByToolUseId(toolUseId) {
|
|
45
86
|
return activeSubagents.get(toolUseId);
|
|
46
87
|
}
|
|
@@ -50,5 +91,6 @@ export function getActiveSubagentsForAgent(parentAgentId) {
|
|
|
50
91
|
export function resetSubagentStateForTests() {
|
|
51
92
|
activeSubagents.clear();
|
|
52
93
|
subagentIdToToolUseId.clear();
|
|
94
|
+
pendingBackgroundTasks.clear();
|
|
53
95
|
subagentCounter = 0;
|
|
54
96
|
}
|