@gkorepanov/ccodex 0.4.2 → 0.4.3

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 (36) hide show
  1. package/config/compatibility.json +1 -1
  2. package/dist/claude/environment.d.ts +1 -0
  3. package/dist/claude/environment.js +6 -0
  4. package/dist/claude/environment.js.map +1 -0
  5. package/dist/claude/modelCatalog.js +2 -1
  6. package/dist/claude/modelCatalog.js.map +1 -1
  7. package/dist/claude/service.d.ts +1 -1
  8. package/dist/claude/service.js +7 -4
  9. package/dist/claude/service.js.map +1 -1
  10. package/dist/claude/session/providerRuntime.d.ts +1 -1
  11. package/dist/claude/session/providerRuntime.js +1 -1
  12. package/dist/claude/session/providerRuntime.js.map +1 -1
  13. package/dist/claude/session/providerRuntimeFactory.d.ts +1 -0
  14. package/dist/claude/session/providerRuntimeFactory.js +6 -3
  15. package/dist/claude/session/providerRuntimeFactory.js.map +1 -1
  16. package/dist/claude/session/runtime.d.ts +8 -0
  17. package/dist/claude/session/runtime.js +33 -5
  18. package/dist/claude/session/runtime.js.map +1 -1
  19. package/dist/claude/session/scopeState.d.ts +5 -3
  20. package/dist/claude/session/scopeState.js +3 -0
  21. package/dist/claude/session/scopeState.js.map +1 -1
  22. package/dist/claude/session/session.d.ts +4 -0
  23. package/dist/claude/session/session.js +239 -56
  24. package/dist/claude/session/session.js.map +1 -1
  25. package/dist/claude/session/shellRunner.js +2 -1
  26. package/dist/claude/session/shellRunner.js.map +1 -1
  27. package/dist/claude/toolMapper.js +8 -3
  28. package/dist/claude/toolMapper.js.map +1 -1
  29. package/dist/config/config.d.ts +1 -0
  30. package/dist/config/config.js +4 -1
  31. package/dist/config/config.js.map +1 -1
  32. package/dist/gateway/clientConnection.js +26 -26
  33. package/dist/gateway/clientConnection.js.map +1 -1
  34. package/examples/config.toml +1 -0
  35. package/legal/THIRD_PARTY_NOTICES.md +1 -28
  36. package/package.json +4 -4
@@ -9,7 +9,7 @@ import { ClaudeMailbox, createDeferred, } from "./mailbox.js";
9
9
  import { remapBoundaries } from "./repository.js";
10
10
  import { isImageRead, planSteps, projectToolCompletion, startTool, updateToolInput, } from "../toolMapper.js";
11
11
  import { isFileMutationTool, toolPolicy } from "../permissionPolicy.js";
12
- import { newChildScope, newMainStreamState, scopeProjection, streamItem, toolAt, } from "./scopeState.js";
12
+ import { newChildScope, newMainStreamState, scopeProjection, streamItem, taskUsesProvider, toolAt, } from "./scopeState.js";
13
13
  import { systemNoticeText } from "../../gateway/transientNotice.js";
14
14
  import { isClaudeStatusCommand } from "../statusCommand.js";
15
15
  import { isCCodexStateCommand } from "../../state/stateCommand.js";
@@ -23,6 +23,7 @@ import { mapUserInput } from "../inputMapper.js";
23
23
  import { bashCommandActions } from "../commandActions.js";
24
24
  import { safeSessionPermissionUpdates } from "./permissionUpdates.js";
25
25
  import { StaleClaudeRuntimeSettingsError, createProviderRuntime, providerPermissionMode, providerRuntimeSettings, runtimeTransportSettings, } from "./providerRuntimeFactory.js";
26
+ import { isRetryableClaudeRuntimeStartupError, } from "./providerRuntime.js";
26
27
  import { assistantHasTools, backgroundOutputFile, backgroundTaskId, isNoQueryAcknowledgement, serverToolResult, toolResults, } from "./providerFacts.js";
