@turns/dev 0.0.15 → 0.0.17

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/README.md CHANGED
@@ -38,6 +38,23 @@ process to stay resident. A run narrowed to a directory — watching or `--once`
38
38
  never prunes saved upload cursors, so it cannot make the sessions it filtered
39
39
  out replay from their first byte the next time the daemon starts.
40
40
 
41
+ Native background tasks retain their detached agent anchor after the foreground
42
+ prompt finishes, including while the daemon client is disconnected. Codex child
43
+ agents, Claude/Letta tasks, ACP task tools, Prime/OMP jobs, and DeepSeek jobs
44
+ suspend the normal five-minute idle cleanup. Follow-up prompts remain available;
45
+ the anchor starts a fresh idle countdown after the last known task finishes or
46
+ its transport exits. Explicit shutdown, interruption, transport replacement, and
47
+ turn timeouts retain their existing semantics.
48
+
49
+ Retention uses native lifecycle events and task-tool results. Assistant prose,
50
+ a completed launch tool, or a failed status query does not prove that a task has
51
+ finished. Copilot, Hermes, and OpenCode captures lack reliable terminal evidence
52
+ in their live streams, so their anchors can stay resident until a later native
53
+ status result, transport exit, or explicit shutdown. No time-based fallback kills
54
+ quiet tasks. One-shot harnesses such as Muse and Antigravity already remain
55
+ foreground-running until their native process exits; there is no warm child
56
+ process for idle cleanup to kill afterward.
57
+
41
58
  The server is selected with `--server URL` or `TURNS_ORIGIN`. Its normalized
42
59
  origin is saved and reused by later invocations and daemon starts. On Linux,
43
60
  Turns uses the XDG config/state directories and installs this user service:
