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.
Files changed (45) hide show
  1. package/dist/assets/{BossLogsModal-B5wtQ9Fh.js → BossLogsModal-ed7L3S97.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-B8twkNNt.js → BossSpawnModal-C-KkKRgI.js} +1 -1
  3. package/dist/assets/{ControlsModal-BKUVlO45.js → ControlsModal-DBJPuS6z.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-Oa0WYexL.js → DockerLogsModal-D8lPR5BA.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-Cnkfr9vJ.js → EmbeddedEditor-D5opmQLi.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-Cfsgqs5D.js → GmailOAuthSetup-CGhj8CNw.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-BZCY7oIc.js → GoogleOAuthSetup-Br7IfgCD.js} +1 -1
  8. package/dist/assets/{IframeModal-BVaOFzOL.js → IframeModal-EOV4pjTD.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CdcwTSOY.js → IntegrationsPanel-BJED0ToF.js} +2 -2
  10. package/dist/assets/{LogViewerModal-U0o-ixDu.js → LogViewerModal-BY1nfMIb.js} +1 -1
  11. package/dist/assets/{MonitoringModal-CdYBNEBK.js → MonitoringModal-C88vTxw-.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-cxSSfOTz.js → PM2LogsModal-CVbr9x3j.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-BkzbecLN.js → RestoreArchivedAreaModal-Du4e4ebk.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-GGc__FVu.js → Scene2DCanvas-DW6-BjZg.js} +1 -1
  15. package/dist/assets/{SceneManager-BWyie0tH.js → SceneManager-DFaHLv0b.js} +1 -1
  16. package/dist/assets/{SkillsPanel-DS7QNt0-.js → SkillsPanel-R6txFGjx.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-CjHaygiR.js → SlackMultiInstanceSetup-Cd7eklY5.js} +1 -1
  18. package/dist/assets/{SpawnModal-MWySBWkA.js → SpawnModal-qJUC6gUU.js} +1 -1
  19. package/dist/assets/{StatisticsModal-D08htYhH.js → StatisticsModal-BVacMzYw.js} +1 -1
  20. package/dist/assets/{SubordinateAssignmentModal-DULM1xpx.js → SubordinateAssignmentModal-BM4JvNyT.js} +1 -1
  21. package/dist/assets/{TriggerManagerPanel-nB6R7Nk4.js → TriggerManagerPanel-CHhTlUF3.js} +1 -1
  22. package/dist/assets/{WorkflowEditorPanel-CKgW8DhB.js → WorkflowEditorPanel-C_V9Xmy8.js} +1 -1
  23. package/dist/assets/{area-logos-CuU1qdkU.js → area-logos-CcnztkrB.js} +1 -1
  24. package/dist/assets/{index-Cc_Ys4_9.js → index-47gQB8Y5.js} +1 -1
  25. package/dist/assets/{index-Csnk6THj.js → index-Bd05QMt_.js} +1 -1
  26. package/dist/assets/{index-CLmNQzTF.js → index-BvfNZfX7.js} +2 -2
  27. package/dist/assets/{index-B6TRH1E2.js → index-BvlPsF-d.js} +1 -1
  28. package/dist/assets/{index-ih8yuWhL.js → index-CF55uz6s.js} +1 -1
  29. package/dist/assets/{index-CWiGs6nn.js → index-CMP6SUq-.js} +1 -1
  30. package/dist/assets/{index-CQPpjjTd.js → index-DKbbtwcq.js} +1 -1
  31. package/dist/assets/{index-8DzLSviG.js → index-DXbpyN-P.js} +1 -1
  32. package/dist/assets/{index-CWYcGGCJ.js → index-DwcZd659.js} +3 -3
  33. package/dist/assets/main-Ba3wdQK_.css +1 -0
  34. package/dist/assets/{main-CHE4wC7P.js → main-Db4or1Jx.js} +42 -42
  35. package/dist/assets/{web-dmnfXY_q.js → web-BahzYw5F.js} +1 -1
  36. package/dist/assets/{web-D1Ur_Mtd.js → web-BnBWe9ee.js} +1 -1
  37. package/dist/assets/{web-C4yrL2Hp.js → web-BsXkVdvj.js} +1 -1
  38. package/dist/index.html +2 -2
  39. package/dist/src/packages/server/claude/backend.js +56 -6
  40. package/dist/src/packages/server/claude/runner/stdout-pipeline.js +4 -0
  41. package/dist/src/packages/server/claude/runner/watchdog.js +34 -0
  42. package/dist/src/packages/server/services/runtime-events.js +76 -9
  43. package/dist/src/packages/server/services/runtime-subagents.js +42 -0
  44. package/package.json +1 -1
  45. package/dist/assets/main-DM0iS4EN.css +0 -1
@@ -1 +1 @@
1
- import{cO as s}from"./main-CHE4wC7P.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
+ 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-CHE4wC7P.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
+ 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-CHE4wC7P.js";import{ImpactStyle as i,NotificationType as r}from"./index-CLmNQzTF.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};
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-CHE4wC7P.js"></script>
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-DM0iS4EN.css">
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 toolResults = [];
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
- toolResults.push(toolResult);
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
- if (toolResults.length > 0) {
319
- log.log(`parseUserEvent: Extracted ${toolResults.length} tool_result(s)`);
320
- return toolResults.length === 1 ? toolResults[0] : toolResults;
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
- // Capture task_started events (links task_id to tool_use_id)
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
- setTimeout(() => {
360
- log.log(`[step_complete] Setting status to idle for agent ${agentId} (lastTask: ${agent.lastAssignedTask})`);
361
- agentService.updateAgent(agentId, {
362
- status: 'idle',
363
- currentTask: undefined,
364
- currentTool: undefined,
365
- });
366
- }, 200);
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tide-commander",
3
- "version": "1.136.0",
3
+ "version": "1.136.1",
4
4
  "description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
5
5
  "repository": {
6
6
  "type": "git",