tide-commander 1.136.0 → 1.136.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 (46) hide show
  1. package/dist/assets/{BossLogsModal-B5wtQ9Fh.js → BossLogsModal-B4p16n9B.js} +1 -1
  2. package/dist/assets/{BossSpawnModal-B8twkNNt.js → BossSpawnModal-BHQ7sxeu.js} +1 -1
  3. package/dist/assets/{ControlsModal-BKUVlO45.js → ControlsModal-BcP5-m0h.js} +1 -1
  4. package/dist/assets/{DockerLogsModal-Oa0WYexL.js → DockerLogsModal-C_Gd6YLn.js} +1 -1
  5. package/dist/assets/{EmbeddedEditor-Cnkfr9vJ.js → EmbeddedEditor-GHVeXIn5.js} +1 -1
  6. package/dist/assets/{GmailOAuthSetup-Cfsgqs5D.js → GmailOAuthSetup-VjAKcJf9.js} +1 -1
  7. package/dist/assets/{GoogleOAuthSetup-BZCY7oIc.js → GoogleOAuthSetup-DFbaN0Ry.js} +1 -1
  8. package/dist/assets/{IframeModal-BVaOFzOL.js → IframeModal-BaoVOZTf.js} +1 -1
  9. package/dist/assets/{IntegrationsPanel-CdcwTSOY.js → IntegrationsPanel-Cg4TxOfh.js} +2 -2
  10. package/dist/assets/{LogViewerModal-U0o-ixDu.js → LogViewerModal-CN2GYpgr.js} +1 -1
  11. package/dist/assets/{MonitoringModal-CdYBNEBK.js → MonitoringModal-oN_JjoC2.js} +1 -1
  12. package/dist/assets/{PM2LogsModal-cxSSfOTz.js → PM2LogsModal-BOQ1Im-i.js} +1 -1
  13. package/dist/assets/{RestoreArchivedAreaModal-BkzbecLN.js → RestoreArchivedAreaModal-DYcjqjbA.js} +1 -1
  14. package/dist/assets/{Scene2DCanvas-GGc__FVu.js → Scene2DCanvas-CsBA5ERQ.js} +1 -1
  15. package/dist/assets/{SceneManager-BWyie0tH.js → SceneManager-BqVfBAQc.js} +1 -1
  16. package/dist/assets/{SkillsPanel-DS7QNt0-.js → SkillsPanel-CtfixcgQ.js} +1 -1
  17. package/dist/assets/{SlackMultiInstanceSetup-CjHaygiR.js → SlackMultiInstanceSetup-3nGO_xs6.js} +1 -1
  18. package/dist/assets/{SpawnModal-MWySBWkA.js → SpawnModal-BvUHN-Q1.js} +1 -1
  19. package/dist/assets/{StatisticsModal-D08htYhH.js → StatisticsModal-DIx8l3F0.js} +1 -1
  20. package/dist/assets/{SubordinateAssignmentModal-DULM1xpx.js → SubordinateAssignmentModal-Bw-V4i4-.js} +1 -1
  21. package/dist/assets/{TriggerManagerPanel-nB6R7Nk4.js → TriggerManagerPanel-C2Mz8wVK.js} +1 -1
  22. package/dist/assets/{WorkflowEditorPanel-CKgW8DhB.js → WorkflowEditorPanel-C2RHV8Dx.js} +1 -1
  23. package/dist/assets/{area-logos-CuU1qdkU.js → area-logos-aaW_jqzI.js} +1 -1
  24. package/dist/assets/{index-CLmNQzTF.js → index-B4JG--zt.js} +2 -2
  25. package/dist/assets/{index-CQPpjjTd.js → index-BGz62M0g.js} +1 -1
  26. package/dist/assets/{index-Cc_Ys4_9.js → index-BIUEhCXu.js} +1 -1
  27. package/dist/assets/{index-ih8yuWhL.js → index-BT7u2ftY.js} +1 -1
  28. package/dist/assets/{index-CWYcGGCJ.js → index-BvR0it5l.js} +5 -5
  29. package/dist/assets/{index-8DzLSviG.js → index-BzCUPOjQ.js} +1 -1
  30. package/dist/assets/{index-B6TRH1E2.js → index-CmGwjdPh.js} +1 -1
  31. package/dist/assets/{index-CWiGs6nn.js → index-D6r1xAEg.js} +1 -1
  32. package/dist/assets/{index-Csnk6THj.js → index-DidVGiaJ.js} +2 -2
  33. package/dist/assets/main-Ba3wdQK_.css +1 -0
  34. package/dist/assets/{main-CHE4wC7P.js → main-rDQDtv_U.js} +115 -115
  35. package/dist/assets/{web-C4yrL2Hp.js → web-B5Ma_-eu.js} +1 -1
  36. package/dist/assets/{web-D1Ur_Mtd.js → web-DojkIxYY.js} +1 -1
  37. package/dist/assets/{web-dmnfXY_q.js → web-Dy1328BB.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/routes/files.js +297 -92
  43. package/dist/src/packages/server/services/runtime-events.js +76 -9
  44. package/dist/src/packages/server/services/runtime-subagents.js +42 -0
  45. package/package.json +1 -1
  46. package/dist/assets/main-DM0iS4EN.css +0 -1
@@ -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-rDQDtv_U.js";import{ImpactStyle as i,NotificationType as r}from"./index-B4JG--zt.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 +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-rDQDtv_U.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 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-rDQDtv_U.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};
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-rDQDtv_U.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) {