@gkorepanov/ccodex 0.4.2 → 0.4.4

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 (44) 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 +2 -1
  8. package/dist/claude/service.js +27 -5
  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 +9 -4
  20. package/dist/claude/session/scopeState.js +6 -2
  21. package/dist/claude/session/scopeState.js.map +1 -1
  22. package/dist/claude/session/session.d.ts +8 -0
  23. package/dist/claude/session/session.js +262 -59
  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/dist/gateway/subscriptions.d.ts +1 -0
  35. package/dist/gateway/subscriptions.js +4 -0
  36. package/dist/gateway/subscriptions.js.map +1 -1
  37. package/dist/handoff/service.js +27 -11
  38. package/dist/handoff/service.js.map +1 -1
  39. package/dist/handoff/store.d.ts +1 -0
  40. package/dist/handoff/store.js +6 -0
  41. package/dist/handoff/store.js.map +1 -1
  42. package/examples/config.toml +1 -0
  43. package/legal/THIRD_PARTY_NOTICES.md +1 -28
  44. 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,7 +23,9 @@ 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";
28
+ import { normalizeClaudeModelIdentifier } from "../modelSelection.js";
27
29
  const nullSource = { providerEventId: null, providerEventType: null };
28
30
  const providerJournalMaxEvents = 20_000;
29
31
  const providerJournalMaxBytes = 256 * 1_024 * 1_024;
@@ -193,10 +195,26 @@ export class ClaudeSession {
193
195
  }
194
196
  async ensureRuntime(reinitialize = false) {
195
197
  const owner = await this.requireRuntimeOwner();
196
- await owner.ready;
198
+ try {
199
+ await owner.ready;
200
+ }
201
+ catch (error) {
202
+ await owner.activation.promise;
203
+ throw error;
204
+ }
197
205
  if (reinitialize)
198
206
  await owner.runtime.reinitialize();
199
207
  }
