tide-commander 1.134.2 → 1.135.0
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-DNQC-Ewi.js → BossLogsModal-Br87ujFN.js} +1 -1
- package/dist/assets/{BossSpawnModal-DSpPdHfB.js → BossSpawnModal-BSMOABpB.js} +1 -1
- package/dist/assets/{ControlsModal-TPM0WTd7.js → ControlsModal-Lo2IEp3b.js} +1 -1
- package/dist/assets/{DockerLogsModal-BsEfK9sY.js → DockerLogsModal-BQCbTBC6.js} +1 -1
- package/dist/assets/{EmbeddedEditor-B7pzeHYJ.js → EmbeddedEditor-D9eEVnzO.js} +1 -1
- package/dist/assets/{GmailOAuthSetup-BD0lLD6-.js → GmailOAuthSetup-B-JJpMcx.js} +1 -1
- package/dist/assets/{GoogleOAuthSetup-q_I-vMvg.js → GoogleOAuthSetup-DOaKpXDn.js} +1 -1
- package/dist/assets/{IframeModal-C0-b5Igy.js → IframeModal-DmQHTIE4.js} +1 -1
- package/dist/assets/{IntegrationsPanel-BPWcucBR.js → IntegrationsPanel-BpksOUNm.js} +2 -2
- package/dist/assets/{LogViewerModal-D1puFD4z.js → LogViewerModal-Bl3pulKG.js} +1 -1
- package/dist/assets/{MonitoringModal-Dy3YmxW7.js → MonitoringModal-F2dTkAkd.js} +1 -1
- package/dist/assets/{PM2LogsModal-DFNTztfk.js → PM2LogsModal-ByjjtKA0.js} +1 -1
- package/dist/assets/{RestoreArchivedAreaModal-CokHqFKe.js → RestoreArchivedAreaModal-7InRrE9D.js} +1 -1
- package/dist/assets/{Scene2DCanvas-Dr1l-tVX.js → Scene2DCanvas-Cp0oJYCK.js} +1 -1
- package/dist/assets/{SceneManager-IcoT7Cdk.js → SceneManager-CFIVQcN9.js} +1 -1
- package/dist/assets/{SkillsPanel-Cc1zvnfm.js → SkillsPanel-BkAjah7F.js} +1 -1
- package/dist/assets/{SlackMultiInstanceSetup-Cjva8TJX.js → SlackMultiInstanceSetup-CtAuHpvV.js} +1 -1
- package/dist/assets/{SpawnModal-DHKupuXe.js → SpawnModal-jq2ZNr65.js} +1 -1
- package/dist/assets/{StatisticsModal-DZXDkn4d.js → StatisticsModal-CpSHUtjK.js} +1 -1
- package/dist/assets/{SubordinateAssignmentModal-DLJNIySK.js → SubordinateAssignmentModal-CQrYwrWx.js} +1 -1
- package/dist/assets/{TriggerManagerPanel-Bd5ZGBaw.js → TriggerManagerPanel-BlY6_f82.js} +1 -1
- package/dist/assets/{WorkflowEditorPanel-D1YylR-A.js → WorkflowEditorPanel-E3beq7xc.js} +1 -1
- package/dist/assets/{area-logos-C1EKS2S6.js → area-logos-DRiHOWlH.js} +1 -1
- package/dist/assets/{index-IPKznDm8.js → index-3VUne2iz.js} +3 -3
- package/dist/assets/{index-0yPqh1QY.js → index-4qjPP76Y.js} +1 -1
- package/dist/assets/{index-BLrXnQWB.js → index-BGmOKITB.js} +1 -1
- package/dist/assets/{index-C87bCtYJ.js → index-BwRLsIIz.js} +1 -1
- package/dist/assets/{index-7EfWB7v9.js → index-C0Gc95WG.js} +2 -2
- package/dist/assets/{index-BmUoFUZP.js → index-Ct7T36S2.js} +1 -1
- package/dist/assets/{index-Ch70UsM8.js → index-nriTUh3-.js} +1 -1
- package/dist/assets/{index-E4hM-HPm.js → index-rcSRlYQ_.js} +1 -1
- package/dist/assets/{index-_wxXq7jG.js → index-sJsQ8jvb.js} +1 -1
- package/dist/assets/{main-DhXGHPKx.js → main-BLcDlWN7.js} +111 -111
- package/dist/assets/main-DM0iS4EN.css +1 -0
- package/dist/assets/{web-g3yRdGTs.js → web-9Nmbqn9e.js} +1 -1
- package/dist/assets/{web-CHTqiViU.js → web-B6VItc9v.js} +1 -1
- package/dist/assets/{web-CDTjrhUg.js → web-BFRtScY4.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/locales/en/terminal.json +13 -0
- package/dist/src/packages/server/claude/interactive/interactive-runner.js +8 -0
- package/dist/src/packages/server/claude/runner/recovery-store.js +102 -6
- package/dist/src/packages/server/claude/runner.js +30 -10
- package/dist/src/packages/server/data/index.js +1 -0
- package/dist/src/packages/server/services/auto-collapse-service.js +8 -2
- package/dist/src/packages/server/services/collapse-context.js +52 -12
- package/dist/src/packages/server/services/subagent-jsonl-watcher.js +112 -82
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +3 -0
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +40 -30
- package/package.json +1 -1
- package/dist/assets/main-rrlO1Py8.css +0 -1
|
@@ -19,9 +19,12 @@ import { createLogger } from '../utils/logger.js';
|
|
|
19
19
|
import { encodeProjectPath } from '../claude/session-loader.js';
|
|
20
20
|
const log = createLogger('SubagentJSONL');
|
|
21
21
|
const MAX_ENTRIES_PER_BROADCAST = 20;
|
|
22
|
-
const IDLE_TIMEOUT_MS =
|
|
22
|
+
const IDLE_TIMEOUT_MS = 600_000; // Stop after 10 minutes of no file changes (long thinking/tool gaps on xhigh effort)
|
|
23
23
|
const POLL_INTERVAL_MS = 2_000; // Poll file every 2s (fallback if fs.watch misses events)
|
|
24
|
-
const MAX_WATCH_DURATION_MS =
|
|
24
|
+
const MAX_WATCH_DURATION_MS = 3_600_000; // Hard limit: 60 minutes max per watcher (background agents routinely run 20-30 min)
|
|
25
|
+
const BIND_POLL_INTERVAL_MS = 1_000; // Retry transcript binding every second until found
|
|
26
|
+
const BIND_GIVE_UP_MS = 120_000; // Stop an unbound watcher after 2 minutes (no transcript ever appeared)
|
|
27
|
+
const FALLBACK_BIND_GRACE_MS = 15_000; // Give meta.json time to appear before the newest-file fallback
|
|
25
28
|
// Key param extraction per tool name
|
|
26
29
|
const TOOL_KEY_PARAMS = {
|
|
27
30
|
Bash: 'command',
|
|
@@ -48,7 +51,7 @@ export function getSubagentsDir(cwd, sessionId) {
|
|
|
48
51
|
/**
|
|
49
52
|
* Start watching for a subagent's JSONL file
|
|
50
53
|
*/
|
|
51
|
-
export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcast, onToolResult) {
|
|
54
|
+
export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcast, onToolResult, options) {
|
|
52
55
|
if (activeWatchers.has(toolUseId)) {
|
|
53
56
|
log.warn(`[Watcher] Already watching for toolUseId=${toolUseId}`);
|
|
54
57
|
return;
|
|
@@ -57,6 +60,8 @@ export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcas
|
|
|
57
60
|
toolUseId,
|
|
58
61
|
parentAgentId,
|
|
59
62
|
subagentsDir,
|
|
63
|
+
startedAt: Date.now(),
|
|
64
|
+
startAtEnd: options?.startAtEnd === true,
|
|
60
65
|
readPosition: 0,
|
|
61
66
|
lineBuffer: '',
|
|
62
67
|
pendingEntries: [],
|
|
@@ -76,6 +81,10 @@ export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcas
|
|
|
76
81
|
// Try to find existing files first, then watch for new ones
|
|
77
82
|
tryFindAndWatchFile(watcher);
|
|
78
83
|
}
|
|
84
|
+
/** Whether a live watcher exists for this Task/Agent toolUseId. */
|
|
85
|
+
export function isWatching(toolUseId) {
|
|
86
|
+
return activeWatchers.has(toolUseId);
|
|
87
|
+
}
|
|
79
88
|
/**
|
|
80
89
|
* Signal that the subagent's Task tool has completed.
|
|
81
90
|
* For team agents, the Task returns immediately but the subagent keeps running.
|
|
@@ -105,6 +114,7 @@ function doStop(watcher) {
|
|
|
105
114
|
}
|
|
106
115
|
flushEntries(watcher);
|
|
107
116
|
// Cleanup all timers and watchers
|
|
117
|
+
clearBindTimer(watcher);
|
|
108
118
|
watcher.dirWatcher?.close();
|
|
109
119
|
watcher.fileWatcher?.close();
|
|
110
120
|
if (watcher.broadcastTimer)
|
|
@@ -138,91 +148,42 @@ function resetIdleTimer(watcher) {
|
|
|
138
148
|
}, IDLE_TIMEOUT_MS);
|
|
139
149
|
}
|
|
140
150
|
/**
|
|
141
|
-
*
|
|
151
|
+
* Bind this watcher to its subagent's transcript inside the shared
|
|
152
|
+
* subagents/ directory, retrying until it appears.
|
|
153
|
+
*
|
|
154
|
+
* Exact binding: each subagent writes agent-<id>.meta.json containing the
|
|
155
|
+
* parent Task/Agent toolUseId — match it against ours. The old "newest
|
|
156
|
+
* .jsonl in the dir" heuristic mis-binds when several subagents run
|
|
157
|
+
* concurrently: at tool_start time the newest file usually belongs to a
|
|
158
|
+
* sibling (this subagent hasn't written anything yet), so this subagent's
|
|
159
|
+
* tool_results never bridge and its Bash cards spin forever.
|
|
142
160
|
*/
|
|
143
161
|
function tryFindAndWatchFile(watcher) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
const retryInterval = setInterval(() => {
|
|
152
|
-
if (watcher.stopped) {
|
|
153
|
-
clearInterval(retryInterval);
|
|
154
|
-
return;
|
|
155
|
-
}
|
|
156
|
-
if (fs.existsSync(subagentsDir)) {
|
|
157
|
-
clearInterval(retryInterval);
|
|
158
|
-
watchDirectory(watcher);
|
|
159
|
-
}
|
|
160
|
-
}, 500);
|
|
161
|
-
// Give up after 30 seconds
|
|
162
|
-
setTimeout(() => clearInterval(retryInterval), 30000);
|
|
162
|
+
if (tryBindTranscript(watcher))
|
|
163
|
+
return;
|
|
164
|
+
// Retry on a timer — also covers the "session/subagents directory doesn't
|
|
165
|
+
// exist yet" case (readdir fails → keep polling).
|
|
166
|
+
watcher.bindTimer = setInterval(() => {
|
|
167
|
+
if (watcher.stopped) {
|
|
168
|
+
clearBindTimer(watcher);
|
|
163
169
|
return;
|
|
164
170
|
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const parentWatcher = fs.watch(parentDir, (eventType, filename) => {
|
|
168
|
-
if (watcher.stopped)
|
|
169
|
-
return;
|
|
170
|
-
if (filename === 'subagents' && fs.existsSync(subagentsDir)) {
|
|
171
|
-
parentWatcher.close();
|
|
172
|
-
watchDirectory(watcher);
|
|
173
|
-
}
|
|
174
|
-
});
|
|
175
|
-
parentWatcher.on('error', () => parentWatcher.close());
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
log.warn(`[Watcher] Failed to watch parent dir: ${parentDir}`);
|
|
179
|
-
}
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
// Directory exists - look for existing files or watch for new ones
|
|
183
|
-
watchDirectory(watcher);
|
|
184
|
-
}
|
|
185
|
-
/**
|
|
186
|
-
* Watch the subagents directory for JSONL files
|
|
187
|
-
*/
|
|
188
|
-
function watchDirectory(watcher) {
|
|
189
|
-
if (watcher.stopped)
|
|
190
|
-
return;
|
|
191
|
-
const { subagentsDir } = watcher;
|
|
192
|
-
// Check for existing .jsonl files
|
|
193
|
-
try {
|
|
194
|
-
const files = fs.readdirSync(subagentsDir).filter(f => f.endsWith('.jsonl'));
|
|
195
|
-
if (files.length > 0) {
|
|
196
|
-
// Pick the most recently modified file
|
|
197
|
-
let newest = files[0];
|
|
198
|
-
let newestMtime = 0;
|
|
199
|
-
for (const f of files) {
|
|
200
|
-
try {
|
|
201
|
-
const stat = fs.statSync(path.join(subagentsDir, f));
|
|
202
|
-
if (stat.mtimeMs > newestMtime) {
|
|
203
|
-
newestMtime = stat.mtimeMs;
|
|
204
|
-
newest = f;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch { /* skip */ }
|
|
208
|
-
}
|
|
209
|
-
startFileWatch(watcher, path.join(subagentsDir, newest));
|
|
171
|
+
if (tryBindTranscript(watcher)) {
|
|
172
|
+
clearBindTimer(watcher);
|
|
210
173
|
return;
|
|
211
174
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
175
|
+
if (Date.now() - watcher.startedAt > BIND_GIVE_UP_MS) {
|
|
176
|
+
log.warn(`[Watcher] No transcript appeared for toolUseId=${watcher.toolUseId} after ${BIND_GIVE_UP_MS / 1000}s, giving up`);
|
|
177
|
+
doStop(watcher);
|
|
178
|
+
}
|
|
179
|
+
}, BIND_POLL_INTERVAL_MS);
|
|
180
|
+
// React faster than the poll when the directory already exists.
|
|
215
181
|
try {
|
|
216
|
-
watcher.dirWatcher = fs.watch(subagentsDir, (
|
|
217
|
-
if (watcher.stopped)
|
|
182
|
+
watcher.dirWatcher = fs.watch(watcher.subagentsDir, () => {
|
|
183
|
+
if (watcher.stopped || watcher.jsonlPath)
|
|
218
184
|
return;
|
|
219
|
-
if (
|
|
220
|
-
|
|
221
|
-
if (fs.existsSync(filePath)) {
|
|
222
|
-
watcher.dirWatcher?.close();
|
|
223
|
-
watcher.dirWatcher = undefined;
|
|
224
|
-
startFileWatch(watcher, filePath);
|
|
225
|
-
}
|
|
185
|
+
if (tryBindTranscript(watcher)) {
|
|
186
|
+
clearBindTimer(watcher);
|
|
226
187
|
}
|
|
227
188
|
});
|
|
228
189
|
watcher.dirWatcher.on('error', () => {
|
|
@@ -230,9 +191,69 @@ function watchDirectory(watcher) {
|
|
|
230
191
|
watcher.dirWatcher = undefined;
|
|
231
192
|
});
|
|
232
193
|
}
|
|
194
|
+
catch { /* directory not created yet — the bind poll covers it */ }
|
|
195
|
+
}
|
|
196
|
+
function clearBindTimer(watcher) {
|
|
197
|
+
if (watcher.bindTimer) {
|
|
198
|
+
clearInterval(watcher.bindTimer);
|
|
199
|
+
watcher.bindTimer = undefined;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* One binding attempt. Returns true when a file watch was started.
|
|
204
|
+
*
|
|
205
|
+
* Fallback for CLI layouts that don't write meta files: after a grace
|
|
206
|
+
* period, bind to the newest .jsonl that has no .meta.json sibling. Files
|
|
207
|
+
* WITH a meta sibling are never eligible — a non-matching meta means the
|
|
208
|
+
* file belongs to a different Task/Agent call.
|
|
209
|
+
*/
|
|
210
|
+
function tryBindTranscript(watcher) {
|
|
211
|
+
const { subagentsDir } = watcher;
|
|
212
|
+
let files;
|
|
213
|
+
try {
|
|
214
|
+
files = fs.readdirSync(subagentsDir);
|
|
215
|
+
}
|
|
233
216
|
catch {
|
|
234
|
-
|
|
217
|
+
return false; // directory doesn't exist yet
|
|
218
|
+
}
|
|
219
|
+
// Exact match via meta.json (agent-<id>.meta.json → agent-<id>.jsonl)
|
|
220
|
+
for (const f of files) {
|
|
221
|
+
if (!f.endsWith('.meta.json'))
|
|
222
|
+
continue;
|
|
223
|
+
try {
|
|
224
|
+
const meta = JSON.parse(fs.readFileSync(path.join(subagentsDir, f), 'utf8'));
|
|
225
|
+
if (meta?.toolUseId !== watcher.toolUseId)
|
|
226
|
+
continue;
|
|
227
|
+
const jsonl = f.replace(/\.meta\.json$/, '.jsonl');
|
|
228
|
+
if (files.includes(jsonl)) {
|
|
229
|
+
startFileWatch(watcher, path.join(subagentsDir, jsonl));
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
return false; // our meta exists but the transcript doesn't yet — keep waiting
|
|
233
|
+
}
|
|
234
|
+
catch { /* unreadable/partially-written meta — skip */ }
|
|
235
|
+
}
|
|
236
|
+
// Fallback: newest orphan .jsonl (no meta sibling), after a grace period
|
|
237
|
+
// that gives our meta.json time to appear.
|
|
238
|
+
if (Date.now() - watcher.startedAt < FALLBACK_BIND_GRACE_MS)
|
|
239
|
+
return false;
|
|
240
|
+
const orphans = files.filter((f) => f.endsWith('.jsonl') && !files.includes(f.replace(/\.jsonl$/, '.meta.json')));
|
|
241
|
+
if (orphans.length === 0)
|
|
242
|
+
return false;
|
|
243
|
+
let newest = orphans[0];
|
|
244
|
+
let newestMtime = 0;
|
|
245
|
+
for (const f of orphans) {
|
|
246
|
+
try {
|
|
247
|
+
const stat = fs.statSync(path.join(subagentsDir, f));
|
|
248
|
+
if (stat.mtimeMs > newestMtime) {
|
|
249
|
+
newestMtime = stat.mtimeMs;
|
|
250
|
+
newest = f;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
catch { /* skip */ }
|
|
235
254
|
}
|
|
255
|
+
startFileWatch(watcher, path.join(subagentsDir, newest));
|
|
256
|
+
return true;
|
|
236
257
|
}
|
|
237
258
|
/**
|
|
238
259
|
* Start watching a specific JSONL file for new content
|
|
@@ -240,8 +261,17 @@ function watchDirectory(watcher) {
|
|
|
240
261
|
function startFileWatch(watcher, filePath) {
|
|
241
262
|
if (watcher.stopped)
|
|
242
263
|
return;
|
|
264
|
+
// Binding done — the directory watcher is no longer needed.
|
|
265
|
+
watcher.dirWatcher?.close();
|
|
266
|
+
watcher.dirWatcher = undefined;
|
|
243
267
|
watcher.jsonlPath = filePath;
|
|
244
|
-
log.log(`[Watcher] Found JSONL file: ${filePath} for toolUseId=${watcher.toolUseId}`);
|
|
268
|
+
log.log(`[Watcher] Found JSONL file: ${filePath} for toolUseId=${watcher.toolUseId}${watcher.startAtEnd ? ' (tail from EOF)' : ''}`);
|
|
269
|
+
if (watcher.startAtEnd) {
|
|
270
|
+
try {
|
|
271
|
+
watcher.readPosition = fs.statSync(filePath).size;
|
|
272
|
+
}
|
|
273
|
+
catch { /* file vanished — readNewLines will retry from 0 */ }
|
|
274
|
+
}
|
|
245
275
|
// Read existing content
|
|
246
276
|
readNewLines(watcher);
|
|
247
277
|
// Start idle timer
|
|
@@ -910,6 +910,9 @@ export async function handleUpdateAgentProperties(ctx, payload) {
|
|
|
910
910
|
if (updates.autoCollapseTz !== undefined) {
|
|
911
911
|
agentUpdates.autoCollapseTz = updates.autoCollapseTz || undefined;
|
|
912
912
|
}
|
|
913
|
+
if (updates.autoCollapsePrompt !== undefined) {
|
|
914
|
+
agentUpdates.autoCollapsePrompt = updates.autoCollapsePrompt || undefined;
|
|
915
|
+
}
|
|
913
916
|
// Apply agent property updates if any
|
|
914
917
|
// agentService.updateAgent tracks pending property changes for notification on next command
|
|
915
918
|
if (Object.keys(agentUpdates).length > 0) {
|
|
@@ -6,7 +6,7 @@ import { parseAllFormats } from '../handlers/agent-handler.js';
|
|
|
6
6
|
import { agentService, runtimeService } from '../../services/index.js';
|
|
7
7
|
import { logger, formatToolActivity } from '../../utils/index.js';
|
|
8
8
|
import { parseBossDelegation, parseBossSpawn, getBossForSubordinate, clearDelegation } from '../handlers/boss-response-handler.js';
|
|
9
|
-
import { startWatching as startJsonlWatching, stopWatching as stopJsonlWatching, getSubagentsDir } from '../../services/subagent-jsonl-watcher.js';
|
|
9
|
+
import { startWatching as startJsonlWatching, stopWatching as stopJsonlWatching, isWatching as isJsonlWatching, getSubagentsDir } from '../../services/subagent-jsonl-watcher.js';
|
|
10
10
|
const log = logger.ws;
|
|
11
11
|
const MAX_SYNTHETIC_DIFF_FILE_BYTES = 256 * 1024;
|
|
12
12
|
function sanitizeParsedContextStats(stats) {
|
|
@@ -31,6 +31,38 @@ export function setupRuntimeListeners(ctx) {
|
|
|
31
31
|
const bossAccumulatedText = new Map();
|
|
32
32
|
// Track agents currently in compacting state so we can clear it when they resume
|
|
33
33
|
const compactingAgents = new Set();
|
|
34
|
+
function armSubagentJsonlWatcher(agentId, toolUseId, options) {
|
|
35
|
+
const parentAgent = agentService.getAgent(agentId);
|
|
36
|
+
if (!parentAgent?.sessionId)
|
|
37
|
+
return;
|
|
38
|
+
const subagentsDir = getSubagentsDir(parentAgent.cwd, parentAgent.sessionId);
|
|
39
|
+
startJsonlWatching(toolUseId, agentId, subagentsDir, (watchedToolUseId, parentAgentId, entries) => {
|
|
40
|
+
ctx.broadcast({
|
|
41
|
+
type: 'subagent_stream',
|
|
42
|
+
payload: { toolUseId: watchedToolUseId, parentAgentId, entries },
|
|
43
|
+
});
|
|
44
|
+
}, (watchedToolUseId, parentAgentId, result) => {
|
|
45
|
+
// The parent CLI stream never echoes subagent tool_result events
|
|
46
|
+
// (only tool_use), so without this bridge the subagent Bash
|
|
47
|
+
// cards in the terminal keep their "running" spinner forever.
|
|
48
|
+
// Mirror the parent-agent behavior: only Bash results get a card.
|
|
49
|
+
if (result.toolName !== 'Bash')
|
|
50
|
+
return;
|
|
51
|
+
ctx.broadcast({
|
|
52
|
+
type: 'event',
|
|
53
|
+
payload: {
|
|
54
|
+
agentId: parentAgentId,
|
|
55
|
+
type: 'tool_result',
|
|
56
|
+
toolName: 'Bash',
|
|
57
|
+
toolOutput: result.output,
|
|
58
|
+
parentToolUseId: watchedToolUseId,
|
|
59
|
+
// Subagent-internal tool_use id — matches the uuid of the
|
|
60
|
+
// corresponding tool_start card so the client pairs exactly.
|
|
61
|
+
uuid: result.toolUseId,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
}, options);
|
|
65
|
+
}
|
|
34
66
|
runtimeService.on('event', (agentId, event) => {
|
|
35
67
|
// Clear compacting state when agent resumes with new output after compaction
|
|
36
68
|
if (compactingAgents.has(agentId) && event.type !== 'compacting') {
|
|
@@ -100,35 +132,7 @@ export function setupRuntimeListeners(ctx) {
|
|
|
100
132
|
ctx.sendActivity(agentId, `Spawned subagent: ${subagent.name} (${subagent.subagentType})`);
|
|
101
133
|
log.log(`[Subagent] Broadcast subagent_started: ${subagent.name} (${subagent.id})`);
|
|
102
134
|
// Start streaming JSONL file for this subagent
|
|
103
|
-
|
|
104
|
-
const subagentsDir = getSubagentsDir(parentAgent.cwd, parentAgent.sessionId);
|
|
105
|
-
startJsonlWatching(event.toolUseId, agentId, subagentsDir, (toolUseId, parentAgentId, entries) => {
|
|
106
|
-
ctx.broadcast({
|
|
107
|
-
type: 'subagent_stream',
|
|
108
|
-
payload: { toolUseId, parentAgentId, entries },
|
|
109
|
-
});
|
|
110
|
-
}, (toolUseId, parentAgentId, result) => {
|
|
111
|
-
// The parent CLI stream never echoes subagent tool_result events
|
|
112
|
-
// (only tool_use), so without this bridge the subagent Bash
|
|
113
|
-
// cards in the terminal keep their "running" spinner forever.
|
|
114
|
-
// Mirror the parent-agent behavior: only Bash results get a card.
|
|
115
|
-
if (result.toolName !== 'Bash')
|
|
116
|
-
return;
|
|
117
|
-
ctx.broadcast({
|
|
118
|
-
type: 'event',
|
|
119
|
-
payload: {
|
|
120
|
-
agentId: parentAgentId,
|
|
121
|
-
type: 'tool_result',
|
|
122
|
-
toolName: 'Bash',
|
|
123
|
-
toolOutput: result.output,
|
|
124
|
-
parentToolUseId: toolUseId,
|
|
125
|
-
// Subagent-internal tool_use id — matches the uuid of the
|
|
126
|
-
// corresponding tool_start card so the client pairs exactly.
|
|
127
|
-
uuid: result.toolUseId,
|
|
128
|
-
},
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
}
|
|
135
|
+
armSubagentJsonlWatcher(agentId, event.toolUseId);
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
}
|
|
@@ -171,6 +175,12 @@ export function setupRuntimeListeners(ctx) {
|
|
|
171
175
|
}
|
|
172
176
|
// Forward subagent internal tool activity to client (events with parentToolUseId)
|
|
173
177
|
if (event.parentToolUseId && event.type === 'tool_start' && event.toolName !== 'Task' && event.toolName !== 'Agent') {
|
|
178
|
+
// Self-heal: if this subagent's watcher is gone (idle/max timeout hit,
|
|
179
|
+
// server restarted mid-run) or was never armed, re-arm it tailing from
|
|
180
|
+
// EOF so this tool's result can still resolve its card's spinner.
|
|
181
|
+
if (!isJsonlWatching(event.parentToolUseId)) {
|
|
182
|
+
armSubagentJsonlWatcher(agentId, event.parentToolUseId, { startAtEnd: true });
|
|
183
|
+
}
|
|
174
184
|
const toolDesc = formatToolActivity(event.toolName, event.toolInput);
|
|
175
185
|
ctx.broadcast({
|
|
176
186
|
type: 'subagent_output',
|