@@ -12,6 +12,7 @@ import { ACP_PROFILES, lettaHeadlessEnvironment, lettaSessionIdentity, PROCESS_H
12
12
  import { asyncQuestionInteractions, asyncQuestionReplyText, isAsyncQuestionInteraction, parseAsyncQuestionReplies, } from "./codex-async-questions.js";
13
13
  import { recordLettaSessionContext } from "./letta-session-context.js";
14
14
  import { recoverAsyncQuestion } from "./codex-async-question-recovery.js";
15
+ import { BackgroundTaskState } from "./background-task-state.js";
15
16
  const socketIndex = process.argv.indexOf('--socket');
16
17
  const socketPath = socketIndex >= 0 ? path.resolve(process.argv[socketIndex + 1] || '') : '';
17
18
  if (!socketPath)
@@ -98,6 +99,7 @@ function childResult(child, timeoutMs) {
98
99
  return result.promise;
99
100
  }
100
101
  class LineTransport {
102
+ backgroundTasks;
101
103
  anchor;
102
104
  harness;
103
105
  nextId;
@@ -107,6 +109,7 @@ class LineTransport {
107
109
  constructor(anchor, harness, command, args, cwd, env = {}) {
108
110
  this.anchor = anchor;
109
111
  this.harness = harness;
112
+ this.backgroundTasks = new BackgroundTaskState(anchor.source);
110
113
  this.nextId = 1;
111
114
  this.pending = new Map();
112
115
  this.lastEventAt = Date.now();
@@ -135,6 +138,9 @@ class LineTransport {
135
138
  for (const waiter of this.pending.values())
136
139
  waiter.reject(error);
137
140
  this.pending.clear();
141
+ this.backgroundTasks.clear();
142
+ if (this.anchor.transport === this && this.anchor.status === 'idle')
143
+ this.anchor.scheduleExit();
138
144
  this.onClose?.(error);
139
145
  });
140
146
  }
@@ -157,6 +163,11 @@ class LineTransport {
157
163
  json = JSON.parse(line);
158
164
  }
159
165
  catch { }
166
+ const wasActive = this.backgroundTasks.active;
167
+ this.backgroundTasks.observe({ json }, this.anchor.agentSessionId);
168
+ if (wasActive !== this.backgroundTasks.active && this.anchor.transport === this && this.anchor.status === 'idle') {
169
+ this.anchor.scheduleExit();
170
+ }
160
171
  this.anchor.emitLive({
161
172
  direction: 'in',
162
173
  source: `${this.harness}.stdout`,
@@ -1168,6 +1179,7 @@ class OmpRpcTransport extends RpcTransport {
1168
1179
  }
1169
1180
  }
1170
1181
  class DeepseekTransport {
1182
+ backgroundTasks;
1171
1183
  anchor;
1172
1184
  sessionId;
1173
1185
  waiter;
@@ -1175,6 +1187,7 @@ class DeepseekTransport {
1175
1187
  base;
1176
1188
  constructor(anchor) {
1177
1189
  this.anchor = anchor;
1190
+ this.backgroundTasks = new BackgroundTaskState('deepseek');
1178
1191
  this.sessionId = anchor.agentSessionId;
1179
1192
  this.waiter = null;
1180
1193
  this.socket = null;
@@ -1184,6 +1197,13 @@ class DeepseekTransport {
1184
1197
  this.base = base;
1185
1198
  this.socket = new WebSocket(`${base.replace(/^http/, 'ws')}/api/events.mux`);
1186
1199
  this.socket.on('message', (bytes) => this.receive(String(bytes)));
1200
+ this.socket.once('close', () => {
1201
+ this.backgroundTasks.clear();
1202
+ this.waiter?.reject(new Error('DeepSeek transport closed.'));
1203
+ this.waiter = null;
1204
+ if (this.anchor.transport === this && this.anchor.status === 'idle')
1205
+ this.anchor.scheduleExit();
1206
+ });
1187
1207
  await new Promise((resolve, reject) => {
1188
1208
  this.socket.once('open', resolve);
1189
1209
  this.socket.once('error', reject);
@@ -1207,6 +1227,11 @@ class DeepseekTransport {
1207
1227
  }
1208
1228
  if (String(frame?.payload?.sessionId || '') !== this.sessionId)
1209
1229
  return;
1230
+ const wasActive = this.backgroundTasks.active;
1231
+ this.backgroundTasks.observe({ json: frame });
1232
+ if (wasActive !== this.backgroundTasks.active && this.anchor.transport === this && this.anchor.status === 'idle') {
1233
+ this.anchor.scheduleExit();
1234
+ }
1210
1235
  this.anchor.emitLive({
1211
1236
  direction: 'in',
1212
1237
  source: 'deepseek.websocket',
@@ -1275,10 +1300,20 @@ class DeepseekTransport {
1275
1300
  async prompt(text, timeoutMs) {
1276
1301
  const waiter = deferred();
1277
1302
  this.waiter = waiter;
1278
- await this.rpc('session.prompt', { sessionId: this.sessionId, mode: 'queue', content: [{ type: 'text', text }] });
1279
1303
  const timer = setTimeout(() => waiter.reject(new Error('DeepSeek turn timed out.')), timeoutMs);
1280
1304
  timer.unref?.();
1281
- return waiter.promise.finally(() => clearTimeout(timer));
1305
+ // Observe both promises immediately: the event socket can close before the
1306
+ // HTTP acknowledgement arrives, and must not produce an unhandled rejection.
1307
+ return Promise.all([
1308
+ this.rpc('session.prompt', { sessionId: this.sessionId, mode: 'queue', content: [{ type: 'text', text }] }),
1309
+ waiter.promise,
1310
+ ])
1311
+ .then(([, result]) => result)
1312
+ .finally(() => {
1313
+ clearTimeout(timer);
1314
+ if (this.waiter === waiter)
1315
+ this.waiter = null;
1316
+ });
1282
1317
  }
1283
1318
  interrupt() {
1284
1319
  return this.rpc('session.cancel', { sessionId: this.sessionId });
@@ -1558,7 +1593,7 @@ class Anchor {
1558
1593
  }
1559
1594
  scheduleExit() {
1560
1595
  clearTimeout(this.idleTimer);
1561
- if (this.pendingInteractions.size) {
1596
+ if (this.status === 'running' || this.pendingInteractions.size || this.transport?.backgroundTasks?.active) {
1562
1597
  this.idleTimer = null;
1563
1598
  return;
1564
1599
  }
@@ -0,0 +1,471 @@
1
+ /** Native background-work leases owned by the detached anchor, not its client.
2
+ * This intentionally consumes protocol envelopes rather than searching stdout,
3
+ * user prompts, assistant prose, or command output for apparent task statuses. */
4
+ const RUNNING = new Set([
5
+ 'accepted',
6
+ 'active',
7
+ 'created',
8
+ 'dispatched',
9
+ 'in_progress',
10
+ 'inprogress',
11
+ 'pending',
12
+ 'queued',
13
+ 'running',
14
+ 'started',
15
+ 'working',
16
+ ]);
17
+ const TERMINAL = new Set([
18
+ 'aborted',
19
+ 'cancelled',
20
+ 'canceled',
21
+ 'completed',
22
+ 'complete',
23
+ 'done',
24
+ 'error',
25
+ 'errored',
26
+ 'exited',
27
+ 'failed',
28
+ 'failure',
29
+ 'finished',
30
+ 'idle',
31
+ 'killed',
32
+ 'rejected',
33
+ 'result_ready',
34
+ 'stopped',
35
+ 'succeeded',
36
+ 'success',
37
+ 'terminated',
38
+ ]);
39
+ const STATUS_TOOLS = /^(?:taskoutput|task_output|tasklist|task_list|taskstop|task_stop|list_agents|wait_agent|read_agent|subagent_status|subagent_wait|subagent_read_result|read_subagent|list_background_processes|read_background_output|job_list|job_output|job_kill|hub|shell_status|shell_wait|shell_list|manage_subagents|get_command_or_subagent_output)$/u;
40
+ const ID_KEYS = [
41
+ 'task_id',
42
+ 'taskId',
43
+ 'agent_id',
44
+ 'agentId',
45
+ 'subagent_id',
46
+ 'subagentId',
47
+ 'agent_path',
48
+ 'agentPath',
49
+ 'delegation_id',
50
+ 'delegationId',
51
+ 'job_id',
52
+ 'jobId',
53
+ 'shell_id',
54
+ 'shellId',
55
+ 'rlm_child_id',
56
+ 'rlmChildId',
57
+ 'backgroundTaskId',
58
+ ];
59
+ const MAX_HISTORY = 512;
60
+ const object = (value) => (value && typeof value === 'object' && !Array.isArray(value) ? value : null);
61
+ const string = (value) => (typeof value === 'string' ? value : '');
62
+ const normalized = (value) => string(value)
63
+ .toLowerCase()
64
+ .replace(/[^a-z0-9]+/gu, '_')
65
+ .replace(/^_|_$/gu, '');
66
+ function decode(value) {
67
+ if (typeof value !== 'string')
68
+ return value;
69
+ try {
70
+ return JSON.parse(value);
71
+ }
72
+ catch {
73
+ return value;
74
+ }
75
+ }
76
+ function status(value) {
77
+ if (object(value))
78
+ value = Object.keys(value).find((key) => RUNNING.has(normalized(key)) || TERMINAL.has(normalized(key)));
79
+ const key = normalized(value);
80
+ if (RUNNING.has(key))
81
+ return 'running';
82
+ if (TERMINAL.has(key) || /^exited_\d+$/u.test(key))
83
+ return 'terminal';
84
+ }
85
+ function background(value) {
86
+ const input = object(decode(value));
87
+ return Boolean(input &&
88
+ (input.run_in_background === true ||
89
+ input.runInBackground === true ||
90
+ input.background === true ||
91
+ input.is_background === true ||
92
+ input.isBackground === true ||
93
+ input.async === true ||
94
+ input.mode === 'background'));
95
+ }
96
+ function nativeIds(value) {
97
+ const record = object(value);
98
+ if (!record)
99
+ return [];
100
+ return ID_KEYS.flatMap((key) => typeof record[key] === 'string' || typeof record[key] === 'number' ? [String(record[key])] : []).slice(0, 1);
101
+ }
102
+ function textIds(value) {
103
+ const ids = [];
104
+ for (const pattern of [
105
+ /(?:task|agent|subagent|delegation|job|shell)[ _-]?id\s*[:=]\s*["'`]?([A-Za-z0-9_./:-]+)/giu,
106
+ /(?:background with ID:|Backgrounded as job|started background job)\s*([A-Za-z0-9_./:-]+)/giu,
107
+ /(?:background\s*\(PID:\s*|\[PID\s+)(\d+)/giu,
108
+ /\bShell\s+([A-Za-z0-9_.:-]+)(?:,|\n)/gu,
109
+ /\brlm_child_id=['"]([^'"]+)['"]/gu,
110
+ /\/delegation\/live\/([^/\s"']+)/gu,
111
+ ])
112
+ for (const match of value.matchAll(pattern))
113
+ ids.push(match[1].replace(/[.,;]+$/u, ''));
114
+ return [...new Set(ids)];
115
+ }
116
+ function contents(value, depth = 0) {
117
+ if (depth > 10)
118
+ return '';
119
+ if (typeof value === 'string')
120
+ return value;
121
+ if (Array.isArray(value))
122
+ return value.map((child) => contents(child, depth + 1)).join('\n');
123
+ return object(value)
124
+ ? contents(value.text ?? value.content ?? value.output ?? value.observation ?? '', depth + 1)
125
+ : '';
126
+ }
127
+ function textStatus(value) {
128
+ // Only provider status headers, not words inside a shell command or its output.
129
+ const reported = value.match(/(?:^|\n|,\s*)(?:status|state|final_sub_status)\s*[:=]\s*["']?([\w -]+)/iu)?.[1];
130
+ if (reported)
131
+ return status(reported.split(/[,\n]/u)[0].trim());
132
+ if (/^(?:Subagent completed:|Agent (?:completed|finished)|<task-notification>)/u.test(value))
133
+ return 'terminal';
134
+ if (/\b(?:Command moved to background|Command running in background with ID:|Agent started in background|Backgrounded as job|started background job)\b/u.test(value))
135
+ return 'running';
136
+ }
137
+ export class BackgroundTaskState {
138
+ activeIds = new Set();
139
+ calls = new Map();
140
+ aliases = new Map();
141
+ terminalIds = new Set();
142
+ children = new Set();
143
+ harness;
144
+ sessionId;
145
+ constructor(harness, sessionId = '') {
146
+ this.harness = harness;
147
+ this.sessionId = sessionId;
148
+ }
149
+ get active() {
150
+ return this.activeIds.size > 0;
151
+ }
152
+ clear() {
153
+ this.activeIds.clear();
154
+ this.calls.clear();
155
+ this.aliases.clear();
156
+ this.terminalIds.clear();
157
+ this.children.clear();
158
+ }
159
+ update(id, state, restart = false) {
160
+ if (typeof id !== 'string' || !id || id.length > 512)
161
+ return;
162
+ const key = this.aliases.get(id) || id;
163
+ const next = status(state);
164
+ if (next === 'terminal') {
165
+ this.activeIds.delete(key);
166
+ this.terminalIds.add(key);
167
+ while (this.terminalIds.size > MAX_HISTORY) {
168
+ const oldest = this.terminalIds.values().next().value;
169
+ this.terminalIds.delete(oldest);
170
+ this.children.delete(oldest);
171
+ for (const [alias, target] of this.aliases)
172
+ if (target === oldest)
173
+ this.aliases.delete(alias);
174
+ }
175
+ }
176
+ else if (next === 'running' && (restart || !this.terminalIds.has(key))) {
177
+ if (restart)
178
+ this.terminalIds.delete(key);
179
+ this.activeIds.add(key);
180
+ }
181
+ }
182
+ bind(callId, ids) {
183
+ if (!ids.length)
184
+ return;
185
+ this.activeIds.delete(`call:${callId}`);
186
+ for (const id of ids)
187
+ this.update(id, 'running');
188
+ }
189
+ tool(id, name, input, output, failed = false) {
190
+ if (typeof id !== 'string' || !id)
191
+ return;
192
+ const args = object(decode(input));
193
+ const previous = this.calls.get(id);
194
+ const toolName = normalized(name).slice(0, 512) || previous?.name || '';
195
+ const launch = previous?.launch ||
196
+ background(args) ||
197
+ /^(?:spawn_agent|spawn_subagent|subagent_spawn|invoke_subagent)$/u.test(toolName);
198
+ const targets = args
199
+ ? [
200
+ ...nativeIds(args),
201
+ ...(Array.isArray(args.task_ids) ? args.task_ids : []),
202
+ ...(Array.isArray(args.ids) ? args.ids : []),
203
+ ...(this.harness === 'poolside' && args.name ? [args.name] : []),
204
+ ].filter((value) => typeof value === 'string')
205
+ : previous?.targets || [];
206
+ const delegation = previous?.delegation || (this.harness === 'hermes' && /\/delegation\/live\//u.test(string(name)));
207
+ if (delegation)
208
+ targets.push(...textIds(string(name)));
209
+ const call = { name: toolName, launch, targets, delegation };
210
+ this.calls.delete(id);
211
+ this.calls.set(id, call);
212
+ if (this.calls.size > MAX_HISTORY) {
213
+ for (const key of this.calls.keys()) {
214
+ // Keep pending launch correlation even during a burst of tool traffic.
215
+ if (key !== id && !this.activeIds.has(`call:${key}`))
216
+ this.calls.delete(key);
217
+ if (this.calls.size <= MAX_HISTORY)
218
+ break;
219
+ }
220
+ }
221
+ // Provider-native lifecycle supplies Devin and Prime identities directly.
222
+ const nativeLaunch = (this.harness === 'devin' && toolName === 'run_subagent') || (this.harness === 'prime' && toolName === 'ipython');
223
+ if (launch && !nativeLaunch && !previous?.launch)
224
+ this.update(`call:${id}`, 'running');
225
+ if (failed && launch)
226
+ this.update(`call:${id}`, 'failed');
227
+ if (output === undefined || failed)
228
+ return;
229
+ const text = contents(output);
230
+ const outputLaunch = /^(?:bash|shell|run_shell_command)$/u.test(toolName) &&
231
+ /^(?:Command moved to background|Command running in background with ID:|Backgrounded as job|started background job)/u.test(text);
232
+ const hermes = this.harness === 'hermes' && (/^delegate/u.test(toolName) || delegation);
233
+ if (!(launch ||
234
+ outputLaunch ||
235
+ STATUS_TOOLS.test(toolName) ||
236
+ hermes ||
237
+ (this.harness === 'muse' && toolName.startsWith('subagent'))))
238
+ return;
239
+ const found = new Set();
240
+ const visit = (value, depth = 0) => {
241
+ if (depth > 10)
242
+ return;
243
+ let parsed = decode(value);
244
+ if (Array.isArray(parsed)) {
245
+ for (const child of parsed)
246
+ visit(child, depth + 1);
247
+ return;
248
+ }
249
+ if (object(parsed)) {
250
+ const ids = nativeIds(parsed);
251
+ // Native list rows use an unqualified id, unlike enclosing tool calls.
252
+ if (typeof parsed.id === 'string' && (parsed.status || parsed.state))
253
+ ids.push(parsed.id);
254
+ if (this.harness === 'antigravity' && typeof parsed.conversationId === 'string')
255
+ ids.push(parsed.conversationId);
256
+ for (const task of ids) {
257
+ found.add(task);
258
+ this.update(task, this.harness === 'muse' ? parsed.roster?.[0]?.state || parsed.status : (parsed.status ?? parsed.state));
259
+ }
260
+ if (!ids.length && !parsed.jobs && !parsed.agents && STATUS_TOOLS.test(toolName)) {
261
+ for (const task of targets)
262
+ this.update(task, parsed.status ?? parsed.state);
263
+ }
264
+ for (const [key, child] of Object.entries(parsed)) {
265
+ if (/prompt$|description$/iu.test(key) ||
266
+ ['command', 'label', 'detailedContent', 'stdout', 'stderr'].includes(key))
267
+ continue;
268
+ // A task row's output is arbitrary program/agent text, not another status record.
269
+ if (status(parsed.status ?? parsed.state) && ['output', 'message', 'result'].includes(key))
270
+ continue;
271
+ visit(child, depth + 1);
272
+ }
273
+ }
274
+ else if (typeof parsed === 'string') {
275
+ parsed = parsed
276
+ .replace(/<output>[\s\S]*?(?:<\/output>|$)/gu, '')
277
+ .split(/\n(?:output|stdout|stderr):\s*\n/iu)[0];
278
+ const xmlId = /<task_id>([^<]+)<\/task_id>/u.exec(parsed)?.[1];
279
+ const xmlStatus = /<status>([^<]+)<\/status>/u.exec(parsed)?.[1];
280
+ if (xmlId && xmlStatus) {
281
+ found.add(xmlId);
282
+ this.update(xmlId, xmlStatus);
283
+ }
284
+ for (const match of parsed.matchAll(/<task\s+([^>]+)>/gu)) {
285
+ const task = /\bid=["']([^"']+)["']/u.exec(match[1])?.[1];
286
+ const state = /\bstate=["']([^"']+)["']/u.exec(match[1])?.[1];
287
+ if (task) {
288
+ found.add(task);
289
+ this.update(task, state);
290
+ }
291
+ }
292
+ if (this.harness === 'antigravity' && toolName === 'manage_subagents') {
293
+ const list = parsed.slice(parsed.indexOf('['));
294
+ if (Array.isArray(decode(list)))
295
+ visit(decode(list), depth + 1);
296
+ }
297
+ const ids = textIds(parsed);
298
+ for (const task of ids)
299
+ found.add(task);
300
+ // PID and job listings expose one status per line, not a global status.
301
+ for (const line of parsed.split('\n')) {
302
+ const pid = /\[PID\s+(\d+)\]\s+(RUNNING|EXITED)/u.exec(line);
303
+ const job = /^([\w.-]+)\s+\[[^\]]+\]\s+(\w+)/u.exec(line);
304
+ if (pid)
305
+ this.update(pid[1], pid[2]);
306
+ if (job)
307
+ this.update(job[1], job[2]);
308
+ }
309
+ const state = textStatus(parsed);
310
+ const addressed = ids.length ? ids : targets;
311
+ if (state)
312
+ for (const task of addressed)
313
+ this.update(task, state === 'terminal' ? 'completed' : 'running');
314
+ if (hermes && /\bresult\s+\|\s+terminal ok\b/u.test(parsed)) {
315
+ for (const task of [...ids, ...targets])
316
+ this.update(task, 'completed');
317
+ }
318
+ }
319
+ };
320
+ visit(output);
321
+ if (launch || outputLaunch) {
322
+ this.bind(id, [...found]);
323
+ // A failed native launch may complete the tool with an error payload.
324
+ if (!found.size && object(output)?.isError === true)
325
+ this.update(`call:${id}`, 'failed');
326
+ }
327
+ }
328
+ observe(event, sessionId = this.sessionId) {
329
+ if (event?.direction === 'out' || String(event?.source || '').endsWith('.stderr'))
330
+ return;
331
+ const frame = object(event?.json) || object(decode(event?.raw));
332
+ if (!frame)
333
+ return;
334
+ if (this.harness === 'codex') {
335
+ const params = frame.params || {};
336
+ const thread = params.threadId;
337
+ if (thread && thread !== sessionId && this.children.has(thread)) {
338
+ if (frame.method === 'turn/started')
339
+ this.update(thread, 'running', true);
340
+ if (['turn/completed', 'turn/failed', 'turn/aborted'].includes(frame.method))
341
+ this.update(thread, 'completed');
342
+ if (frame.method === 'thread/status/changed')
343
+ this.update(thread, params.status?.type);
344
+ }
345
+ if (thread && sessionId && thread !== sessionId && !this.children.has(thread))
346
+ return;
347
+ const item = params.item;
348
+ if (!['item/started', 'item/completed'].includes(frame.method) || !object(item))
349
+ return;
350
+ if (item.type === 'subAgentActivity' || item.type === 'sub_agent_activity') {
351
+ const id = item.agentThreadId || item.agent_thread_id || item.agentPath || item.agent_path;
352
+ if (typeof id === 'string') {
353
+ this.children.add(id);
354
+ const alias = item.agentPath || item.agent_path;
355
+ if (alias)
356
+ this.aliases.set(alias, id);
357
+ this.update(id, item.kind, frame.method === 'item/started' && item.kind === 'started');
358
+ }
359
+ }
360
+ if (item.type === 'collabAgentToolCall') {
361
+ for (const id of Array.isArray(item.receiverThreadIds) ? item.receiverThreadIds : []) {
362
+ this.children.add(id);
363
+ if (['spawnAgent', 'spawn_agent', 'resumeAgent', 'resume_agent', 'sendInput', 'send_input'].includes(item.tool) &&
364
+ item.status !== 'failed')
365
+ this.update(id, 'running', frame.method === 'item/started');
366
+ if (['closeAgent', 'close_agent'].includes(item.tool) &&
367
+ frame.method === 'item/completed' &&
368
+ item.status === 'completed')
369
+ this.update(id, 'completed');
370
+ }
371
+ for (const [id, state] of Object.entries(item.agentsStates || item.agentsStatus || {})) {
372
+ this.children.add(id);
373
+ this.update(id, object(state)?.status ?? state);
374
+ }
375
+ }
376
+ return;
377
+ }
378
+ if (frame.type === 'system' && ['claude', 'letta'].includes(this.harness)) {
379
+ if (frame.subtype === 'task_started' && frame.is_backgrounded !== false)
380
+ this.update(frame.task_id, 'running', true);
381
+ if (frame.subtype === 'task_notification')
382
+ this.update(frame.task_id, 'completed');
383
+ }
384
+ const update = frame.params?.update;
385
+ if (object(update) && ['session/update', '_x.ai/session_notification'].includes(frame.method)) {
386
+ if (sessionId && frame.params.sessionId && frame.params.sessionId !== sessionId)
387
+ return;
388
+ const meta = update._meta || {};
389
+ const started = meta['cognition.ai/subagent_started'];
390
+ const ended = meta['cognition.ai/subagent_completed'];
391
+ if (started?.isBackground)
392
+ this.update(started.agentId, 'running', true);
393
+ if (ended)
394
+ this.update(ended.agentId, 'completed');
395
+ if (['tool_call', 'tool_call_update'].includes(update.sessionUpdate)) {
396
+ const known = this.calls.get(update.toolCallId);
397
+ const toolName = meta.toolName ||
398
+ meta.tool_name ||
399
+ meta['codebuddy.ai/toolName'] ||
400
+ meta['cognition.ai/inferenceToolName'] ||
401
+ meta['x.ai/tool']?.name ||
402
+ known?.name ||
403
+ update.title ||
404
+ string(update.toolCallId).split('__call_')[0];
405
+ const out = update.status !== 'pending' &&
406
+ (update.status === 'completed' ||
407
+ update.rawOutput !== undefined ||
408
+ (this.harness === 'hermes' && known?.delegation))
409
+ ? this.harness === 'qwen'
410
+ ? // Qwen's rawOutput is often only 'Listed N background agents';
411
+ // the native IDs and per-agent statuses live in content.
412
+ [update.rawOutput, update.content]
413
+ : (update.rawOutput ?? update.content)
414
+ : undefined;
415
+ this.tool(update.toolCallId, toolName, update.rawInput, out, update.status === 'failed');
416
+ }
417
+ return;
418
+ }
419
+ if (this.harness === 'gemini' && frame.method === 'session/request_permission' && frame.params?.toolCall) {
420
+ const call = frame.params.toolCall;
421
+ this.tool(call.toolCallId, string(call.toolCallId).split('__call_')[0], call.rawInput);
422
+ }
423
+ if (['claude', 'letta'].includes(this.harness)) {
424
+ const message = frame.message || frame;
425
+ for (const block of Array.isArray(message.content) ? message.content : []) {
426
+ if (block.type === 'tool_use')
427
+ this.tool(block.id, block.name, block.input);
428
+ if (block.type === 'tool_result')
429
+ this.tool(block.tool_use_id, '', undefined, block.content, block.is_error === true);
430
+ }
431
+ if (frame.message_type === 'tool_call_message')
432
+ this.tool(frame.tool_call?.tool_call_id, frame.tool_call?.name, frame.tool_call?.arguments);
433
+ if (frame.message_type === 'tool_return_message')
434
+ this.tool(frame.tool_call_id, '', undefined, frame.tool_return, frame.status === 'error');
435
+ }
436
+ if (['omp', 'prime', 'pi'].includes(this.harness)) {
437
+ if (frame.type === 'tool_execution_start')
438
+ this.tool(frame.toolCallId, frame.toolName, frame.args);
439
+ if (frame.type === 'tool_execution_end')
440
+ this.tool(frame.toolCallId, frame.toolName, undefined, frame.result, frame.isError);
441
+ if (frame.type === 'rlm_child_update' && this.harness === 'prime')
442
+ this.update(frame.child?.id, frame.child?.status);
443
+ }
444
+ if (this.harness === 'deepseek' && frame.method === 'session/event') {
445
+ const event = frame.payload?.event;
446
+ const data = event?.data || {};
447
+ if (event?.type === 'tool/call')
448
+ this.tool(data.callId, data.name, data.arguments);
449
+ if (event?.type === 'tool/result')
450
+ for (const part of Array.isArray(data.message?.content) ? data.message.content : []) {
451
+ if (part.type === 'tool-result')
452
+ this.tool(part.toolCallId, '', undefined, part.content, part.isError);
453
+ }
454
+ }
455
+ if (this.harness === 'antigravity' && frame.event === 'step_update') {
456
+ const step = frame.step_update;
457
+ for (const child of Array.isArray(step?.subagent_info?.subagents) ? step.subagent_info.subagents : [])
458
+ this.update(child.conversation_id, 'running');
459
+ const tool = step?.tool_info;
460
+ if (tool)
461
+ this.tool(String(step.step_index), tool.name, tool.parameters, tool.output);
462
+ }
463
+ if (this.harness === 'muse' && frame.payload?.kind === 'task_lifecycle') {
464
+ const event = frame.payload.event;
465
+ if (event?.kind === 'proposed')
466
+ this.tool(event.task_id, string(event.task_kind).replace(/^tool\./u, ''), undefined);
467
+ if (event?.kind === 'output')
468
+ this.tool(event.task_id, '', undefined, event.chunk);
469
+ }
470
+ }
471
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@turns/dev",
3
- "version": "0.0.15",
3
+ "version": "0.0.17",
4
4
  "description": "Upload local AI agent sessions to turns.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",