208
+ async ensureEphemeralRuntime() {
209
+ try {
210
+ await this.ensureRuntime();
211
+ }
212
+ catch (error) {
213
+ if (!isRetryableClaudeRuntimeStartupError(error))
214
+ throw error;
215
+ await this.ensureRuntime();
216
+ }
217
+ }
200
218
  async materializeRuntime() {
201
219
  for (;;) {
202
220
  const claim = await this.submitLineage({
@@ -558,6 +576,7 @@ export class ClaudeSession {
558
576
  reasoningSummary: record.reasoningSummary,
559
577
  collaborationMode: record.collaborationMode,
560
578
  outputSchema: record.outputSchema,
579
+ interactiveQuestions: this.runtimeDependencies.interactiveQuestions ?? true,
561
580
  };
562
581
  }
563
582
  async createRuntimeGeneration(startup) {
@@ -592,7 +611,8 @@ export class ClaudeSession {
592
611
  const ready = this.waitForProviderRuntime(runtime);
593
612
  await Promise.resolve();
594
613
  if (runtime.hasExited) {
595
- throw new Error(`Claude runtime '${this.threadId}' became unavailable during initialization.`);
614
+ await ready;
615
+ throw runtime.initializationFailure(new Error(`Claude runtime '${this.threadId}' became unavailable during initialization.`));
596
616
  }
597
617
  const transportSettings = runtimeTransportSettings(startup);
598
618
  const rateLimitGeneration = dependencies.rateLimits.register({
@@ -650,6 +670,9 @@ export class ClaudeSession {
650
670
  }),
651
671
  ]);
652
672
  }
673
+ catch (error) {
674
+ throw runtime.initializationFailure(error);
675
+ }
653
676
  finally {
654
677
  if (timer)
655
678
  clearTimeout(timer);
@@ -1152,7 +1175,6 @@ export class ClaudeSession {
1152
1175
  }
1153
1176
  catch (error) {
1154
1177
  failure = error instanceof Error ? error.message : String(error);
1155
- throw error;
1156
1178
  }
1157
1179
  finally {
1158
1180
  await this.submitProviderProjection(fact.runtimeGeneration, {
@@ -1164,6 +1186,15 @@ export class ClaudeSession {
1164
1186
  ...(failure ? { error: failure } : {}),
1165
1187
  });
1166
1188
  }
1189
+ if (failure) {
1190
+ await this.submitProviderProjection(fact.runtimeGeneration, {
1191
+ type: "runtimeExited",
1192
+ runtimeGeneration: fact.runtimeGeneration,
1193
+ message: `Claude provider projection failed: ${failure}`,
1194
+ codexErrorInfo: null,
1195
+ });
1196
+ runtime.beginClose();
1197
+ }
1167
1198
  });
1168
1199
  }
1169
1200
  async projectProviderMessage(projection, runtime, runtimeGeneration, message) {
@@ -1265,7 +1296,9 @@ export class ClaudeSession {
1265
1296
  description: message.summary,
1266
1297
  outputFile: message.output_file,
1267
1298
  }, owner);
1268
- await this.providerSubmitLifecycle(projection, runtimeGeneration, { type: "taskNotification" });
1299
+ if (owner === this.threadId) {
1300
+ await this.providerSubmitLifecycle(projection, runtimeGeneration, { type: "taskNotification" });
1301
+ }
1269
1302
  await this.applyProviderMainStream(projection, runtimeGeneration, {
1270
1303
  kind: "taskComplete",
1271
1304
  taskId: message.task_id,
@@ -1321,9 +1354,12 @@ export class ClaudeSession {
1321
1354
  return "stateOnly";
1322
1355
  }
1323
1356
  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);
1357
+ const child = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1358
+ if (child?.active) {
1359
+ await this.providerChildStreamEvent(projection, runtimeGeneration, child.threadId, message);
1360
+ }
1361
+ else if (child) {
1362
+ return "retainedOnly";
1327
1363
  }
1328
1364
  else {
1329
1365
  await this.providerMainStreamEvent(projection, runtimeGeneration, message);
@@ -1346,12 +1382,14 @@ export class ClaudeSession {
1346
1382
  if (message.supersedes?.length) {
1347
1383
  await this.providerEvictMessages(projection, runtimeGeneration, message.supersedes);
1348
1384
  }
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);
1385
+ const child = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1386
+ if (child?.active) {
1387
+ const itemIds = await this.providerProjectAssistant(projection, runtimeGeneration, message, child.threadId);
1388
+ await this.providerLinkItems(runtimeGeneration, message.uuid, child.threadId, itemIds);
1353
1389
  return "projected";
1354
1390
  }
1391
+ if (child)
1392
+ return "retainedOnly";
1355
1393
  if (activeTurnId) {
1356
1394
  if (message.error) {
1357
1395
  await this.submitProviderProjection(runtimeGeneration, {
@@ -1387,14 +1425,16 @@ export class ClaudeSession {
1387
1425
  if ("uuid" in message && typeof message.uuid === "string") {
1388
1426
  runtime.releaseMessage(message.uuid);
1389
1427
  }
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);
1428
+ const child = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1429
+ if (child?.active) {
1430
+ const itemIds = await this.providerProjectToolResults(projection, runtimeGeneration, message, child.threadId);
1393
1431
  if ("uuid" in message && typeof message.uuid === "string") {
1394
- await this.providerLinkItems(runtimeGeneration, message.uuid, childThreadId, itemIds);
1432
+ await this.providerLinkItems(runtimeGeneration, message.uuid, child.threadId, itemIds);
1395
1433
  }
1396
1434
  return "projected";
1397
1435
  }
1436
+ if (child)
1437
+ return "retainedOnly";
1398
1438
  if (activeTurnId && "uuid" in message && typeof message.uuid === "string") {
1399
1439
  await this.submitProviderProjection(runtimeGeneration, {
1400
1440
  type: "providerBoundary",
@@ -1717,6 +1757,12 @@ export class ClaudeSession {
1717
1757
  if (name === "AskUserQuestion") {
1718
1758
  return this.askProviderUser(projection, runtimeGeneration, input, options);
1719
1759
  }
1760
+ if (providerPermissionMode(settings) === "auto") {
1761
+ return {
1762
+ behavior: "deny",
1763
+ message: options.decisionReason ?? options.description ?? `Claude Auto did not allow tool '${name}'.`,
1764
+ };
1765
+ }
1720
1766
  if (name === "ExitPlanMode") {
1721
1767
  return {
1722
1768
  behavior: "deny",
@@ -2024,11 +2070,14 @@ export class ClaudeSession {
2024
2070
  async providerChildThreadFor(runtimeGeneration, providerId) {
2025
2071
  if (!providerId)
2026
2072
  return undefined;
2027
- return (await this.submitProviderProjection(runtimeGeneration, {
2073
+ const inspection = await this.submitProviderProjection(runtimeGeneration, {
2028
2074
  type: "inspectRuntime",
2029
2075
  runtimeGeneration,
2030
2076
  providerId,
2031
- })).childThreadId ?? undefined;
2077
+ });
2078
+ return inspection.childThreadId
2079
+ ? { threadId: inspection.childThreadId, active: inspection.interruptible }
2080
+ : undefined;
2032
2081
  }
2033
2082
  async providerLinkItems(runtimeGeneration, providerMessageId, ownerThreadId, itemIds) {
2034
2083
  await this.submitProviderProjection(runtimeGeneration, {
@@ -4106,8 +4155,22 @@ export class ClaudeSession {
4106
4155
  if (command.runtimeGeneration !== this.runtimeGeneration
4107
4156
  || this.interruptFence || this.dropLateFacts)
4108
4157
  return undefined;
4109
- if (command.fact.kind === "inspect" && !this.scopes.has(command.ownerThreadId ?? this.threadId))
4110
- return undefined;
4158
+ {
4159
+ const ownerThreadId = command.ownerThreadId ?? this.threadId;
4160
+ if (!this.scopes.has(ownerThreadId)) {
4161
+ if (command.fact.kind === "inspect")
4162
+ return undefined;
4163
+ switch (command.fact.kind) {
4164
+ case "taskStart":
4165
+ case "taskProgress":
4166
+ case "taskMembership":
4167
+ case "taskOutput":
4168
+ case "taskComplete":
4169
+ case "taskStop":
4170
+ return this.projectDetachedTaskFact(command.fact, command.source);
4171
+ }
4172
+ }
4173
+ }
4111
4174
  {
4112
4175
  if (command.fact.kind === "taskComplete" && !command.fact.outputDrained) {
4113
4176
  const task = this.tasks.get(command.fact.taskId);
@@ -4331,27 +4394,14 @@ export class ClaudeSession {
4331
4394
  if (this.dropLateFacts || this.adminOperation && this.adminOperation.kind !== "rename"
4332
4395
  || this.repository.archived(command.threadId))
4333
4396
  return false;
4334
- const record = this.repository.read(command.threadId, false);
4335
- if (!record)
4397
+ const state = this.scopes.get(command.threadId);
4398
+ if (!state || command.requestedTurnId && command.requestedTurnId !== state.turnId
4399
+ || this.repository.readTurn(command.threadId, state.turnId)?.status !== "inProgress")
4336
4400
  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
- }
4401
+ this.projectMainStream(command.threadId, {
4402
+ kind: "instantAgent",
4403
+ text: systemNoticeText(command.message, "error"),
4404
+ }, nullSource);
4355
4405
  return true;
4356
4406
  }
4357
4407
  case "startCompact": {
@@ -5188,8 +5238,13 @@ export class ClaudeSession {
5188
5238
  this.pendingNoQuery = this.pendingInputs = 0;
5189
5239
  this.preparedRuntimeInputs.clear();
5190
5240
  this.disposeRuntimeOperations();
5191
- if (active)
5241
+ if (active) {
5192
5242
  active.commandCompleted = true;
5243
+ active.acknowledged = active.notifications;
5244
+ active.goals = 0;
5245
+ active.goalInFlight = false;
5246
+ delete active.request;
5247
+ }
5193
5248
  this.settleForcedScopes(source);
5194
5249
  if (fact.type === "runtimeExit" && generation !== undefined) {
5195
5250
  this.runtimeGeneration = undefined;
@@ -5199,8 +5254,10 @@ export class ClaudeSession {
5199
5254
  else if (fact.type === "timer" && active?.result && this.hasNotifications()
5200
5255
  && fact.generation === active.notifications && fact.generation > active.diagnosed) {
5201
5256
  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);
5257
+ this.runtimeDependencies?.logger.warn("claude.lifecycle.notification-unacknowledged", {
5258
+ threadId: this.threadId,
5259
+ generation: fact.generation,
5260
+ });
5204
5261
  }
5205
5262
  this.maybeFinish(source, fact.type === "sync");
5206
5263
  }
@@ -5759,7 +5816,7 @@ export class ClaudeSession {
5759
5816
  let owner = this.toolOwner(command.providerId);
5760
5817
  if (!owner && command.ownerProviderId) {
5761
5818
  owner = this.toolOwner(command.ownerProviderId);
5762
- const task = [...this.tasks.values()].find((candidate) => candidate.providerId === command.ownerProviderId || candidate.taskId === command.ownerProviderId);
5819
+ const task = [...this.tasks.values()].find((candidate) => taskUsesProvider(candidate, command.ownerProviderId) || candidate.taskId === command.ownerProviderId);
5763
5820
  if (task?.childThreadId)
5764
5821
  owner = this.scopes.get(task.childThreadId);
5765
5822
  }
@@ -5892,11 +5949,12 @@ export class ClaudeSession {
5892
5949
  }
5893
5950
  let ownerThreadId = command.ownerThreadId ?? this.threadId;
5894
5951
  let targetThreadId = ownerThreadId;
5952
+ let providerTask;
5895
5953
  if (command.providerId) {
5896
5954
  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;
5955
+ providerTask = [...this.tasks.values()].find((candidate) => taskUsesProvider(candidate, command.providerId) || candidate.taskId === command.providerId);
5956
+ ownerThreadId = providerTask?.ownerThreadId ?? scope?.ownerThreadId ?? ownerThreadId;
5957
+ targetThreadId = providerTask?.childThreadId ?? ownerThreadId;
5900
5958
  }
5901
5959
  const childTask = command.childThreadId
5902
5960
  ? [...this.tasks.values()].find((candidate) => candidate.childThreadId === command.childThreadId)
@@ -5923,12 +5981,14 @@ export class ClaudeSession {
5923
5981
  modelContextWindow: this.requireRecord(false).modelContextWindow,
5924
5982
  interruptible: command.expectedTurnId
5925
5983
  ? targetState?.turnId === command.expectedTurnId
5926
- : childTask ? !childTask.terminal && Boolean(targetState) : Boolean(targetState),
5984
+ : childTask ? !childTask.terminal && Boolean(targetState)
5985
+ : providerTask?.childThreadId ? !providerTask.terminal && Boolean(targetState)
5986
+ : Boolean(targetState),
5927
5987
  ownerThreadId,
5928
5988
  ownerTurnId: ownerState?.turnId ?? null,
5929
5989
  taskIds,
5930
5990
  childThreadId: command.providerId
5931
- ? [...this.tasks.values()].find((task) => task.providerId === command.providerId || task.taskId === command.providerId)?.childThreadId ?? null
5991
+ ? [...this.tasks.values()].find((task) => taskUsesProvider(task, command.providerId) || task.taskId === command.providerId)?.childThreadId ?? null
5932
5992
  : null,
5933
5993
  taskId: childTask?.taskId ?? null,
5934
5994
  quiescent: this.isQuiescent(),
@@ -6346,8 +6406,9 @@ export class ClaudeSession {
6346
6406
  const existing = tool && turn.items.find((item) => item.id === tool.itemId);
6347
6407
  const subagent = Boolean(fact.subagentType) || fact.taskType === "agent" || fact.taskType === "subagent";
6348
6408
  if (!task && !existing && !subagent) {
6409
+ const providerId = fact.providerId ?? fact.taskId;
6349
6410
  task = { taskId: fact.taskId, ownerThreadId: state.ownerThreadId,
6350
- itemId: `claude-task:${fact.taskId}`, providerId: fact.providerId ?? fact.taskId,
6411
+ itemId: `claude-task:${fact.taskId}`, providerId, providerIds: new Set([providerId]),
6351
6412
  turnId: turn.id, childThreadId: undefined, outputFile: fact.outputFile, terminal: false };
6352
6413
  this.tasks.set(task.taskId, task);
6353
6414
  }
@@ -6357,8 +6418,9 @@ export class ClaudeSession {
6357
6418
  status: "inProgress", senderThreadId: state.ownerThreadId, receiverThreadIds: [],
6358
6419
  prompt: fact.prompt ?? fact.description, model: null, reasoningEffort: null, agentsStates: {},
6359
6420
  };
6421
+ const providerId = fact.providerId ?? item.id;
6360
6422
  const createdTask = { taskId: fact.taskId, ownerThreadId: state.ownerThreadId, itemId: item.id,
6361
- providerId: fact.providerId ?? item.id, turnId: turn.id, childThreadId: undefined,
6423
+ providerId, providerIds: new Set([providerId]), turnId: turn.id, childThreadId: undefined,
6362
6424
  outputFile: fact.outputFile, terminal: false };
6363
6425
  task = createdTask;
6364
6426
  this.tasks.set(createdTask.taskId, createdTask);
@@ -6369,12 +6431,32 @@ export class ClaudeSession {
6369
6431
  }, source);
6370
6432
  }
6371
6433
  if (subagent && item.type === "collabAgentToolCall") {
6372
- childThread = this.createChildScope(state.ownerThreadId, fact, source);
6434
+ const requestedModel = typeof tool?.input.model === "string" ? tool.input.model : undefined;
6435
+ childThread = this.createChildScope(state.ownerThreadId, fact, source, requestedModel);
6373
6436
  task.childThreadId = childThread.id;
6374
6437
  item.receiverThreadIds = [childThread.id];
6375
6438
  item.agentsStates = { [childThread.id]: { status: "running", message: fact.description } };
6376
6439
  }
6377
6440
  }
6441
+ const resumed = task?.terminal && subagent && task.childThreadId && fact.providerId
6442
+ && !taskUsesProvider(task, fact.providerId);
6443
+ if (resumed && task?.childThreadId) {
6444
+ childThread = this.resumeChildScope(task.childThreadId, fact, source);
6445
+ task.terminal = false;
6446
+ task.providerId = fact.providerId;
6447
+ task.providerIds.add(fact.providerId);
6448
+ task.itemId = existing?.id ?? task.itemId;
6449
+ task.turnId = turn.id;
6450
+ task.outputFile = fact.outputFile;
6451
+ if (existing?.type === "collabAgentToolCall") {
6452
+ existing.receiverThreadIds = [task.childThreadId];
6453
+ existing.agentsStates = {
6454
+ [task.childThreadId]: { status: "running", message: fact.description },
6455
+ };
6456
+ if (tool)
6457
+ this.beginTool(turn, state, tool, true, source);
6458
+ }
6459
+ }
6378
6460
  if (task && tool) {
6379
6461
  tool.backgroundTaskId = task.taskId;
6380
6462
  if (task.outputFile)
@@ -6402,8 +6484,24 @@ export class ClaudeSession {
6402
6484
  handled = taskIds.length > 0;
6403
6485
  }
6404
6486
  else if (task) {
6405
- const ownerState = this.requireMainStreamState(task.ownerThreadId);
6487
+ const ownerState = this.scopes.get(task.ownerThreadId);
6406
6488
  const ownerTurn = this.repository.readTurn(task.ownerThreadId, task.turnId);
6489
+ if (!ownerState || !ownerTurn || ownerTurn.status !== "inProgress") {
6490
+ if (fact.kind === "taskComplete" && !task.terminal) {
6491
+ this.stopBackgroundTailer(task);
6492
+ task.outputFile = fact.outputFile ?? task.outputFile;
6493
+ task.terminal = true;
6494
+ if (task.childThreadId)
6495
+ this.finishChildScope(task.childThreadId, fact.status, fact.summary, source);
6496
+ }
6497
+ return scopeProjection(turn, {
6498
+ handled: fact.kind === "taskComplete",
6499
+ taskIds: [task.taskId],
6500
+ outputFile: task.outputFile,
6501
+ childThreadId: task.childThreadId,
6502
+ terminal: task.terminal,
6503
+ });
6504
+ }
6407
6505
  const item = ownerTurn.items.find((candidate) => candidate.id === task.itemId);
6408
6506
  if (fact.kind === "taskProgress") {
6409
6507
  if (item?.type === "collabAgentToolCall")
@@ -6444,10 +6542,12 @@ export class ClaudeSession {
6444
6542
  status: fact.status === "completed" ? "completed" : fact.status === "stopped" ? "interrupted" : "errored",
6445
6543
  message: fact.summary,
6446
6544
  };
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);
6545
+ if (item.tool !== "sendInput" || !ownerState.completedItems.has(item.id)) {
6546
+ ownerState.completedItems.add(item.id);
6547
+ this.publishTurn(ownerTurn, "item/completed", {
6548
+ item, threadId: task.ownerThreadId, turnId: ownerTurn.id, completedAtMs: Date.now(),
6549
+ }, source);
6550
+ }
6451
6551
  }
6452
6552
  if (task.childThreadId)
6453
6553
  this.finishChildScope(task.childThreadId, fact.status, fact.summary, source);
@@ -6488,11 +6588,49 @@ export class ClaudeSession {
6488
6588
  tool: task ? this.scopes.get(task.ownerThreadId)?.tools.get(task.providerId) : undefined,
6489
6589
  });
6490
6590
  }
6591
+ projectDetachedTaskFact(fact, source) {
6592
+ if (!("taskId" in fact))
6593
+ return undefined;
6594
+ const task = this.tasks.get(fact.taskId);
6595
+ if (!task)
6596
+ return undefined;
6597
+ const turn = this.repository.readTurn(task.ownerThreadId, task.turnId);
6598
+ if (!turn)
6599
+ return undefined;
6600
+ if (fact.kind === "taskStart") {
6601
+ task.outputFile = fact.outputFile ?? task.outputFile;
6602
+ }
6603
+ else if (fact.kind === "taskComplete" && !task.terminal) {
6604
+ this.stopBackgroundTailer(task);
6605
+ task.outputFile = fact.outputFile ?? task.outputFile;
6606
+ task.terminal = true;
6607
+ if (task.childThreadId)
6608
+ this.finishChildScope(task.childThreadId, fact.status, fact.summary, source);
6609
+ }
6610
+ return scopeProjection(turn, {
6611
+ handled: true,
6612
+ taskIds: [task.taskId],
6613
+ outputFile: task.outputFile,
6614
+ childThreadId: task.childThreadId,
6615
+ terminal: task.terminal,
6616
+ });
6617
+ }
6491
6618
  updateTool(turn, state, tool, input, source) {
6492
6619
  const item = turn.items.find((candidate) => candidate.id === tool.itemId);
6493
6620
  if (!item)
6494
6621
  return;
6495
6622
  updateToolInput(item, tool, input, state.record.thread.cwd);
6623
+ if (item.type === "collabAgentToolCall" && item.tool === "sendInput") {
6624
+ const target = typeof input.to === "string" ? input.to
6625
+ : typeof input.recipient === "string" ? input.recipient : undefined;
6626
+ const task = target ? this.tasks.get(target) : undefined;
6627
+ if (task?.childThreadId) {
6628
+ item.receiverThreadIds = [task.childThreadId];
6629
+ item.agentsStates = {
6630
+ [task.childThreadId]: { status: task.terminal ? "completed" : "running", message: null },
6631
+ };
6632
+ }
6633
+ }
6496
6634
  if (tool.name === "TaskOutput") {
6497
6635
  const id = typeof input.task_id === "string" ? input.task_id
6498
6636
  : typeof input.taskId === "string" ? input.taskId : undefined;
@@ -6511,6 +6649,9 @@ export class ClaudeSession {
6511
6649
  if (!item || tool.started || tool.foldedTaskId || isImageRead(tool, record.thread.cwd)
6512
6650
  || item.type === "commandExecution" && !item.command)
6513
6651
  return;
6652
+ if (item.type === "collabAgentToolCall" && item.tool === "sendInput"
6653
+ && (!item.prompt || item.receiverThreadIds.length === 0))
6654
+ return;
6514
6655
  if (tool.name === "Bash" && tool.input.run_in_background === true && !tool.backgroundTaskId)
6515
6656
  return;
6516
6657
  if (!force
@@ -6536,6 +6677,16 @@ export class ClaudeSession {
6536
6677
  const projection = projectToolCompletion(item, tool, output, isError, result, state.record.thread.cwd);
6537
6678
  turn.items[index] = projection.completed;
6538
6679
  state.completedItems.add(item.id);
6680
+ if (projection.completed.type === "collabAgentToolCall"
6681
+ && projection.completed.tool === "spawnAgent" && projection.completed.model) {
6682
+ const childThreadId = projection.completed.receiverThreadIds[0];
6683
+ const child = childThreadId ? this.repository.read(childThreadId, false) : undefined;
6684
+ if (child)
6685
+ this.repository.update({
6686
+ ...child,
6687
+ resolvedModel: normalizeClaudeModelIdentifier(projection.completed.model),
6688
+ });
6689
+ }
6539
6690
  if (!tool.started) {
6540
6691
  tool.started = true;
6541
6692
  this.publishTurn(turn, "item/started", {
@@ -6548,9 +6699,17 @@ export class ClaudeSession {
6548
6699
  item: projection.completed, threadId: state.ownerThreadId, turnId: turn.id, completedAtMs: Date.now(),
6549
6700
  }, source);
6550
6701
  }
6551
- createChildScope(parentThreadId, fact, source) {
6702
+ createChildScope(parentThreadId, fact, source, requestedModel) {
6552
6703
  const parent = this.repository.read(parentThreadId, false);
6553
- const { record, turn, item } = newChildScope(parent, parentThreadId, fact);
6704
+ const normalized = requestedModel && requestedModel !== "inherit"
6705
+ ? normalizeClaudeModelIdentifier(requestedModel) : undefined;
6706
+ const model = normalized
6707
+ ? this.runtimeDependencies?.resolveChildModel?.(normalized) ?? {
6708
+ modelPickerId: `claude:${normalized}`,
6709
+ claudeModelValue: normalized,
6710
+ }
6711
+ : undefined;
6712
+ const { record, turn, item } = newChildScope(parent, parentThreadId, fact, model);
6554
6713
  const { thread } = record;
6555
6714
  const childThreadId = thread.id;
6556
6715
  this.repository.create(record);
@@ -6565,6 +6724,50 @@ export class ClaudeSession {
6565
6724
  this.publishAt(childThreadId, turn.id, "item/completed", { item, threadId: childThreadId, turnId: turn.id, completedAtMs: Date.now() }, source);
6566
6725
  return thread;
6567
6726
  }
6727
+ resumeChildScope(childThreadId, fact, source) {
6728
+ const record = this.repository.read(childThreadId, false);
6729
+ if (!record)
6730
+ throw new Error(`Unknown Claude child thread '${childThreadId}'.`);
6731
+ const startedAt = Math.floor(Date.now() / 1_000);
6732
+ const text = fact.prompt ?? fact.description;
6733
+ const item = {
6734
+ type: "userMessage", id: uuidv7(), clientId: null,
6735
+ content: [{ type: "text", text, text_elements: [] }],
6736
+ };
6737
+ const turn = {
6738
+ id: uuidv7(), items: [item], itemsView: "full", status: "inProgress", error: null,
6739
+ startedAt, completedAt: null, durationMs: null,
6740
+ };
6741
+ const updated = {
6742
+ ...record,
6743
+ thread: {
6744
+ ...record.thread,
6745
+ preview: text,
6746
+ status: { type: "active", activeFlags: [] },
6747
+ updatedAt: startedAt,
6748
+ recencyAt: startedAt,
6749
+ },
6750
+ };
6751
+ const status = { threadId: childThreadId, status: updated.thread.status };
6752
+ this.commitState(updated, [{
6753
+ turnId: turn.id,
6754
+ method: "thread/status/changed",
6755
+ params: status,
6756
+ providerEventId: source.providerEventId,
6757
+ providerEventType: source.providerEventType,
6758
+ }], turn, true);
6759
+ const state = newMainStreamState(childThreadId, turn.id, updated);
6760
+ state.completedItems.add(item.id);
6761
+ this.scopes.set(childThreadId, state);
6762
+ this.publishAt(childThreadId, turn.id, "turn/started", { threadId: childThreadId, turn }, source);
6763
+ this.publishAt(childThreadId, turn.id, "item/started", {
6764
+ item, threadId: childThreadId, turnId: turn.id, startedAtMs: Date.now(),
6765
+ }, source);
6766
+ this.publishAt(childThreadId, turn.id, "item/completed", {
6767
+ item, threadId: childThreadId, turnId: turn.id, completedAtMs: Date.now(),
6768
+ }, source);
6769
+ return updated.thread;
6770
+ }
6568
6771
  finishChildScope(childThreadId, status, summary, source) {
6569
6772
  const state = this.scopes.get(childThreadId);
6570
6773
  const record = this.repository.read(childThreadId, false);