@gkorepanov/ccodex 0.4.1 → 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 (45) 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 +21 -7
  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 +2 -0
  14. package/dist/claude/session/providerRuntimeFactory.js +7 -4
  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 +5 -0
  23. package/dist/claude/session/session.js +242 -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 +2 -0
  30. package/dist/config/config.js +7 -1
  31. package/dist/config/config.js.map +1 -1
  32. package/dist/gateway/clientConnection.d.ts +2 -1
  33. package/dist/gateway/clientConnection.js +45 -35
  34. package/dist/gateway/clientConnection.js.map +1 -1
  35. package/dist/gateway/server.js +8 -1
  36. package/dist/gateway/server.js.map +1 -1
  37. package/dist/gateway/stockSideThreads.d.ts +37 -0
  38. package/dist/gateway/stockSideThreads.js +246 -0
  39. package/dist/gateway/stockSideThreads.js.map +1 -0
  40. package/dist/gateway/threadList.d.ts +3 -2
  41. package/dist/gateway/threadList.js +12 -6
  42. package/dist/gateway/threadList.js.map +1 -1
  43. package/examples/config.toml +2 -0
  44. package/legal/THIRD_PARTY_NOTICES.md +1 -28
  45. 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({
@@ -539,6 +556,9 @@ export class ClaudeSession {
539
556
  resume: resumeOverride ?? record.lastClaudeMessageUuid !== null,
540
557
  cwd: record.thread.cwd,
541
558
  ephemeral: record.thread.ephemeral,
559
+ persistSession: !record.thread.ephemeral || Boolean(this.runtimeDependencies.persistUserSideSessions
560
+ && !record.thread.parentThreadId
561
+ && record.thread.threadSource === "user"),
542
562
  claudeBinary: this.runtimeDependencies.claudeBinary,
543
563
  model: record.claudeModelValue,
544
564
  settingsGeneration: settingsGeneration(record),
@@ -555,6 +575,7 @@ export class ClaudeSession {
555
575
  reasoningSummary: record.reasoningSummary,
556
576
  collaborationMode: record.collaborationMode,
557
577
  outputSchema: record.outputSchema,
578
+ interactiveQuestions: this.runtimeDependencies.interactiveQuestions ?? true,
558
579
  };
559
580
  }
560
581
  async createRuntimeGeneration(startup) {
@@ -589,7 +610,8 @@ export class ClaudeSession {
589
610
  const ready = this.waitForProviderRuntime(runtime);
590
611
  await Promise.resolve();
591
612
  if (runtime.hasExited) {
592
- 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.`));
593
615
  }
594
616
  const transportSettings = runtimeTransportSettings(startup);
595
617
  const rateLimitGeneration = dependencies.rateLimits.register({
@@ -647,6 +669,9 @@ export class ClaudeSession {
647
669
  }),
648
670
  ]);
649
671
  }
672
+ catch (error) {
673
+ throw runtime.initializationFailure(error);
674
+ }
650
675
  finally {
651
676
  if (timer)
652
677
  clearTimeout(timer);
@@ -1149,7 +1174,6 @@ export class ClaudeSession {
1149
1174
  }
1150
1175
  catch (error) {
1151
1176
  failure = error instanceof Error ? error.message : String(error);
1152
- throw error;
1153
1177
  }
1154
1178
  finally {
1155
1179
  await this.submitProviderProjection(fact.runtimeGeneration, {
@@ -1161,6 +1185,15 @@ export class ClaudeSession {
1161
1185
  ...(failure ? { error: failure } : {}),
1162
1186
  });
1163
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
+ }
1164
1197
  });
1165
1198
  }
1166
1199
  async projectProviderMessage(projection, runtime, runtimeGeneration, message) {
@@ -1262,7 +1295,9 @@ export class ClaudeSession {
1262
1295
  description: message.summary,
1263
1296
  outputFile: message.output_file,
1264
1297
  }, owner);
1265
- await this.providerSubmitLifecycle(projection, runtimeGeneration, { type: "taskNotification" });
1298
+ if (owner === this.threadId) {
1299
+ await this.providerSubmitLifecycle(projection, runtimeGeneration, { type: "taskNotification" });
1300
+ }
1266
1301
  await this.applyProviderMainStream(projection, runtimeGeneration, {
1267
1302
  kind: "taskComplete",
1268
1303
  taskId: message.task_id,
@@ -1318,9 +1353,12 @@ export class ClaudeSession {
1318
1353
  return "stateOnly";
1319
1354
  }
1320
1355
  if (message.type === "stream_event") {
1321
- const childThreadId = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1322
- if (childThreadId) {
1323
- 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";
1324
1362
  }
1325
1363
  else {
1326
1364
  await this.providerMainStreamEvent(projection, runtimeGeneration, message);
@@ -1343,12 +1381,14 @@ export class ClaudeSession {
1343
1381
  if (message.supersedes?.length) {
1344
1382
  await this.providerEvictMessages(projection, runtimeGeneration, message.supersedes);
1345
1383
  }
1346
- const childThreadId = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1347
- if (childThreadId) {
1348
- const itemIds = await this.providerProjectAssistant(projection, runtimeGeneration, message, childThreadId);
1349
- 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);
1350
1388
  return "projected";
1351
1389
  }
1390
+ if (child)
1391
+ return "retainedOnly";
1352
1392
  if (activeTurnId) {
1353
1393
  if (message.error) {
1354
1394
  await this.submitProviderProjection(runtimeGeneration, {
@@ -1384,14 +1424,16 @@ export class ClaudeSession {
1384
1424
  if ("uuid" in message && typeof message.uuid === "string") {
1385
1425
  runtime.releaseMessage(message.uuid);
1386
1426
  }
1387
- const childThreadId = await this.providerChildThreadFor(runtimeGeneration, message.parent_tool_use_id);
1388
- if (childThreadId) {
1389
- 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);
1390
1430
  if ("uuid" in message && typeof message.uuid === "string") {
1391
- await this.providerLinkItems(runtimeGeneration, message.uuid, childThreadId, itemIds);
1431
+ await this.providerLinkItems(runtimeGeneration, message.uuid, child.threadId, itemIds);
1392
1432
  }
1393
1433
  return "projected";
1394
1434
  }
1435
+ if (child)
1436
+ return "retainedOnly";
1395
1437
  if (activeTurnId && "uuid" in message && typeof message.uuid === "string") {
1396
1438
  await this.submitProviderProjection(runtimeGeneration, {
1397
1439
  type: "providerBoundary",
@@ -1714,6 +1756,12 @@ export class ClaudeSession {
1714
1756
  if (name === "AskUserQuestion") {
1715
1757
  return this.askProviderUser(projection, runtimeGeneration, input, options);
1716
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
+ }
1717
1765
  if (name === "ExitPlanMode") {
1718
1766
  return {
1719
1767
  behavior: "deny",
@@ -2021,11 +2069,14 @@ export class ClaudeSession {
2021
2069
  async providerChildThreadFor(runtimeGeneration, providerId) {
2022
2070
  if (!providerId)
2023
2071
  return undefined;
2024
- return (await this.submitProviderProjection(runtimeGeneration, {
2072
+ const inspection = await this.submitProviderProjection(runtimeGeneration, {
2025
2073
  type: "inspectRuntime",
2026
2074
  runtimeGeneration,
2027
2075
  providerId,
2028
- })).childThreadId ?? undefined;
2076
+ });
2077
+ return inspection.childThreadId
2078
+ ? { threadId: inspection.childThreadId, active: inspection.interruptible }
2079
+ : undefined;
2029
2080
  }
2030
2081
  async providerLinkItems(runtimeGeneration, providerMessageId, ownerThreadId, itemIds) {
2031
2082
  await this.submitProviderProjection(runtimeGeneration, {
@@ -4103,8 +4154,22 @@ export class ClaudeSession {
4103
4154
  if (command.runtimeGeneration !== this.runtimeGeneration
4104
4155
  || this.interruptFence || this.dropLateFacts)
4105
4156
  return undefined;
4106
- if (command.fact.kind === "inspect" && !this.scopes.has(command.ownerThreadId ?? this.threadId))
4107
- 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
+ }
4108
4173
  {
4109
4174
  if (command.fact.kind === "taskComplete" && !command.fact.outputDrained) {
4110
4175
  const task = this.tasks.get(command.fact.taskId);
@@ -4328,27 +4393,14 @@ export class ClaudeSession {
4328
4393
  if (this.dropLateFacts || this.adminOperation && this.adminOperation.kind !== "rename"
4329
4394
  || this.repository.archived(command.threadId))
4330
4395
  return false;
4331
- const record = this.repository.read(command.threadId, false);
4332
- 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")
4333
4399
  return false;
4334
- const turnId = command.requestedTurnId
4335
- ?? (command.threadId === this.threadId ? this.shell?.turnId : undefined)
4336
- ?? this.scopes.get(command.threadId)?.turnId
4337
- ?? record.lastCompletedTurnId
4338
- ?? uuidv7();
4339
- const params = {
4340
- error: {
4341
- message: command.message,
4342
- codexErrorInfo: command.codexErrorInfo,
4343
- additionalDetails: null,
4344
- },
4345
- willRetry: false,
4346
- threadId: command.threadId,
4347
- turnId,
4348
- };
4349
- if (this.repository.appendEvent(command.threadId, turnId, "error", params)) {
4350
- this.output.emit(command.threadId, "error", params);
4351
- }
4400
+ this.projectMainStream(command.threadId, {
4401
+ kind: "instantAgent",
4402
+ text: systemNoticeText(command.message, "error"),
4403
+ }, nullSource);
4352
4404
  return true;
4353
4405
  }
4354
4406
  case "startCompact": {
@@ -5185,8 +5237,13 @@ export class ClaudeSession {
5185
5237
  this.pendingNoQuery = this.pendingInputs = 0;
5186
5238
  this.preparedRuntimeInputs.clear();
5187
5239
  this.disposeRuntimeOperations();
5188
- if (active)
5240
+ if (active) {
5189
5241
  active.commandCompleted = true;
5242
+ active.acknowledged = active.notifications;
5243
+ active.goals = 0;
5244
+ active.goalInFlight = false;
5245
+ delete active.request;
5246
+ }
5190
5247
  this.settleForcedScopes(source);
5191
5248
  if (fact.type === "runtimeExit" && generation !== undefined) {
5192
5249
  this.runtimeGeneration = undefined;
@@ -5196,8 +5253,10 @@ export class ClaudeSession {
5196
5253
  else if (fact.type === "timer" && active?.result && this.hasNotifications()
5197
5254
  && fact.generation === active.notifications && fact.generation > active.diagnosed) {
5198
5255
  active.diagnosed = fact.generation;
5199
- this.projectMainStream(this.threadId, { kind: "instantAgent", text: systemNoticeText("Claude did not emit a request, result, or idle lifecycle signal after a background-task notification. "
5200
- + "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
+ });
5201
5260
  }
5202
5261
  this.maybeFinish(source, fact.type === "sync");
5203
5262
  }
@@ -5756,7 +5815,7 @@ export class ClaudeSession {
5756
5815
  let owner = this.toolOwner(command.providerId);
5757
5816
  if (!owner && command.ownerProviderId) {
5758
5817
  owner = this.toolOwner(command.ownerProviderId);
5759
- 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);
5760
5819
  if (task?.childThreadId)
5761
5820
  owner = this.scopes.get(task.childThreadId);
5762
5821
  }
@@ -5889,11 +5948,12 @@ export class ClaudeSession {
5889
5948
  }
5890
5949
  let ownerThreadId = command.ownerThreadId ?? this.threadId;
5891
5950
  let targetThreadId = ownerThreadId;
5951
+ let providerTask;
5892
5952
  if (command.providerId) {
5893
5953
  const scope = [...this.scopes.values()].find((candidate) => candidate.tools.has(command.providerId));
5894
- const task = [...this.tasks.values()].find((candidate) => candidate.providerId === command.providerId || candidate.taskId === command.providerId);
5895
- ownerThreadId = task?.ownerThreadId ?? scope?.ownerThreadId ?? ownerThreadId;
5896
- 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;
5897
5957
  }
5898
5958
  const childTask = command.childThreadId
5899
5959
  ? [...this.tasks.values()].find((candidate) => candidate.childThreadId === command.childThreadId)
@@ -5920,12 +5980,14 @@ export class ClaudeSession {
5920
5980
  modelContextWindow: this.requireRecord(false).modelContextWindow,
5921
5981
  interruptible: command.expectedTurnId
5922
5982
  ? targetState?.turnId === command.expectedTurnId
5923
- : childTask ? !childTask.terminal && Boolean(targetState) : Boolean(targetState),
5983
+ : childTask ? !childTask.terminal && Boolean(targetState)
5984
+ : providerTask?.childThreadId ? !providerTask.terminal && Boolean(targetState)
5985
+ : Boolean(targetState),
5924
5986
  ownerThreadId,
5925
5987
  ownerTurnId: ownerState?.turnId ?? null,
5926
5988
  taskIds,
5927
5989
  childThreadId: command.providerId
5928
- ? [...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
5929
5991
  : null,
5930
5992
  taskId: childTask?.taskId ?? null,
5931
5993
  quiescent: this.isQuiescent(),
@@ -6343,8 +6405,9 @@ export class ClaudeSession {
6343
6405
  const existing = tool && turn.items.find((item) => item.id === tool.itemId);
6344
6406
  const subagent = Boolean(fact.subagentType) || fact.taskType === "agent" || fact.taskType === "subagent";
6345
6407
  if (!task && !existing && !subagent) {
6408
+ const providerId = fact.providerId ?? fact.taskId;
6346
6409
  task = { taskId: fact.taskId, ownerThreadId: state.ownerThreadId,
6347
- itemId: `claude-task:${fact.taskId}`, providerId: fact.providerId ?? fact.taskId,
6410
+ itemId: `claude-task:${fact.taskId}`, providerId, providerIds: new Set([providerId]),
6348
6411
  turnId: turn.id, childThreadId: undefined, outputFile: fact.outputFile, terminal: false };
6349
6412
  this.tasks.set(task.taskId, task);
6350
6413
  }
@@ -6354,8 +6417,9 @@ export class ClaudeSession {
6354
6417
  status: "inProgress", senderThreadId: state.ownerThreadId, receiverThreadIds: [],
6355
6418
  prompt: fact.prompt ?? fact.description, model: null, reasoningEffort: null, agentsStates: {},
6356
6419
  };
6420
+ const providerId = fact.providerId ?? item.id;
6357
6421
  const createdTask = { taskId: fact.taskId, ownerThreadId: state.ownerThreadId, itemId: item.id,
6358
- providerId: fact.providerId ?? item.id, turnId: turn.id, childThreadId: undefined,
6422
+ providerId, providerIds: new Set([providerId]), turnId: turn.id, childThreadId: undefined,
6359
6423
  outputFile: fact.outputFile, terminal: false };
6360
6424
  task = createdTask;
6361
6425
  this.tasks.set(createdTask.taskId, createdTask);
@@ -6372,6 +6436,25 @@ export class ClaudeSession {
6372
6436
  item.agentsStates = { [childThread.id]: { status: "running", message: fact.description } };
6373
6437
  }
6374
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
+ }
6375
6458
  if (task && tool) {
6376
6459
  tool.backgroundTaskId = task.taskId;
6377
6460
  if (task.outputFile)
@@ -6399,8 +6482,24 @@ export class ClaudeSession {
6399
6482
  handled = taskIds.length > 0;
6400
6483
  }
6401
6484
  else if (task) {
6402
- const ownerState = this.requireMainStreamState(task.ownerThreadId);
6485
+ const ownerState = this.scopes.get(task.ownerThreadId);
6403
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
+ }
6404
6503
  const item = ownerTurn.items.find((candidate) => candidate.id === task.itemId);
6405
6504
  if (fact.kind === "taskProgress") {
6406
6505
  if (item?.type === "collabAgentToolCall")
@@ -6441,10 +6540,12 @@ export class ClaudeSession {
6441
6540
  status: fact.status === "completed" ? "completed" : fact.status === "stopped" ? "interrupted" : "errored",
6442
6541
  message: fact.summary,
6443
6542
  };
6444
- ownerState.completedItems.add(item.id);
6445
- this.publishTurn(ownerTurn, "item/completed", {
6446
- item, threadId: task.ownerThreadId, turnId: ownerTurn.id, completedAtMs: Date.now(),
6447
- }, 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
+ }
6448
6549
  }
6449
6550
  if (task.childThreadId)
6450
6551
  this.finishChildScope(task.childThreadId, fact.status, fact.summary, source);
@@ -6485,11 +6586,49 @@ export class ClaudeSession {
6485
6586
  tool: task ? this.scopes.get(task.ownerThreadId)?.tools.get(task.providerId) : undefined,
6486
6587
  });
6487
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
+ }
6488
6616
  updateTool(turn, state, tool, input, source) {
6489
6617
  const item = turn.items.find((candidate) => candidate.id === tool.itemId);
6490
6618
  if (!item)
6491
6619
  return;
6492
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
+ }
6493
6632
  if (tool.name === "TaskOutput") {
6494
6633
  const id = typeof input.task_id === "string" ? input.task_id
6495
6634
  : typeof input.taskId === "string" ? input.taskId : undefined;
@@ -6508,6 +6647,9 @@ export class ClaudeSession {
6508
6647
  if (!item || tool.started || tool.foldedTaskId || isImageRead(tool, record.thread.cwd)
6509
6648
  || item.type === "commandExecution" && !item.command)
6510
6649
  return;
6650
+ if (item.type === "collabAgentToolCall" && item.tool === "sendInput"
6651
+ && (!item.prompt || item.receiverThreadIds.length === 0))
6652
+ return;
6511
6653
  if (tool.name === "Bash" && tool.input.run_in_background === true && !tool.backgroundTaskId)
6512
6654
  return;
6513
6655
  if (!force
@@ -6562,6 +6704,50 @@ export class ClaudeSession {
6562
6704
  this.publishAt(childThreadId, turn.id, "item/completed", { item, threadId: childThreadId, turnId: turn.id, completedAtMs: Date.now() }, source);
6563
6705
  return thread;
6564
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
+ }
6565
6751
  finishChildScope(childThreadId, status, summary, source) {
6566
6752
  const state = this.scopes.get(childThreadId);
6567
6753
  const record = this.repository.read(childThreadId, false);