27
28
  const nullSource = { providerEventId: null, providerEventType: null };
28
29
  const providerJournalMaxEvents = 20_000;
@@ -193,10 +194,26 @@ export class ClaudeSession {
193
194
  }
194
195
  async ensureRuntime(reinitialize = false) {
195
196
  const owner = await this.requireRuntimeOwner();
196
- await owner.ready;
197
+ try {
198
+ await owner.ready;
199
+ }
200
+ catch (error) {
201
+ await owner.activation.promise;
202
+ throw error;
203
+ }
197
204
  if (reinitialize)
198
205
  await owner.runtime.reinitialize();
199
206
  }
207
+ async ensureEphemeralRuntime() {
208
+ try {
209
+ await this.ensureRuntime();
210
+ }
211
+ catch (error) {
212
+ if (!isRetryableClaudeRuntimeStartupError(error))
213
+ throw error;
214
+ await this.ensureRuntime();
215
+ }
216
+ }
200
217
  async materializeRuntime() {
201
218
  for (;;) {
202
219
  const claim = await this.submitLineage({
@@ -558,6 +575,7 @@ export class ClaudeSession {
558
575
  reasoningSummary: record.reasoningSummary,
559
576
  collaborationMode: record.collaborationMode,
560
577
  outputSchema: record.outputSchema,
578
+ interactiveQuestions: this.runtimeDependencies.interactiveQuestions ?? true,
561
579
  };
562
580
  }
563
581
  async createRuntimeGeneration(startup) {
@@ -592,7 +610,8 @@ export class ClaudeSession {
592
610
  const ready = this.waitForProviderRuntime(runtime);
593
611
  await Promise.resolve();
594
612
  if (runtime.hasExited) {
595
- throw new Error(`Claude runtime '${this.threadId}' became unavailable during initialization.`);
613
+ await ready;
614
+ throw runtime.initializationFailure(new Error(`Claude runtime '${this.threadId}' became unavailable during initialization.`));
596
615
  }
597
616
  const transportSettings = runtimeTransportSettings(startup);
598
617
  const rateLimitGeneration = dependencies.rateLimits.register({
@@ -650,6 +669,9 @@ export class ClaudeSession {
650
669
  }),
651
670
  ]);
652
671
  }
672
+ catch (error) {
673
+ throw runtime.initializationFailure(error);
674
+ }
653
675
  finally {
654
676
  if (timer)
655
677
  clearTimeout(timer);
@@ -1152,7 +1174,6 @@ export class ClaudeSession {
1152
1174
  }
1153
1175
  catch (error) {
1154
1176
  failure = error instanceof Error ? error.message : String(error);
1155
- throw error;
1156
1177
  }
1157
1178
  finally {
1158
1179
  await this.submitProviderProjection(fact.runtimeGeneration, {
@@ -1164,6 +1185,15 @@ export class ClaudeSession {
1164
1185
  ...(failure ? { error: failure } : {}),
1165
1186
  });
1166
1187
  }
1188
+ if (failure) {
1189
+ await this.submitProviderProjection(fact.runtimeGeneration, {
1190
+ type: "runtimeExited",
1191
+ runtimeGeneration: fact.runtimeGeneration,
1192
+ message: `Claude provider projection failed: ${failure}`,
1193
+ codexErrorInfo: null,
1194
+ });
1195
+ runtime.beginClose();
1196
+ }
1167
1197
  });
1168
1198
  }
1169
1199
  async projectProviderMessage(projection, runtime, runtimeGeneration, message) {
@@ -1265,7 +1295,9 @@ export class ClaudeSession {
1265
1295
  description: message.summary,
1266
1296
  outputFile: message.output_file,
1267
1297
  }, owner);
1268
- await this.providerSubmitLifecycle(projection, runtimeGeneration, { type: "taskNotification" });
1298
+ if (owner === this.threadId) {
1299
+ await this.providerSubmitLifecycle(projection, runtimeGeneration, { type: "taskNotification" });
1300
+ }
1269
1301
  await this.applyProviderMainStream(projection, runtimeGeneration, {
1270
1302
  kind: "taskComplete",
1271
1303
  taskId: message.task_id,
@@ -1321,9 +1353,12 @@ export class ClaudeSession {
1321
1353
  return "stateOnly";
1322
1354
  }
1323
1355
  if (message.type === "stream_event") {
1324
- const childThreadId = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1325
- if (childThreadId) {
1326
- await this.providerChildStreamEvent(projection, runtimeGeneration, childThreadId, message);
1356
+ const child = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1357
+ if (child?.active) {
1358
+ await this.providerChildStreamEvent(projection, runtimeGeneration, child.threadId, message);
1359
+ }
1360
+ else if (child) {
1361
+ return "retainedOnly";
1327
1362
  }
1328
1363
  else {
1329
1364
  await this.providerMainStreamEvent(projection, runtimeGeneration, message);
@@ -1346,12 +1381,14 @@ export class ClaudeSession {
1346
1381
  if (message.supersedes?.length) {
1347
1382
  await this.providerEvictMessages(projection, runtimeGeneration, message.supersedes);
1348
1383
  }
1349
- const childThreadId = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1350
- if (childThreadId) {
1351
- const itemIds = await this.providerProjectAssistant(projection, runtimeGeneration, message, childThreadId);
1352
- await this.providerLinkItems(runtimeGeneration, message.uuid, childThreadId, itemIds);
1384
+ const child = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1385
+ if (child?.active) {
1386
+ const itemIds = await this.providerProjectAssistant(projection, runtimeGeneration, message, child.threadId);
1387
+ await this.providerLinkItems(runtimeGeneration, message.uuid, child.threadId, itemIds);
1353
1388
  return "projected";
1354
1389
  }
1390
+ if (child)
1391
+ return "retainedOnly";
1355
1392
  if (activeTurnId) {
1356
1393
  if (message.error) {
1357
1394
  await this.submitProviderProjection(runtimeGeneration, {
@@ -1387,14 +1424,16 @@ export class ClaudeSession {
1387
1424
  if ("uuid" in message && typeof message.uuid === "string") {
1388
1425
  runtime.releaseMessage(message.uuid);
1389
1426
  }
1390
- const childThreadId = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1391
- if (childThreadId) {
1392
- const itemIds = await this.providerProjectToolResults(projection, runtimeGeneration, message, childThreadId);
1427
+ const child = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1428
+ if (child?.active) {
1429
+ const itemIds = await this.providerProjectToolResults(projection, runtimeGeneration, message, child.threadId);
1393
1430
  if ("uuid" in message && typeof message.uuid === "string") {
1394
- await this.providerLinkItems(runtimeGeneration, message.uuid, childThreadId, itemIds);
1431
+ await this.providerLinkItems(runtimeGeneration, message.uuid, child.threadId, itemIds);
1395
1432
  }
1396
1433
  return "projected";
1397
1434
  }
1435
+ if (child)
1436
+ return "retainedOnly";
1398
1437
  if (activeTurnId && "uuid" in message && typeof message.uuid === "string") {
1399
1438
  await this.submitProviderProjection(runtimeGeneration, {
1400
1439
  type: "providerBoundary",
@@ -1717,6 +1756,12 @@ export class ClaudeSession {
1717
1756
  if (name === "AskUserQuestion") {
1718
1757
  return this.askProviderUser(projection, runtimeGeneration, input, options);
1719
1758
  }
1759
+ if (providerPermissionMode(settings) === "auto") {
1760
+ return {
1761
+ behavior: "deny",
1762
+ message: options.decisionReason ?? options.description ?? `Claude Auto did not allow tool '${name}'.`,
1763
+ };
1764
+ }
1720
1765
  if (name === "ExitPlanMode") {
1721
1766
  return {
1722
1767
  behavior: "deny",
@@ -2024,11 +2069,14 @@ export class ClaudeSession {
2024
2069
  async providerChildThreadFor(runtimeGeneration, providerId) {
2025
2070
  if (!providerId)
2026
2071
  return undefined;
2027
- return (await this.submitProviderProjection(runtimeGeneration, {
2072
+ const inspection = await this.submitProviderProjection(runtimeGeneration, {
2028
2073
  type: "inspectRuntime",
2029
2074
  runtimeGeneration,
2030
2075
  providerId,
2031
- })).childThreadId ?? undefined;
2076
+ });
2077
+ return inspection.childThreadId
2078
+ ? { threadId: inspection.childThreadId, active: inspection.interruptible }
2079
+ : undefined;
2032
2080
  }
2033
2081
  async providerLinkItems(runtimeGeneration, providerMessageId, ownerThreadId, itemIds) {
2034
2082
  await this.submitProviderProjection(runtimeGeneration, {
@@ -4106,8 +4154,22 @@ export class ClaudeSession {
4106
4154
  if (command.runtimeGeneration !== this.runtimeGeneration
4107
4155
  || this.interruptFence || this.dropLateFacts)
4108
4156
  return undefined;
4109
- if (command.fact.kind === "inspect" && !this.scopes.has(command.ownerThreadId ?? this.threadId))
4110
- return undefined;
4157
+ {
4158
+ const ownerThreadId = command.ownerThreadId ?? this.threadId;
4159
+ if (!this.scopes.has(ownerThreadId)) {
4160
+ if (command.fact.kind === "inspect")
4161
+ return undefined;
4162
+ switch (command.fact.kind) {
4163
+ case "taskStart":
4164
+ case "taskProgress":
4165
+ case "taskMembership":
4166
+ case "taskOutput":
4167
+ case "taskComplete":
4168
+ case "taskStop":
4169
+ return this.projectDetachedTaskFact(command.fact, command.source);
4170
+ }
4171
+ }
4172
+ }
4111
4173
  {
4112
4174
  if (command.fact.kind === "taskComplete" && !command.fact.outputDrained) {
4113
4175
  const task = this.tasks.get(command.fact.taskId);
@@ -4331,27 +4393,14 @@ export class ClaudeSession {
4331
4393
  if (this.dropLateFacts || this.adminOperation && this.adminOperation.kind !== "rename"
4332
4394
  || this.repository.archived(command.threadId))
4333
4395
  return false;
4334
- const record = this.repository.read(command.threadId, false);
4335
- if (!record)
4396
+ const state = this.scopes.get(command.threadId);
4397
+ if (!state || command.requestedTurnId && command.requestedTurnId !== state.turnId
4398
+ || this.repository.readTurn(command.threadId, state.turnId)?.status !== "inProgress")
4336
4399
  return false;
4337
- const turnId = command.requestedTurnId
4338
- ?? (command.threadId === this.threadId ? this.shell?.turnId : undefined)
4339
- ?? this.scopes.get(command.threadId)?.turnId
4340
- ?? record.lastCompletedTurnId
4341
- ?? uuidv7();
4342
- const params = {
4343
- error: {
4344
- message: command.message,
4345
- codexErrorInfo: command.codexErrorInfo,
4346
- additionalDetails: null,
4347
- },
4348
- willRetry: false,
4349
- threadId: command.threadId,
4350
- turnId,
4351
- };
4352
- if (this.repository.appendEvent(command.threadId, turnId, "error", params)) {
4353
- this.output.emit(command.threadId, "error", params);
4354
- }
4400
+ this.projectMainStream(command.threadId, {
4401
+ kind: "instantAgent",
4402
+ text: systemNoticeText(command.message, "error"),
4403
+ }, nullSource);
4355
4404
  return true;
4356
4405
  }
4357
4406
  case "startCompact": {
@@ -5188,8 +5237,13 @@ export class ClaudeSession {
5188
5237
  this.pendingNoQuery = this.pendingInputs = 0;
5189
5238
  this.preparedRuntimeInputs.clear();
5190
5239
  this.disposeRuntimeOperations();
5191
- if (active)
5240
+ if (active) {
5192
5241
  active.commandCompleted = true;
5242
+ active.acknowledged = active.notifications;
5243
+ active.goals = 0;
5244
+ active.goalInFlight = false;
5245
+ delete active.request;
5246
+ }
5193
5247
  this.settleForcedScopes(source);
5194
5248
  if (fact.type === "runtimeExit" && generation !== undefined) {
5195
5249
  this.runtimeGeneration = undefined;
@@ -5199,8 +5253,10 @@ export class ClaudeSession {
5199
5253
  else if (fact.type === "timer" && active?.result && this.hasNotifications()
5200
5254
  && fact.generation === active.notifications && fact.generation > active.diagnosed) {
5201
5255
  active.diagnosed = fact.generation;
5202
- this.projectMainStream(this.threadId, { kind: "instantAgent", text: systemNoticeText("Claude did not emit a request, result, or idle lifecycle signal after a background-task notification. "
5203
- + "The turn remains active; use Stop if needed.", "error") }, source);
5256
+ this.runtimeDependencies?.logger.warn("claude.lifecycle.notification-unacknowledged", {
5257
+ threadId: this.threadId,
5258
+ generation: fact.generation,
5259
+ });
5204
5260
  }
5205
5261
  this.maybeFinish(source, fact.type === "sync");
5206
5262
  }
@@ -5759,7 +5815,7 @@ export class ClaudeSession {
5759
5815
  let owner = this.toolOwner(command.providerId);
5760
5816
  if (!owner && command.ownerProviderId) {
5761
5817
  owner = this.toolOwner(command.ownerProviderId);
5762
- const task = [...this.tasks.values()].find((candidate) => candidate.providerId === command.ownerProviderId || candidate.taskId === command.ownerProviderId);
5818
+ const task = [...this.tasks.values()].find((candidate) => taskUsesProvider(candidate, command.ownerProviderId) || candidate.taskId === command.ownerProviderId);
5763
5819
  if (task?.childThreadId)
5764
5820
  owner = this.scopes.get(task.childThreadId);
5765
5821
  }
@@ -5892,11 +5948,12 @@ export class ClaudeSession {
5892
5948
  }
5893
5949
  let ownerThreadId = command.ownerThreadId ?? this.threadId;
5894
5950
  let targetThreadId = ownerThreadId;
5951
+ let providerTask;
5895
5952
  if (command.providerId) {
5896
5953
  const scope = [...this.scopes.values()].find((candidate) => candidate.tools.has(command.providerId));
5897
- const task = [...this.tasks.values()].find((candidate) => candidate.providerId === command.providerId || candidate.taskId === command.providerId);
5898
- ownerThreadId = task?.ownerThreadId ?? scope?.ownerThreadId ?? ownerThreadId;
5899
- targetThreadId = ownerThreadId;
5954
+ providerTask = [...this.tasks.values()].find((candidate) => taskUsesProvider(candidate, command.providerId) || candidate.taskId === command.providerId);
5955
+ ownerThreadId = providerTask?.ownerThreadId ?? scope?.ownerThreadId ?? ownerThreadId;
5956
+ targetThreadId = providerTask?.childThreadId ?? ownerThreadId;
5900
5957
  }
5901
5958
  const childTask = command.childThreadId
5902
5959
  ? [...this.tasks.values()].find((candidate) => candidate.childThreadId === command.childThreadId)
@@ -5923,12 +5980,14 @@ export class ClaudeSession {
5923
5980
  modelContextWindow: this.requireRecord(false).modelContextWindow,
5924
5981
  interruptible: command.expectedTurnId
5925
5982
  ? targetState?.turnId === command.expectedTurnId
5926
- : childTask ? !childTask.terminal && Boolean(targetState) : Boolean(targetState),
5983
+ : childTask ? !childTask.terminal && Boolean(targetState)
5984
+ : providerTask?.childThreadId ? !providerTask.terminal && Boolean(targetState)
5985
+ : Boolean(targetState),
5927
5986
  ownerThreadId,
5928
5987
  ownerTurnId: ownerState?.turnId ?? null,
5929
5988
  taskIds,
5930
5989
  childThreadId: command.providerId
5931
- ? [...this.tasks.values()].find((task) => task.providerId === command.providerId || task.taskId === command.providerId)?.childThreadId ?? null
5990
+ ? [...this.tasks.values()].find((task) => taskUsesProvider(task, command.providerId) || task.taskId === command.providerId)?.childThreadId ?? null
5932
5991
  : null,
5933
5992
  taskId: childTask?.taskId ?? null,
5934
5993
  quiescent: this.isQuiescent(),
@@ -6346,8 +6405,9 @@ export class ClaudeSession {
6346
6405
  const existing = tool && turn.items.find((item) => item.id === tool.itemId);
6347
6406
  const subagent = Boolean(fact.subagentType) || fact.taskType === "agent" || fact.taskType === "subagent";
6348
6407
  if (!task && !existing && !subagent) {
6408
+ const providerId = fact.providerId ?? fact.taskId;
6349
6409
  task = { taskId: fact.taskId, ownerThreadId: state.ownerThreadId,
6350
- itemId: `claude-task:${fact.taskId}`, providerId: fact.providerId ?? fact.taskId,
6410
+ itemId: `claude-task:${fact.taskId}`, providerId, providerIds: new Set([providerId]),
6351
6411
  turnId: turn.id, childThreadId: undefined, outputFile: fact.outputFile, terminal: false };
6352
6412
  this.tasks.set(task.taskId, task);
6353
6413
  }
@@ -6357,8 +6417,9 @@ export class ClaudeSession {
6357
6417
  status: "inProgress", senderThreadId: state.ownerThreadId, receiverThreadIds: [],
6358
6418
  prompt: fact.prompt ?? fact.description, model: null, reasoningEffort: null, agentsStates: {},
6359
6419
  };
6420
+ const providerId = fact.providerId ?? item.id;
6360
6421
  const createdTask = { taskId: fact.taskId, ownerThreadId: state.ownerThreadId, itemId: item.id,
6361
- providerId: fact.providerId ?? item.id, turnId: turn.id, childThreadId: undefined,
6422
+ providerId, providerIds: new Set([providerId]), turnId: turn.id, childThreadId: undefined,
6362
6423
  outputFile: fact.outputFile, terminal: false };
6363
6424
  task = createdTask;
6364
6425
  this.tasks.set(createdTask.taskId, createdTask);
@@ -6375,6 +6436,25 @@ export class ClaudeSession {
6375
6436
  item.agentsStates = { [childThread.id]: { status: "running", message: fact.description } };
6376
6437
  }
6377
6438
  }
6439
+ const resumed = task?.terminal && subagent && task.childThreadId && fact.providerId
6440
+ && !taskUsesProvider(task, fact.providerId);
6441
+ if (resumed && task?.childThreadId) {
6442
+ childThread = this.resumeChildScope(task.childThreadId, fact, source);
6443
+ task.terminal = false;
6444
+ task.providerId = fact.providerId;
6445
+ task.providerIds.add(fact.providerId);
6446
+ task.itemId = existing?.id ?? task.itemId;
6447
+ task.turnId = turn.id;
6448
+ task.outputFile = fact.outputFile;
6449
+ if (existing?.type === "collabAgentToolCall") {
6450
+ existing.receiverThreadIds = [task.childThreadId];
6451
+ existing.agentsStates = {
6452
+ [task.childThreadId]: { status: "running", message: fact.description },
6453
+ };
6454
+ if (tool)
6455
+ this.beginTool(turn, state, tool, true, source);
6456
+ }
6457
+ }
6378
6458
  if (task && tool) {
6379
6459
  tool.backgroundTaskId = task.taskId;
6380
6460
  if (task.outputFile)
@@ -6402,8 +6482,24 @@ export class ClaudeSession {
6402
6482
  handled = taskIds.length > 0;
6403
6483
  }
6404
6484
  else if (task) {
6405
- const ownerState = this.requireMainStreamState(task.ownerThreadId);
6485
+ const ownerState = this.scopes.get(task.ownerThreadId);
6406
6486
  const ownerTurn = this.repository.readTurn(task.ownerThreadId, task.turnId);
6487
+ if (!ownerState || !ownerTurn || ownerTurn.status !== "inProgress") {
6488
+ if (fact.kind === "taskComplete" && !task.terminal) {
6489
+ this.stopBackgroundTailer(task);
6490
+ task.outputFile = fact.outputFile ?? task.outputFile;
6491
+ task.terminal = true;
6492
+ if (task.childThreadId)
6493
+ this.finishChildScope(task.childThreadId, fact.status, fact.summary, source);
6494
+ }
6495
+ return scopeProjection(turn, {
6496
+ handled: fact.kind === "taskComplete",
6497
+ taskIds: [task.taskId],
6498
+ outputFile: task.outputFile,
6499
+ childThreadId: task.childThreadId,
6500
+ terminal: task.terminal,
6501
+ });
6502
+ }
6407
6503
  const item = ownerTurn.items.find((candidate) => candidate.id === task.itemId);
6408
6504
  if (fact.kind === "taskProgress") {
6409
6505
  if (item?.type === "collabAgentToolCall")
@@ -6444,10 +6540,12 @@ export class ClaudeSession {
6444
6540
  status: fact.status === "completed" ? "completed" : fact.status === "stopped" ? "interrupted" : "errored",
6445
6541
  message: fact.summary,
6446
6542
  };
6447
- ownerState.completedItems.add(item.id);
6448
- this.publishTurn(ownerTurn, "item/completed", {
6449
- item, threadId: task.ownerThreadId, turnId: ownerTurn.id, completedAtMs: Date.now(),
6450
- }, source);
6543
+ if (item.tool !== "sendInput" || !ownerState.completedItems.has(item.id)) {
6544
+ ownerState.completedItems.add(item.id);
6545
+ this.publishTurn(ownerTurn, "item/completed", {
6546
+ item, threadId: task.ownerThreadId, turnId: ownerTurn.id, completedAtMs: Date.now(),
6547
+ }, source);
6548
+ }
6451
6549
  }
6452
6550
  if (task.childThreadId)
6453
6551
  this.finishChildScope(task.childThreadId, fact.status, fact.summary, source);
@@ -6488,11 +6586,49 @@ export class ClaudeSession {
6488
6586
  tool: task ? this.scopes.get(task.ownerThreadId)?.tools.get(task.providerId) : undefined,
6489
6587
  });
6490
6588
  }
6589
+ projectDetachedTaskFact(fact, source) {
6590
+ if (!("taskId" in fact))
6591
+ return undefined;
6592
+ const task = this.tasks.get(fact.taskId);
6593
+ if (!task)
6594
+ return undefined;
6595
+ const turn = this.repository.readTurn(task.ownerThreadId, task.turnId);
6596
+ if (!turn)
6597
+ return undefined;
6598
+ if (fact.kind === "taskStart") {
6599
+ task.outputFile = fact.outputFile ?? task.outputFile;
6600
+ }
6601
+ else if (fact.kind === "taskComplete" && !task.terminal) {
6602
+ this.stopBackgroundTailer(task);
6603
+ task.outputFile = fact.outputFile ?? task.outputFile;
6604
+ task.terminal = true;
6605
+ if (task.childThreadId)
6606
+ this.finishChildScope(task.childThreadId, fact.status, fact.summary, source);
6607
+ }
6608
+ return scopeProjection(turn, {
6609
+ handled: true,
6610
+ taskIds: [task.taskId],
6611
+ outputFile: task.outputFile,
6612
+ childThreadId: task.childThreadId,
6613
+ terminal: task.terminal,
6614
+ });
6615
+ }
6491
6616
  updateTool(turn, state, tool, input, source) {
6492
6617
  const item = turn.items.find((candidate) => candidate.id === tool.itemId);
6493
6618
  if (!item)
6494
6619
  return;
6495
6620
  updateToolInput(item, tool, input, state.record.thread.cwd);
6621
+ if (item.type === "collabAgentToolCall" && item.tool === "sendInput") {
6622
+ const target = typeof input.to === "string" ? input.to
6623
+ : typeof input.recipient === "string" ? input.recipient : undefined;
6624
+ const task = target ? this.tasks.get(target) : undefined;
6625
+ if (task?.childThreadId) {
6626
+ item.receiverThreadIds = [task.childThreadId];
6627
+ item.agentsStates = {
6628
+ [task.childThreadId]: { status: task.terminal ? "completed" : "running", message: null },
6629
+ };
6630
+ }
6631
+ }
6496
6632
  if (tool.name === "TaskOutput") {
6497
6633
  const id = typeof input.task_id === "string" ? input.task_id
6498
6634
  : typeof input.taskId === "string" ? input.taskId : undefined;
@@ -6511,6 +6647,9 @@ export class ClaudeSession {
6511
6647
  if (!item || tool.started || tool.foldedTaskId || isImageRead(tool, record.thread.cwd)
6512
6648
  || item.type === "commandExecution" && !item.command)
6513
6649
  return;
6650
+ if (item.type === "collabAgentToolCall" && item.tool === "sendInput"
6651
+ && (!item.prompt || item.receiverThreadIds.length === 0))
6652
+ return;
6514
6653
  if (tool.name === "Bash" && tool.input.run_in_background === true && !tool.backgroundTaskId)
6515
6654
  return;
6516
6655
  if (!force
@@ -6565,6 +6704,50 @@ export class ClaudeSession {
6565
6704
  this.publishAt(childThreadId, turn.id, "item/completed", { item, threadId: childThreadId, turnId: turn.id, completedAtMs: Date.now() }, source);
6566
6705
  return thread;
6567
6706
  }
6707
+ resumeChildScope(childThreadId, fact, source) {
6708
+ const record = this.repository.read(childThreadId, false);
6709
+ if (!record)
6710
+ throw new Error(`Unknown Claude child thread '${childThreadId}'.`);
6711
+ const startedAt = Math.floor(Date.now() / 1_000);
6712
+ const text = fact.prompt ?? fact.description;
6713
+ const item = {
6714
+ type: "userMessage", id: uuidv7(), clientId: null,
6715
+ content: [{ type: "text", text, text_elements: [] }],
6716
+ };
6717
+ const turn = {
6718
+ id: uuidv7(), items: [item], itemsView: "full", status: "inProgress", error: null,
6719
+ startedAt, completedAt: null, durationMs: null,
6720
+ };
6721
+ const updated = {
6722
+ ...record,
6723
+ thread: {
6724
+ ...record.thread,
6725
+ preview: text,
6726
+ status: { type: "active", activeFlags: [] },
6727
+ updatedAt: startedAt,
6728
+ recencyAt: startedAt,
6729
+ },
6730
+ };
6731
+ const status = { threadId: childThreadId, status: updated.thread.status };
6732
+ this.commitState(updated, [{
6733
+ turnId: turn.id,
6734
+ method: "thread/status/changed",
6735
+ params: status,
6736
+ providerEventId: source.providerEventId,
6737
+ providerEventType: source.providerEventType,
6738
+ }], turn, true);
6739
+ const state = newMainStreamState(childThreadId, turn.id, updated);
6740
+ state.completedItems.add(item.id);
6741
+ this.scopes.set(childThreadId, state);
6742
+ this.publishAt(childThreadId, turn.id, "turn/started", { threadId: childThreadId, turn }, source);
6743
+ this.publishAt(childThreadId, turn.id, "item/started", {
6744
+ item, threadId: childThreadId, turnId: turn.id, startedAtMs: Date.now(),
6745
+ }, source);
6746
+ this.publishAt(childThreadId, turn.id, "item/completed", {
6747
+ item, threadId: childThreadId, turnId: turn.id, completedAtMs: Date.now(),
6748
+ }, source);
6749
+ return updated.thread;
6750
+ }
6568
6751
  finishChildScope(childThreadId, status, summary, source) {
6569
6752
  const state = this.scopes.get(childThreadId);
6570
6753
  const record = this.repository.read(childThreadId, false);