@turns/dev 0.0.16 → 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`,
@@ -188,7 +199,6 @@ class LineTransport {
188
199
  }
189
200
  }
190
201
  class ClaudeTransport extends LineTransport {
191
- backgroundTasks = new Set();
192
202
  sessionId;
193
203
  permissionMode;
194
204
  turnWaiter;
@@ -228,18 +238,6 @@ class ClaudeTransport extends LineTransport {
228
238
  onFrame(frame) {
229
239
  if (!frame || typeof frame !== 'object')
230
240
  return;
231
- if (frame.type === 'system' && typeof frame.task_id === 'string') {
232
- if (frame.subtype === 'task_started' && frame.is_backgrounded !== false) {
233
- this.backgroundTasks.add(frame.task_id);
234
- }
235
- else if (frame.subtype === 'task_notification') {
236
- this.backgroundTasks.delete(frame.task_id);
237
- }
238
- // A result completes the foreground prompt, not the tasks it launched.
239
- // Keep accepting new prompts while those tasks retain the warm process.
240
- if (this.anchor.status === 'idle')
241
- this.anchor.scheduleExit();
242
- }
243
241
  if (frame.type === 'system' && frame.subtype === 'init' && frame.session_id) {
244
242
  this.sessionId = String(frame.session_id);
245
243
  if (this.sessionId !== this.anchor.agentSessionId)
@@ -269,9 +267,6 @@ class ClaudeTransport extends LineTransport {
269
267
  void this.answerReverse(frame);
270
268
  }
271
269
  onClose(error) {
272
- this.backgroundTasks.clear();
273
- if (this.anchor.status === 'idle')
274
- this.anchor.scheduleExit();
275
270
  if (this.turnWaiter) {
276
271
  this.turnWaiter.reject(error);
277
272
  this.turnWaiter = null;
@@ -1184,6 +1179,7 @@ class OmpRpcTransport extends RpcTransport {
1184
1179
  }
1185
1180
  }
1186
1181
  class DeepseekTransport {
1182
+ backgroundTasks;
1187
1183
  anchor;
1188
1184
  sessionId;
1189
1185
  waiter;
@@ -1191,6 +1187,7 @@ class DeepseekTransport {
1191
1187
  base;
1192
1188
  constructor(anchor) {
1193
1189
  this.anchor = anchor;
1190
+ this.backgroundTasks = new BackgroundTaskState('deepseek');
1194
1191
  this.sessionId = anchor.agentSessionId;
1195
1192
  this.waiter = null;
1196
1193
  this.socket = null;
@@ -1200,6 +1197,13 @@ class DeepseekTransport {
1200
1197
  this.base = base;
1201
1198
  this.socket = new WebSocket(`${base.replace(/^http/, 'ws')}/api/events.mux`);
1202
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
+ });
1203
1207
  await new Promise((resolve, reject) => {
1204
1208
  this.socket.once('open', resolve);
1205
1209
  this.socket.once('error', reject);
@@ -1223,6 +1227,11 @@ class DeepseekTransport {
1223
1227
  }
1224
1228
  if (String(frame?.payload?.sessionId || '') !== this.sessionId)
1225
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
+ }
1226
1235
  this.anchor.emitLive({
1227
1236
  direction: 'in',
1228
1237
  source: 'deepseek.websocket',
@@ -1291,10 +1300,20 @@ class DeepseekTransport {
1291
1300
  async prompt(text, timeoutMs) {
1292
1301
  const waiter = deferred();
1293
1302
  this.waiter = waiter;
1294
- await this.rpc('session.prompt', { sessionId: this.sessionId, mode: 'queue', content: [{ type: 'text', text }] });
1295
1303
  const timer = setTimeout(() => waiter.reject(new Error('DeepSeek turn timed out.')), timeoutMs);
1296
1304
  timer.unref?.();
1297
- 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
+ });
1298
1317
  }
1299
1318
  interrupt() {
1300
1319
  return this.rpc('session.cancel', { sessionId: this.sessionId });
@@ -1574,7 +1593,7 @@ class Anchor {
1574
1593
  }
1575
1594
  scheduleExit() {
1576
1595
  clearTimeout(this.idleTimer);
1577
- if (this.status === 'running' || this.pendingInteractions.size || this.transport?.backgroundTasks?.size) {
1596
+ if (this.status === 'running' || this.pendingInteractions.size || this.transport?.backgroundTasks?.active) {
1578
1597
  this.idleTimer = null;
1579
1598
  return;
1580
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.16",
3
+ "version": "0.0.17",
4
4
  "description": "Upload local AI agent sessions to turns.dev",
5
5
  "license": "MIT",
6
6
  "type": "module",