@p4code/cli 0.6.53 → 0.6.55

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/dist/bin.mjs CHANGED
@@ -263,7 +263,7 @@ const make$123 = () => {
263
263
  const layer$124 = Layer.sync(NetService, make$123);
264
264
  //#endregion
265
265
  //#region package.json
266
- var version$1 = "0.6.53";
266
+ var version$1 = "0.6.55";
267
267
  //#endregion
268
268
  //#region ../../packages/shared/src/serverInstance.ts
269
269
  /**
@@ -3015,6 +3015,10 @@ const ExecutionEnvironmentCapabilities = Schema$1.Struct({
3015
3015
  /** Server understands thread.scheduled-task.create / cancel and fires
3016
3016
  pending tasks. Same version-skew contract as threadSettlement. */
3017
3017
  threadScheduledTasks: Schema$1.optionalKey(Schema$1.Boolean),
3018
+ /** Server holds thread.turn.start with queueIfBusy until the running turn
3019
+ ends and understands thread.queued-message.send / remove. Same
3020
+ version-skew contract as threadSettlement. */
3021
+ threadMessageQueue: Schema$1.optionalKey(Schema$1.Boolean),
3018
3022
  /** Server exposes the pull-request list, detail, activity, diff, and mutation APIs. Absent on
3019
3023
  servers from before the pull-request workspace shipped, so clients must not probe them. */
3020
3024
  pullRequests: Schema$1.optionalKey(Schema$1.Boolean),
@@ -3341,6 +3345,18 @@ const OrchestrationScheduledTask = Schema$1.Struct({
3341
3345
  cancelledAt: Schema$1.NullOr(IsoDateTime).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
3342
3346
  failure: Schema$1.NullOr(Schema$1.String).pipe(Schema$1.withDecodingDefault(Effect.succeed(null)))
3343
3347
  });
3348
+ /**
3349
+ * A user message sent while the thread was busy, held until the running turn
3350
+ * ends. The server sends queued messages one per turn in order; steering one
3351
+ * sends it into the running turn right away, and removing it drops it.
3352
+ */
3353
+ const OrchestrationQueuedMessage = Schema$1.Struct({
3354
+ messageId: MessageId,
3355
+ text: Schema$1.String,
3356
+ attachments: Schema$1.Array(ChatAttachment),
3357
+ modelSelection: Schema$1.optional(ModelSelection),
3358
+ createdAt: IsoDateTime
3359
+ });
3344
3360
  const SourceProposedPlanReference = Schema$1.Struct({
3345
3361
  threadId: ThreadId,
3346
3362
  planId: OrchestrationProposedPlanId
@@ -3599,10 +3615,12 @@ const OrchestrationThread = Schema$1.Struct({
3599
3615
  snoozedAt: Schema$1.optional(Schema$1.NullOr(IsoDateTime)),
3600
3616
  pinnedAt: Schema$1.optional(Schema$1.NullOr(IsoDateTime)),
3601
3617
  pinOrderKey: Schema$1.optional(Schema$1.NullOr(Schema$1.String)),
3618
+ parentThreadId: Schema$1.optional(Schema$1.NullOr(ThreadId)),
3602
3619
  deletedAt: Schema$1.NullOr(IsoDateTime),
3603
3620
  messages: Schema$1.Array(OrchestrationMessage),
3604
3621
  proposedPlans: Schema$1.Array(OrchestrationProposedPlan).pipe(Schema$1.withDecodingDefault(Effect.succeed([]))),
3605
3622
  scheduledTasks: Schema$1.optional(Schema$1.Array(OrchestrationScheduledTask)),
3623
+ queuedMessages: Schema$1.optional(Schema$1.Array(OrchestrationQueuedMessage)),
3606
3624
  activities: Schema$1.Array(OrchestrationThreadActivity),
3607
3625
  checkpoints: Schema$1.Array(OrchestrationCheckpointSummary),
3608
3626
  session: Schema$1.NullOr(OrchestrationSession)
@@ -3766,6 +3784,7 @@ const OrchestrationThreadShell = Schema$1.Struct({
3766
3784
  snoozedAt: Schema$1.optional(Schema$1.NullOr(IsoDateTime)),
3767
3785
  pinnedAt: Schema$1.optional(Schema$1.NullOr(IsoDateTime)),
3768
3786
  pinOrderKey: Schema$1.optional(Schema$1.NullOr(Schema$1.String)),
3787
+ parentThreadId: Schema$1.optional(Schema$1.NullOr(ThreadId)),
3769
3788
  session: Schema$1.NullOr(OrchestrationSession),
3770
3789
  latestUserMessageAt: Schema$1.NullOr(IsoDateTime),
3771
3790
  hasPendingApprovals: Schema$1.Boolean,
@@ -3937,6 +3956,8 @@ const ThreadCreateCommand = Schema$1.Struct({
3937
3956
  unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
3938
3957
  branch: Schema$1.NullOr(TrimmedNonEmptyString),
3939
3958
  worktreePath: Schema$1.NullOr(TrimmedNonEmptyString),
3959
+ /** Links a sub-thread to the thread that spawned it. */
3960
+ parentThreadId: Schema$1.optional(ThreadId),
3940
3961
  createdAt: IsoDateTime
3941
3962
  });
3942
3963
  /**
@@ -4145,6 +4166,8 @@ const ThreadTurnStartCommand = Schema$1.Struct({
4145
4166
  activityId: EventId,
4146
4167
  outcome: Schema$1.String
4147
4168
  })),
4169
+ /** Hold the message until the running turn ends instead of steering into it. */
4170
+ queueIfBusy: Schema$1.optional(Schema$1.Boolean),
4148
4171
  createdAt: IsoDateTime
4149
4172
  });
4150
4173
  const ClientThreadTurnStartCommand = Schema$1.Struct({
@@ -4165,6 +4188,22 @@ const ClientThreadTurnStartCommand = Schema$1.Struct({
4165
4188
  unpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
4166
4189
  bootstrap: Schema$1.optional(ThreadTurnStartBootstrap),
4167
4190
  sourceProposedPlan: Schema$1.optional(SourceProposedPlanReference),
4191
+ queueIfBusy: Schema$1.optional(Schema$1.Boolean),
4192
+ createdAt: IsoDateTime
4193
+ });
4194
+ /** Sends a queued message now: it steers into the running turn, or starts one when idle. */
4195
+ const ThreadQueuedMessageSendCommand = Schema$1.Struct({
4196
+ type: Schema$1.Literal("thread.queued-message.send"),
4197
+ commandId: CommandId,
4198
+ threadId: ThreadId,
4199
+ messageId: MessageId,
4200
+ createdAt: IsoDateTime
4201
+ });
4202
+ const ThreadQueuedMessageRemoveCommand = Schema$1.Struct({
4203
+ type: Schema$1.Literal("thread.queued-message.remove"),
4204
+ commandId: CommandId,
4205
+ threadId: ThreadId,
4206
+ messageId: MessageId,
4168
4207
  createdAt: IsoDateTime
4169
4208
  });
4170
4209
  const ThreadTurnInterruptCommand = Schema$1.Struct({
@@ -4273,6 +4312,8 @@ const DispatchableClientOrchestrationCommand = Schema$1.Union([
4273
4312
  ThreadPullRequestUnlinkCommand,
4274
4313
  ThreadComputerUseGrantSetCommand,
4275
4314
  ThreadTurnStartCommand,
4315
+ ThreadQueuedMessageSendCommand,
4316
+ ThreadQueuedMessageRemoveCommand,
4276
4317
  ThreadTurnInterruptCommand,
4277
4318
  ThreadApprovalRespondCommand,
4278
4319
  ThreadUserInputRespondCommand,
@@ -4308,6 +4349,8 @@ const ClientOrchestrationCommand = Schema$1.Union([
4308
4349
  ThreadPullRequestUnlinkCommand,
4309
4350
  ThreadComputerUseGrantSetCommand,
4310
4351
  ClientThreadTurnStartCommand,
4352
+ ThreadQueuedMessageSendCommand,
4353
+ ThreadQueuedMessageRemoveCommand,
4311
4354
  ThreadTurnInterruptCommand,
4312
4355
  ThreadApprovalRespondCommand,
4313
4356
  ThreadUserInputRespondCommand,
@@ -4498,6 +4541,8 @@ const OrchestrationEventType = Schema$1.Literals([
4498
4541
  "thread.pull-requests-updated",
4499
4542
  "thread.computer-use-grant-set",
4500
4543
  "thread.message-sent",
4544
+ "thread.message-queued",
4545
+ "thread.queued-message-removed",
4501
4546
  "thread.turn-start-requested",
4502
4547
  "thread.turn-interrupt-requested",
4503
4548
  "thread.approval-response-requested",
@@ -4573,6 +4618,8 @@ const ThreadCreatedPayload$1 = Schema$1.Struct({
4573
4618
  * messages into this one. Optional so events from pre-fork servers decode.
4574
4619
  */
4575
4620
  forkedFromThreadId: Schema$1.optional(ThreadId),
4621
+ /** The spawning thread for a sub-thread. Optional so older events decode. */
4622
+ parentThreadId: Schema$1.optional(ThreadId),
4576
4623
  createdAt: IsoDateTime,
4577
4624
  updatedAt: IsoDateTime
4578
4625
  });
@@ -4685,6 +4732,17 @@ const ThreadMessageSentPayload = Schema$1.Struct({
4685
4732
  createdAt: IsoDateTime,
4686
4733
  updatedAt: IsoDateTime
4687
4734
  });
4735
+ const ThreadMessageQueuedPayload$1 = Schema$1.Struct({
4736
+ threadId: ThreadId,
4737
+ message: OrchestrationQueuedMessage
4738
+ });
4739
+ const ThreadQueuedMessageRemovedPayload$1 = Schema$1.Struct({
4740
+ threadId: ThreadId,
4741
+ messageId: MessageId,
4742
+ /** "sent" when it left the queue as a turn or steer, "removed" when dropped. */
4743
+ reason: Schema$1.Literals(["sent", "removed"]),
4744
+ removedAt: IsoDateTime
4745
+ });
4688
4746
  const ThreadTurnStartRequestedPayload = Schema$1.Struct({
4689
4747
  threadId: ThreadId,
4690
4748
  messageId: MessageId,
@@ -4985,6 +5043,16 @@ const OrchestrationEvent = Schema$1.Union([
4985
5043
  type: Schema$1.Literal("thread.message-sent"),
4986
5044
  payload: ThreadMessageSentPayload
4987
5045
  }),
5046
+ Schema$1.Struct({
5047
+ ...EventBaseFields,
5048
+ type: Schema$1.Literal("thread.message-queued"),
5049
+ payload: ThreadMessageQueuedPayload$1
5050
+ }),
5051
+ Schema$1.Struct({
5052
+ ...EventBaseFields,
5053
+ type: Schema$1.Literal("thread.queued-message-removed"),
5054
+ payload: ThreadQueuedMessageRemovedPayload$1
5055
+ }),
4988
5056
  Schema$1.Struct({
4989
5057
  ...EventBaseFields,
4990
5058
  type: Schema$1.Literal("thread.turn-start-requested"),
@@ -11212,6 +11280,7 @@ const ClientSettingsSchema = Schema$1.Struct({
11212
11280
  composerControlsExpanded: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
11213
11281
  confirmThreadArchive: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(false))),
11214
11282
  confirmThreadDelete: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
11283
+ queueFollowUpMessages: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
11215
11284
  dismissedProviderUpdateNotificationKeys: Schema$1.Array(TrimmedNonEmptyString).pipe(Schema$1.withDecodingDefault(Effect.succeed([]))),
11216
11285
  diffIgnoreWhitespace: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
11217
11286
  computerUsePermissionHosts: ComputerUsePermissionHosts.pipe(Schema$1.withDecodingDefault(Effect.succeed({}))),
@@ -12005,6 +12074,7 @@ Schema$1.Struct({
12005
12074
  composerControlsExpanded: Schema$1.optionalKey(Schema$1.Boolean),
12006
12075
  confirmThreadArchive: Schema$1.optionalKey(Schema$1.Boolean),
12007
12076
  confirmThreadDelete: Schema$1.optionalKey(Schema$1.Boolean),
12077
+ queueFollowUpMessages: Schema$1.optionalKey(Schema$1.Boolean),
12008
12078
  diffIgnoreWhitespace: Schema$1.optionalKey(Schema$1.Boolean),
12009
12079
  computerUsePermissionHosts: Schema$1.optionalKey(ComputerUsePermissionHosts),
12010
12080
  environmentIdentificationMode: Schema$1.optionalKey(EnvironmentIdentificationMode),
@@ -13573,7 +13643,8 @@ const ThreadSpawnInput = Schema$1.Struct({
13573
13643
  compressMode: Schema$1.optional(CompressMode),
13574
13644
  unpromptedSubagents: Schema$1.optional(Schema$1.Boolean),
13575
13645
  fusion: Schema$1.optional(Schema$1.Boolean.annotate({ description: "Create two new threads as a separate Fusion pair, then start the supervisor with prompt. The builder stays empty until advised. Leaves the caller and its existing pair unchanged. Cannot be combined with fusionWatcher." })),
13576
- fusionWatcher: Schema$1.optional(Schema$1.Boolean.annotate({ description: "Set true only when creating the watcher to pair with the current thread. Defaults to the saved Fusion supervisor model. No special user command syntax is required." }))
13646
+ fusionWatcher: Schema$1.optional(Schema$1.Boolean.annotate({ description: "Set true only when creating the watcher to pair with the current thread. Defaults to the saved Fusion supervisor model. No special user command syntax is required." })),
13647
+ workspace: Schema$1.optional(Schema$1.Literals(["worktree", "shared"]).annotate({ description: "Where the sub-thread works. 'worktree' (the default for plain spawns in a git project) gives it its own git worktree and branch started from this thread's current HEAD commit, so parallel sub-threads never edit the same files; land it later with thread_merge_child. Commit first: uncommitted edits here are not visible to it. 'shared' runs it in this thread's checkout. Fusion spawns always share." }))
13577
13648
  });
13578
13649
  const ThreadSpawnResult = Schema$1.Struct({
13579
13650
  /** Watchable with `thread_watch_events`, and addressable by every tool here. */
@@ -13581,7 +13652,71 @@ const ThreadSpawnResult = Schema$1.Struct({
13581
13652
  projectId: ProjectId,
13582
13653
  title: TrimmedNonEmptyString,
13583
13654
  pairId: Schema$1.optional(ThreadPairId),
13584
- watcherThreadId: Schema$1.optional(ThreadId)
13655
+ watcherThreadId: Schema$1.optional(ThreadId),
13656
+ /** Set when the sub-thread got its own worktree. */
13657
+ branch: Schema$1.optional(TrimmedNonEmptyString),
13658
+ worktreePath: Schema$1.optional(TrimmedNonEmptyString),
13659
+ baseCommitSha: Schema$1.optional(TrimmedNonEmptyString),
13660
+ /** This thread had uncommitted changes the sub-thread cannot see. */
13661
+ parentDirty: Schema$1.optional(Schema$1.Boolean)
13662
+ });
13663
+ const SubThreadState = Schema$1.Literals([
13664
+ "running",
13665
+ "needs-input",
13666
+ "completed",
13667
+ "failed",
13668
+ "missing"
13669
+ ]);
13670
+ const ThreadWaitInput = Schema$1.Struct({
13671
+ threadIds: Schema$1.Array(ThreadId).check(Schema$1.isMinLength(1), Schema$1.isMaxLength(16)).annotate({ description: "Sub-threads to wait on, from thread_spawn. Up to 16." }),
13672
+ mode: Schema$1.optional(Schema$1.Literals(["all", "any"]).annotate({ description: "'all' (default) returns once every sub-thread has stopped running; 'any' returns as soon as one stops, so you can merge or answer it while the rest keep working." })),
13673
+ timeoutSeconds: Schema$1.optional(Schema$1.Int.check(Schema$1.isBetween({
13674
+ minimum: 1,
13675
+ maximum: 300
13676
+ })).annotate({ description: "Longest wait before returning the current states with timedOut true. Default 45. Call again to keep waiting." }))
13677
+ });
13678
+ const ThreadWaitChild = Schema$1.Struct({
13679
+ threadId: ThreadId,
13680
+ title: Schema$1.NullOr(TrimmedNonEmptyString),
13681
+ /** running, needs-input (a question or approval waits), completed, failed, or missing. */
13682
+ state: SubThreadState,
13683
+ branch: Schema$1.NullOr(TrimmedNonEmptyString),
13684
+ worktreePath: Schema$1.NullOr(TrimmedNonEmptyString),
13685
+ /** The latest reply, trimmed to 1500 characters. */
13686
+ lastAssistantMessage: Schema$1.NullOr(Schema$1.String),
13687
+ lastError: Schema$1.NullOr(Schema$1.String)
13688
+ });
13689
+ const ThreadWaitResult = Schema$1.Struct({
13690
+ timedOut: Schema$1.Boolean,
13691
+ children: Schema$1.Array(ThreadWaitChild)
13692
+ });
13693
+ const ThreadMessageInput = Schema$1.Struct({
13694
+ threadId: ThreadId.annotate({ description: "A sub-thread this thread spawned." }),
13695
+ text: TrimmedNonEmptyString.annotate({ description: "Message to send. If the sub-thread is still working it is queued and sent when its turn ends." })
13696
+ });
13697
+ const ThreadMessageResult = Schema$1.Struct({
13698
+ threadId: ThreadId,
13699
+ messageId: MessageId
13700
+ });
13701
+ const ThreadMergeChildInput = Schema$1.Struct({ threadId: ThreadId.annotate({ description: "A finished sub-thread whose branch should land in this thread's worktree." }) });
13702
+ const ThreadMergeChildResult = Schema$1.Struct({
13703
+ threadId: ThreadId,
13704
+ /**
13705
+ * merged: a merge commit landed. up-to-date: nothing new. conflicted: the
13706
+ * merge was aborted, this worktree is unchanged, and conflictedFiles lists
13707
+ * what the sub-thread must resolve. refused: nothing ran; see detail.
13708
+ */
13709
+ outcome: Schema$1.Literals([
13710
+ "merged",
13711
+ "up-to-date",
13712
+ "conflicted",
13713
+ "refused"
13714
+ ]),
13715
+ childBranch: Schema$1.optional(TrimmedNonEmptyString),
13716
+ parentBranch: Schema$1.optional(Schema$1.NullOr(TrimmedNonEmptyString)),
13717
+ mergeCommitSha: Schema$1.optional(TrimmedNonEmptyString),
13718
+ conflictedFiles: Schema$1.optional(Schema$1.Array(Schema$1.String)),
13719
+ detail: Schema$1.optional(Schema$1.String)
13585
13720
  });
13586
13721
  const ThreadPairCreateInput = Schema$1.Struct({
13587
13722
  watcherThreadId: Schema$1.optional(ThreadId.annotate({ description: "Legacy pairing: use an existing spawned supervisor and keep this chat as builder. Omit to keep this chat and its history as supervisor and create a new empty builder, idle until advised." })),
@@ -18632,6 +18767,46 @@ var _062_ProviderResetCreditAttempts_default = Effect.gen(function* () {
18632
18767
  WHERE outcome IS NULL OR outcome = 'failed'`;
18633
18768
  });
18634
18769
  //#endregion
18770
+ //#region src/persistence/Migrations/064_ProjectionThreadQueuedMessages.ts
18771
+ /**
18772
+ * User messages sent while a thread was busy, held until its turn ends. A row
18773
+ * exists only while the message waits; sending or removing it deletes the row.
18774
+ */
18775
+ var _064_ProjectionThreadQueuedMessages_default = Effect.gen(function* () {
18776
+ const sql = yield* SqlClient.SqlClient;
18777
+ yield* sql`
18778
+ CREATE TABLE IF NOT EXISTS projection_thread_queued_messages (
18779
+ message_id TEXT PRIMARY KEY,
18780
+ thread_id TEXT NOT NULL,
18781
+ text TEXT NOT NULL,
18782
+ attachments_json TEXT NOT NULL,
18783
+ model_selection_json TEXT,
18784
+ created_at TEXT NOT NULL
18785
+ )
18786
+ `;
18787
+ yield* sql`
18788
+ CREATE INDEX IF NOT EXISTS idx_projection_thread_queued_messages_thread_created
18789
+ ON projection_thread_queued_messages(thread_id, created_at)
18790
+ `;
18791
+ });
18792
+ //#endregion
18793
+ //#region src/persistence/Migrations/065_ProjectionThreadsParentThread.ts
18794
+ /**
18795
+ * Links a sub-thread to the thread that spawned it, so a parent keeps control
18796
+ * of its children across restarts and clients can group them under it.
18797
+ */
18798
+ var _065_ProjectionThreadsParentThread_default = Effect.gen(function* () {
18799
+ const sql = yield* SqlClient.SqlClient;
18800
+ yield* sql`
18801
+ ALTER TABLE projection_threads
18802
+ ADD COLUMN parent_thread_id TEXT
18803
+ `;
18804
+ yield* sql`
18805
+ CREATE INDEX IF NOT EXISTS idx_projection_threads_parent_thread
18806
+ ON projection_threads(parent_thread_id)
18807
+ `;
18808
+ });
18809
+ //#endregion
18635
18810
  //#region src/persistence/Migrations/001_OrchestrationEvents.ts
18636
18811
  var _001_OrchestrationEvents_default = Effect.gen(function* () {
18637
18812
  const sql = yield* SqlClient.SqlClient;
@@ -20828,6 +21003,16 @@ const migrationEntries = [
20828
21003
  63,
20829
21004
  "RemoveThreadSettlement",
20830
21005
  _063_RemoveThreadSettlement_default
21006
+ ],
21007
+ [
21008
+ 64,
21009
+ "ProjectionThreadQueuedMessages",
21010
+ _064_ProjectionThreadQueuedMessages_default
21011
+ ],
21012
+ [
21013
+ 65,
21014
+ "ProjectionThreadsParentThread",
21015
+ _065_ProjectionThreadsParentThread_default
20831
21016
  ]
20832
21017
  ];
20833
21018
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -37223,6 +37408,8 @@ const ThreadUnpinnedPayload = ThreadUnpinnedPayload$1;
37223
37408
  const ThreadSnoozedPayload = ThreadSnoozedPayload$1;
37224
37409
  const ThreadUnsnoozedPayload = ThreadUnsnoozedPayload$1;
37225
37410
  const MessageSentPayloadSchema = ThreadMessageSentPayload;
37411
+ const ThreadMessageQueuedPayload = ThreadMessageQueuedPayload$1;
37412
+ const ThreadQueuedMessageRemovedPayload = ThreadQueuedMessageRemovedPayload$1;
37226
37413
  const ThreadProposedPlanUpsertedPayload = ThreadProposedPlanUpsertedPayload$1;
37227
37414
  const ThreadScheduledTaskCreatedPayload = ThreadScheduledTaskCreatedPayload$1;
37228
37415
  const ThreadScheduledTaskCancelledPayload = ThreadScheduledTaskCancelledPayload$1;
@@ -37440,6 +37627,7 @@ function projectEvent(model, event) {
37440
37627
  computerUseHostId: payload.computerUseHostId ?? null,
37441
37628
  branch: payload.branch,
37442
37629
  worktreePath: payload.worktreePath,
37630
+ parentThreadId: payload.parentThreadId ?? null,
37443
37631
  latestTurn: null,
37444
37632
  createdAt: payload.createdAt,
37445
37633
  updatedAt: payload.updatedAt,
@@ -37641,6 +37829,24 @@ function projectEvent(model, event) {
37641
37829
  })
37642
37830
  };
37643
37831
  });
37832
+ case "thread.message-queued": return Effect.gen(function* () {
37833
+ const payload = yield* decodeForEvent(ThreadMessageQueuedPayload, event.payload, event.type, "payload");
37834
+ const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
37835
+ if (!thread) return nextBase;
37836
+ return {
37837
+ ...nextBase,
37838
+ threads: updateThread(nextBase.threads, payload.threadId, { queuedMessages: [...(thread.queuedMessages ?? []).filter((entry) => entry.messageId !== payload.message.messageId), payload.message] })
37839
+ };
37840
+ });
37841
+ case "thread.queued-message-removed": return Effect.gen(function* () {
37842
+ const payload = yield* decodeForEvent(ThreadQueuedMessageRemovedPayload, event.payload, event.type, "payload");
37843
+ const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
37844
+ if (!thread) return nextBase;
37845
+ return {
37846
+ ...nextBase,
37847
+ threads: updateThread(nextBase.threads, payload.threadId, { queuedMessages: (thread.queuedMessages ?? []).filter((entry) => entry.messageId !== payload.messageId) })
37848
+ };
37849
+ });
37644
37850
  case "thread.scheduled-task.created": return Effect.gen(function* () {
37645
37851
  const payload = yield* decodeForEvent(ThreadScheduledTaskCreatedPayload, event.payload, event.type, "payload");
37646
37852
  const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
@@ -37788,7 +37994,7 @@ function archiveWorkspaceLifecycle(cleanup, updatedAt) {
37788
37994
  function findActiveThreadPair(readModel, threadId) {
37789
37995
  return (readModel.threadPairs ?? []).find((pair) => pair.detachedAt === null && (pair.implementerThreadId === threadId || pair.watcherThreadId === threadId));
37790
37996
  }
37791
- const QUEUED_TURN_START_GRACE_MS = 120 * 1e3;
37997
+ const QUEUED_TURN_START_GRACE_MS$1 = 120 * 1e3;
37792
37998
  /**
37793
37999
  * Blocked-on-you work derived from the thread's retained activities: an
37794
38000
  * approval or user-input request with no later resolution for the same
@@ -37842,7 +38048,16 @@ function threadHasQueuedTurnStart(thread, occurredAt) {
37842
38048
  thread.latestTurn.completedAt
37843
38049
  ].map((candidate) => candidate == null ? Number.NEGATIVE_INFINITY : Date.parse(candidate)));
37844
38050
  const queuedAgeMs = Date.parse(occurredAt) - latestUserMessageAtMs;
37845
- return thread.session?.status !== "error" && Number.isFinite(latestUserMessageAtMs) && latestUserMessageAtMs > latestTurnAtMs && Math.abs(queuedAgeMs) <= QUEUED_TURN_START_GRACE_MS;
38051
+ return thread.session?.status !== "error" && Number.isFinite(latestUserMessageAtMs) && latestUserMessageAtMs > latestTurnAtMs && Math.abs(queuedAgeMs) <= QUEUED_TURN_START_GRACE_MS$1;
38052
+ }
38053
+ /**
38054
+ * A thread is busy while a turn runs or one is about to: the session is
38055
+ * starting or running, or a sent message has not been adopted by a turn yet.
38056
+ * Queued messages wait for this to clear.
38057
+ */
38058
+ function isThreadBusy(thread, occurredAt) {
38059
+ const status = thread.session?.status;
38060
+ return status === "running" || status === "starting" || threadHasQueuedTurnStart(thread, occurredAt);
37846
38061
  }
37847
38062
  function withEventBase(input) {
37848
38063
  return Crypto.Crypto.pipe(Effect.flatMap((crypto) => crypto.randomUUIDv4.pipe(Effect.map((eventId) => ({
@@ -38017,6 +38232,15 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
38017
38232
  commandType: command.type,
38018
38233
  detail: `Sidechat parent '${sidechatParent.id}' belongs to a different project.`
38019
38234
  });
38235
+ const spawningParent = command.parentThreadId === void 0 ? null : yield* requireThreadNotArchived({
38236
+ readModel,
38237
+ command,
38238
+ threadId: command.parentThreadId
38239
+ });
38240
+ if (spawningParent && spawningParent.projectId !== command.projectId) return yield* new OrchestrationCommandInvariantError({
38241
+ commandType: command.type,
38242
+ detail: `Parent thread '${spawningParent.id}' belongs to a different project.`
38243
+ });
38020
38244
  return {
38021
38245
  ...yield* withEventBase({
38022
38246
  aggregateKind: "thread",
@@ -38037,6 +38261,7 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
38037
38261
  computerUseHostId: sidechatParent ? null : command.computerUseHostId ?? null,
38038
38262
  branch: sidechatParent ? sidechatParent.branch : command.branch,
38039
38263
  worktreePath: sidechatParent ? sidechatParent.worktreePath : command.worktreePath,
38264
+ ...spawningParent ? { parentThreadId: spawningParent.id } : {},
38040
38265
  createdAt: command.createdAt,
38041
38266
  updatedAt: command.createdAt
38042
38267
  }
@@ -39013,6 +39238,25 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
39013
39238
  commandType: command.type,
39014
39239
  detail: budgetBlock
39015
39240
  });
39241
+ if (command.queueIfBusy === true && isThreadBusy(targetThread, command.createdAt)) return {
39242
+ ...yield* withEventBase({
39243
+ aggregateKind: "thread",
39244
+ aggregateId: command.threadId,
39245
+ occurredAt: command.createdAt,
39246
+ commandId: command.commandId
39247
+ }),
39248
+ type: "thread.message-queued",
39249
+ payload: {
39250
+ threadId: command.threadId,
39251
+ message: {
39252
+ messageId: command.message.messageId,
39253
+ text: command.message.text,
39254
+ attachments: command.message.attachments,
39255
+ ...command.modelSelection !== void 0 ? { modelSelection: command.modelSelection } : {},
39256
+ createdAt: command.createdAt
39257
+ }
39258
+ }
39259
+ };
39016
39260
  if (activePair?.watcherThreadId === command.threadId && targetThread.interactionMode !== "default") return yield* decideCommandSequence({
39017
39261
  readModel,
39018
39262
  commands: [{
@@ -39101,6 +39345,56 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
39101
39345
  turnStartRequestedEvent
39102
39346
  ];
39103
39347
  }
39348
+ case "thread.queued-message.send":
39349
+ case "thread.queued-message.remove": {
39350
+ const thread = yield* requireThread({
39351
+ readModel,
39352
+ command,
39353
+ threadId: command.threadId
39354
+ });
39355
+ const queued = (thread.queuedMessages ?? []).find((entry) => entry.messageId === command.messageId);
39356
+ if (queued === void 0) return yield* new OrchestrationCommandInvariantError({
39357
+ commandType: command.type,
39358
+ detail: `Message '${command.messageId}' is not queued on thread '${command.threadId}'.`
39359
+ });
39360
+ const removedEvent = {
39361
+ ...yield* withEventBase({
39362
+ aggregateKind: "thread",
39363
+ aggregateId: command.threadId,
39364
+ occurredAt: command.createdAt,
39365
+ commandId: command.commandId
39366
+ }),
39367
+ type: "thread.queued-message-removed",
39368
+ payload: {
39369
+ threadId: command.threadId,
39370
+ messageId: command.messageId,
39371
+ reason: command.type === "thread.queued-message.send" ? "sent" : "removed",
39372
+ removedAt: command.createdAt
39373
+ }
39374
+ };
39375
+ if (command.type === "thread.queued-message.remove") return removedEvent;
39376
+ const turnStart = yield* decideCommand({
39377
+ readModel,
39378
+ command: {
39379
+ type: "thread.turn.start",
39380
+ commandId: command.commandId,
39381
+ threadId: command.threadId,
39382
+ message: {
39383
+ messageId: queued.messageId,
39384
+ role: "user",
39385
+ text: queued.text,
39386
+ attachments: queued.attachments
39387
+ },
39388
+ ...queued.modelSelection !== void 0 ? { modelSelection: queued.modelSelection } : {},
39389
+ runtimeMode: thread.runtimeMode,
39390
+ interactionMode: thread.interactionMode,
39391
+ compressMode: thread.compressMode,
39392
+ unpromptedSubagents: thread.unpromptedSubagents,
39393
+ createdAt: command.createdAt
39394
+ }
39395
+ });
39396
+ return [removedEvent, ...Array.isArray(turnStart) ? turnStart : [turnStart]];
39397
+ }
39104
39398
  case "thread.turn.interrupt":
39105
39399
  yield* requireThread({
39106
39400
  readModel,
@@ -39297,13 +39591,13 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
39297
39591
  });
39298
39592
  return events;
39299
39593
  }
39300
- case "thread.session.set":
39301
- yield* requireThread({
39594
+ case "thread.session.set": {
39595
+ const thread = yield* requireThread({
39302
39596
  readModel,
39303
39597
  command,
39304
39598
  threadId: command.threadId
39305
39599
  });
39306
- return {
39600
+ const sessionSetEvent = {
39307
39601
  ...yield* withEventBase({
39308
39602
  aggregateKind: "thread",
39309
39603
  aggregateId: command.threadId,
@@ -39317,6 +39611,26 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
39317
39611
  session: command.session
39318
39612
  }
39319
39613
  };
39614
+ const queuedHead = thread.queuedMessages?.[0];
39615
+ const previousStatus = thread.session?.status;
39616
+ if (queuedHead === void 0 || command.session.status !== "ready" && command.session.status !== "stopped" || previousStatus !== "running" && previousStatus !== "starting") return sessionSetEvent;
39617
+ const settledReadModel = yield* projectEvent(readModel, {
39618
+ ...sessionSetEvent,
39619
+ sequence: readModel.snapshotSequence + 1
39620
+ }).pipe(Effect.orDie);
39621
+ const settledThread = settledReadModel.threads.find((entry) => entry.id === thread.id);
39622
+ if (settledThread === void 0 || isThreadBusy(settledThread, command.createdAt)) return sessionSetEvent;
39623
+ return [sessionSetEvent, ...yield* decideCommandSequence({
39624
+ readModel: settledReadModel,
39625
+ commands: [{
39626
+ type: "thread.queued-message.send",
39627
+ commandId: CommandId.make(`queued-message:${queuedHead.messageId}`),
39628
+ threadId: command.threadId,
39629
+ messageId: queuedHead.messageId,
39630
+ createdAt: command.createdAt
39631
+ }]
39632
+ }).pipe(Effect.catchTag("OrchestrationCommandInvariantError", () => Effect.succeed([])))];
39633
+ }
39320
39634
  case "thread.message.assistant.delta":
39321
39635
  yield* requireThread({
39322
39636
  readModel,
@@ -39863,6 +40177,19 @@ const GetProjectionThreadScheduledTaskInput = Schema$1.Struct({ taskId: Orchestr
39863
40177
  const ListProjectionThreadScheduledTasksInput = Schema$1.Struct({ threadId: ThreadId });
39864
40178
  var ProjectionThreadScheduledTaskRepository = class extends Context.Service()("@p4code/cli/persistence/Services/ProjectionThreadScheduledTasks/ProjectionThreadScheduledTaskRepository") {};
39865
40179
  //#endregion
40180
+ //#region src/persistence/Services/ProjectionThreadQueuedMessages.ts
40181
+ const ProjectionThreadQueuedMessage = Schema$1.Struct({
40182
+ messageId: MessageId,
40183
+ threadId: ThreadId,
40184
+ text: Schema$1.String,
40185
+ attachments: Schema$1.fromJsonString(Schema$1.Array(ChatAttachment)),
40186
+ modelSelection: Schema$1.NullOr(Schema$1.fromJsonString(ModelSelection)),
40187
+ createdAt: IsoDateTime
40188
+ });
40189
+ const DeleteProjectionThreadQueuedMessageInput = Schema$1.Struct({ messageId: MessageId });
40190
+ const DeleteProjectionThreadQueuedMessagesByThreadInput = Schema$1.Struct({ threadId: ThreadId });
40191
+ var ProjectionThreadQueuedMessageRepository = class extends Context.Service()("@p4code/cli/persistence/Services/ProjectionThreadQueuedMessages/ProjectionThreadQueuedMessageRepository") {};
40192
+ //#endregion
39866
40193
  //#region src/persistence/Services/ProjectionThreadSessions.ts
39867
40194
  /**
39868
40195
  * ProjectionThreadSessionRepository - Repository interface for thread sessions.
@@ -39993,6 +40320,7 @@ const ProjectionThread = Schema$1.Struct({
39993
40320
  snoozedAt: Schema$1.NullOr(IsoDateTime),
39994
40321
  pinnedAt: Schema$1.NullOr(IsoDateTime),
39995
40322
  pinOrderKey: Schema$1.NullOr(Schema$1.String),
40323
+ parentThreadId: Schema$1.optional(Schema$1.NullOr(ThreadId)),
39996
40324
  latestUserMessageAt: Schema$1.NullOr(IsoDateTime),
39997
40325
  pendingApprovalCount: NonNegativeInt,
39998
40326
  pendingUserInputCount: NonNegativeInt,
@@ -40646,6 +40974,62 @@ const makeProjectionThreadScheduledTaskRepository = Effect.gen(function* () {
40646
40974
  });
40647
40975
  const ProjectionThreadScheduledTaskRepositoryLive = Layer.effect(ProjectionThreadScheduledTaskRepository, makeProjectionThreadScheduledTaskRepository);
40648
40976
  //#endregion
40977
+ //#region src/persistence/Layers/ProjectionThreadQueuedMessages.ts
40978
+ const makeProjectionThreadQueuedMessageRepository = Effect.gen(function* () {
40979
+ const sql = yield* SqlClient.SqlClient;
40980
+ const upsertRow = SqlSchema.void({
40981
+ Request: ProjectionThreadQueuedMessage,
40982
+ execute: (row) => sql`
40983
+ INSERT INTO projection_thread_queued_messages (
40984
+ message_id,
40985
+ thread_id,
40986
+ text,
40987
+ attachments_json,
40988
+ model_selection_json,
40989
+ created_at
40990
+ )
40991
+ VALUES (
40992
+ ${row.messageId},
40993
+ ${row.threadId},
40994
+ ${row.text},
40995
+ ${row.attachments},
40996
+ ${row.modelSelection},
40997
+ ${row.createdAt}
40998
+ )
40999
+ ON CONFLICT (message_id)
41000
+ DO UPDATE SET
41001
+ thread_id = excluded.thread_id,
41002
+ text = excluded.text,
41003
+ attachments_json = excluded.attachments_json,
41004
+ model_selection_json = excluded.model_selection_json,
41005
+ created_at = excluded.created_at
41006
+ `
41007
+ });
41008
+ const deleteRow = SqlSchema.void({
41009
+ Request: DeleteProjectionThreadQueuedMessageInput,
41010
+ execute: ({ messageId }) => sql`
41011
+ DELETE FROM projection_thread_queued_messages
41012
+ WHERE message_id = ${messageId}
41013
+ `
41014
+ });
41015
+ const deleteThreadRows = SqlSchema.void({
41016
+ Request: DeleteProjectionThreadQueuedMessagesByThreadInput,
41017
+ execute: ({ threadId }) => sql`
41018
+ DELETE FROM projection_thread_queued_messages
41019
+ WHERE thread_id = ${threadId}
41020
+ `
41021
+ });
41022
+ const upsert = (row) => upsertRow(row).pipe(Effect.mapError(toPersistenceSqlError("ProjectionThreadQueuedMessageRepository.upsert:query")));
41023
+ const deleteByMessageId = (input) => deleteRow(input).pipe(Effect.mapError(toPersistenceSqlError("ProjectionThreadQueuedMessageRepository.deleteByMessageId:query")));
41024
+ const deleteByThreadId = (input) => deleteThreadRows(input).pipe(Effect.mapError(toPersistenceSqlError("ProjectionThreadQueuedMessageRepository.deleteByThreadId:query")));
41025
+ return {
41026
+ upsert,
41027
+ deleteByMessageId,
41028
+ deleteByThreadId
41029
+ };
41030
+ });
41031
+ const ProjectionThreadQueuedMessageRepositoryLive = Layer.effect(ProjectionThreadQueuedMessageRepository, makeProjectionThreadQueuedMessageRepository);
41032
+ //#endregion
40649
41033
  //#region src/persistence/Layers/ProjectionThreadSessions.ts
40650
41034
  const makeProjectionThreadSessionRepository = Effect.gen(function* () {
40651
41035
  const sql = yield* SqlClient.SqlClient;
@@ -40984,6 +41368,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
40984
41368
  snoozed_at,
40985
41369
  pinned_at,
40986
41370
  pin_order_key,
41371
+ parent_thread_id,
40987
41372
  latest_user_message_at,
40988
41373
  pending_approval_count,
40989
41374
  pending_user_input_count,
@@ -41020,6 +41405,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
41020
41405
  ${row.snoozedAt},
41021
41406
  ${row.pinnedAt},
41022
41407
  ${row.pinOrderKey},
41408
+ ${row.parentThreadId ?? null},
41023
41409
  ${row.latestUserMessageAt},
41024
41410
  ${row.pendingApprovalCount},
41025
41411
  ${row.pendingUserInputCount},
@@ -41056,6 +41442,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
41056
41442
  snoozed_at = excluded.snoozed_at,
41057
41443
  pinned_at = excluded.pinned_at,
41058
41444
  pin_order_key = excluded.pin_order_key,
41445
+ parent_thread_id = COALESCE(excluded.parent_thread_id, projection_threads.parent_thread_id),
41059
41446
  latest_user_message_at = excluded.latest_user_message_at,
41060
41447
  pending_approval_count = excluded.pending_approval_count,
41061
41448
  pending_user_input_count = excluded.pending_user_input_count,
@@ -41097,6 +41484,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
41097
41484
  snoozed_at AS "snoozedAt",
41098
41485
  pinned_at AS "pinnedAt",
41099
41486
  pin_order_key AS "pinOrderKey",
41487
+ parent_thread_id AS "parentThreadId",
41100
41488
  latest_user_message_at AS "latestUserMessageAt",
41101
41489
  pending_approval_count AS "pendingApprovalCount",
41102
41490
  pending_user_input_count AS "pendingUserInputCount",
@@ -41140,6 +41528,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
41140
41528
  snoozed_at AS "snoozedAt",
41141
41529
  pinned_at AS "pinnedAt",
41142
41530
  pin_order_key AS "pinOrderKey",
41531
+ parent_thread_id AS "parentThreadId",
41143
41532
  latest_user_message_at AS "latestUserMessageAt",
41144
41533
  pending_approval_count AS "pendingApprovalCount",
41145
41534
  pending_user_input_count AS "pendingUserInputCount",
@@ -41189,6 +41578,7 @@ const ORCHESTRATION_PROJECTOR_NAMES = {
41189
41578
  threadMessages: "projection.thread-messages",
41190
41579
  threadProposedPlans: "projection.thread-proposed-plans",
41191
41580
  threadScheduledTasks: "projection.thread-scheduled-tasks",
41581
+ threadQueuedMessages: "projection.thread-queued-messages",
41192
41582
  threadActivities: "projection.thread-activities",
41193
41583
  threadSessions: "projection.thread-sessions",
41194
41584
  threadTurns: "projection.thread-turns",
@@ -41400,6 +41790,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
41400
41790
  const projectionThreadMessageRepository = yield* ProjectionThreadMessageRepository;
41401
41791
  const projectionThreadProposedPlanRepository = yield* ProjectionThreadProposedPlanRepository;
41402
41792
  const projectionThreadScheduledTaskRepository = yield* ProjectionThreadScheduledTaskRepository;
41793
+ const projectionThreadQueuedMessageRepository = yield* ProjectionThreadQueuedMessageRepository;
41403
41794
  const projectionThreadActivityRepository = yield* ProjectionThreadActivityRepository;
41404
41795
  const projectionThreadSessionRepository = yield* ProjectionThreadSessionRepository;
41405
41796
  const projectionTurnRepository = yield* ProjectionTurnRepository;
@@ -41619,6 +42010,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
41619
42010
  snoozedAt: null,
41620
42011
  pinnedAt: null,
41621
42012
  pinOrderKey: null,
42013
+ parentThreadId: event.payload.parentThreadId ?? null,
41622
42014
  latestUserMessageAt: null,
41623
42015
  pendingApprovalCount: 0,
41624
42016
  pendingUserInputCount: 0,
@@ -41936,6 +42328,27 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
41936
42328
  default: return;
41937
42329
  }
41938
42330
  });
42331
+ const applyThreadQueuedMessagesProjection = Effect.fn("applyThreadQueuedMessagesProjection")(function* (event) {
42332
+ switch (event.type) {
42333
+ case "thread.message-queued":
42334
+ yield* projectionThreadQueuedMessageRepository.upsert({
42335
+ messageId: event.payload.message.messageId,
42336
+ threadId: event.payload.threadId,
42337
+ text: event.payload.message.text,
42338
+ attachments: event.payload.message.attachments,
42339
+ modelSelection: event.payload.message.modelSelection ?? null,
42340
+ createdAt: event.payload.message.createdAt
42341
+ });
42342
+ return;
42343
+ case "thread.queued-message-removed":
42344
+ yield* projectionThreadQueuedMessageRepository.deleteByMessageId({ messageId: event.payload.messageId });
42345
+ return;
42346
+ case "thread.deleted":
42347
+ yield* projectionThreadQueuedMessageRepository.deleteByThreadId({ threadId: event.payload.threadId });
42348
+ return;
42349
+ default: return;
42350
+ }
42351
+ });
41939
42352
  const applyThreadScheduledTasksProjection = Effect.fn("applyThreadScheduledTasksProjection")(function* (event) {
41940
42353
  switch (event.type) {
41941
42354
  case "thread.scheduled-task.created":
@@ -42345,6 +42758,10 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
42345
42758
  name: ORCHESTRATION_PROJECTOR_NAMES.threadScheduledTasks,
42346
42759
  apply: applyThreadScheduledTasksProjection
42347
42760
  },
42761
+ {
42762
+ name: ORCHESTRATION_PROJECTOR_NAMES.threadQueuedMessages,
42763
+ apply: applyThreadQueuedMessagesProjection
42764
+ },
42348
42765
  {
42349
42766
  name: ORCHESTRATION_PROJECTOR_NAMES.threadActivities,
42350
42767
  apply: applyThreadActivitiesProjection
@@ -42394,7 +42811,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
42394
42811
  projectEvent
42395
42812
  };
42396
42813
  });
42397
- const OrchestrationProjectionPipelineLive = Layer.effect(OrchestrationProjectionPipeline, makeOrchestrationProjectionPipeline()).pipe(Layer.provideMerge(ProjectionProjectRepositoryLive), Layer.provideMerge(ProjectionThreadRepositoryLive), Layer.provideMerge(ProjectionThreadMessageRepositoryLive), Layer.provideMerge(ProjectionThreadProposedPlanRepositoryLive), Layer.provideMerge(ProjectionThreadScheduledTaskRepositoryLive), Layer.provideMerge(ProjectionThreadActivityRepositoryLive), Layer.provideMerge(ProjectionThreadSessionRepositoryLive), Layer.provideMerge(ProjectionTurnRepositoryLive), Layer.provideMerge(ProjectionPendingApprovalRepositoryLive), Layer.provideMerge(ProjectionStateRepositoryLive));
42814
+ const OrchestrationProjectionPipelineLive = Layer.effect(OrchestrationProjectionPipeline, makeOrchestrationProjectionPipeline()).pipe(Layer.provideMerge(ProjectionProjectRepositoryLive), Layer.provideMerge(ProjectionThreadRepositoryLive), Layer.provideMerge(ProjectionThreadMessageRepositoryLive), Layer.provideMerge(ProjectionThreadProposedPlanRepositoryLive), Layer.provideMerge(ProjectionThreadScheduledTaskRepositoryLive), Layer.provideMerge(ProjectionThreadQueuedMessageRepositoryLive), Layer.provideMerge(ProjectionThreadActivityRepositoryLive), Layer.provideMerge(ProjectionThreadSessionRepositoryLive), Layer.provideMerge(ProjectionTurnRepositoryLive), Layer.provideMerge(ProjectionPendingApprovalRepositoryLive), Layer.provideMerge(ProjectionStateRepositoryLive));
42398
42815
  //#endregion
42399
42816
  //#region src/persistence/Services/ProjectionCheckpoints.ts
42400
42817
  /**
@@ -42496,6 +42913,10 @@ function deriveLocalBranchNameFromRemoteRef$1(branchName) {
42496
42913
  if (firstSeparatorIndex <= 0 || firstSeparatorIndex === branchName.length - 1) return branchName;
42497
42914
  return branchName.slice(firstSeparatorIndex + 1);
42498
42915
  }
42916
+ function buildTemporaryWorktreeBranchName(randomHex) {
42917
+ const token = randomHex(4).toLowerCase().replace(/[^0-9a-f]/g, "").slice(0, 8);
42918
+ return `${WORKTREE_BRANCH_PREFIX}/${token}`;
42919
+ }
42499
42920
  function isTemporaryWorktreeBranch(refName) {
42500
42921
  return TEMP_WORKTREE_BRANCH_PATTERN.test(refName.trim().toLowerCase());
42501
42922
  }
@@ -43170,6 +43591,24 @@ function mapScheduledTaskRow(row) {
43170
43591
  failure: row.failure
43171
43592
  };
43172
43593
  }
43594
+ function mapQueuedMessageRow(row) {
43595
+ return {
43596
+ messageId: row.messageId,
43597
+ text: row.text,
43598
+ attachments: row.attachments,
43599
+ ...row.modelSelection !== null ? { modelSelection: row.modelSelection } : {},
43600
+ createdAt: row.createdAt
43601
+ };
43602
+ }
43603
+ function groupQueuedMessagesByThread(rows) {
43604
+ const byThread = /* @__PURE__ */ new Map();
43605
+ for (const row of rows) {
43606
+ const messages = byThread.get(row.threadId) ?? [];
43607
+ messages.push(mapQueuedMessageRow(row));
43608
+ byThread.set(row.threadId, messages);
43609
+ }
43610
+ return byThread;
43611
+ }
43173
43612
  function groupScheduledTasksByThread(rows) {
43174
43613
  const byThread = /* @__PURE__ */ new Map();
43175
43614
  for (const row of rows) {
@@ -43516,6 +43955,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43516
43955
  snoozed_at AS "snoozedAt",
43517
43956
  pinned_at AS "pinnedAt",
43518
43957
  pin_order_key AS "pinOrderKey",
43958
+ parent_thread_id AS "parentThreadId",
43519
43959
  latest_user_message_at AS "latestUserMessageAt",
43520
43960
  pending_approval_count AS "pendingApprovalCount",
43521
43961
  pending_user_input_count AS "pendingUserInputCount",
@@ -43562,6 +44002,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43562
44002
  snoozed_at AS "snoozedAt",
43563
44003
  pinned_at AS "pinnedAt",
43564
44004
  pin_order_key AS "pinOrderKey",
44005
+ parent_thread_id AS "parentThreadId",
43565
44006
  latest_user_message_at AS "latestUserMessageAt",
43566
44007
  pending_approval_count AS "pendingApprovalCount",
43567
44008
  pending_user_input_count AS "pendingUserInputCount",
@@ -43610,6 +44051,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43610
44051
  snoozed_at AS "snoozedAt",
43611
44052
  pinned_at AS "pinnedAt",
43612
44053
  pin_order_key AS "pinOrderKey",
44054
+ parent_thread_id AS "parentThreadId",
43613
44055
  latest_user_message_at AS "latestUserMessageAt",
43614
44056
  pending_approval_count AS "pendingApprovalCount",
43615
44057
  pending_user_input_count AS "pendingUserInputCount",
@@ -43675,6 +44117,33 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43675
44117
  failure
43676
44118
  FROM projection_thread_scheduled_tasks
43677
44119
  ORDER BY thread_id ASC, run_at ASC, task_id ASC
44120
+ `
44121
+ });
44122
+ const queuedMessageColumns = sql`
44123
+ message_id AS "messageId",
44124
+ thread_id AS "threadId",
44125
+ text,
44126
+ attachments_json AS "attachments",
44127
+ model_selection_json AS "modelSelection",
44128
+ created_at AS "createdAt"
44129
+ `;
44130
+ const listThreadQueuedMessageRows = SqlSchema.findAll({
44131
+ Request: Schema$1.Void,
44132
+ Result: ProjectionThreadQueuedMessage,
44133
+ execute: () => sql`
44134
+ SELECT ${queuedMessageColumns}
44135
+ FROM projection_thread_queued_messages
44136
+ ORDER BY thread_id ASC, rowid ASC
44137
+ `
44138
+ });
44139
+ const listThreadQueuedMessageRowsByThread = SqlSchema.findAll({
44140
+ Request: ThreadIdLookupInput,
44141
+ Result: ProjectionThreadQueuedMessage,
44142
+ execute: ({ threadId }) => sql`
44143
+ SELECT ${queuedMessageColumns}
44144
+ FROM projection_thread_queued_messages
44145
+ WHERE thread_id = ${threadId}
44146
+ ORDER BY rowid ASC
43678
44147
  `
43679
44148
  });
43680
44149
  const listThreadScheduledTaskRowsByThread = SqlSchema.findAll({
@@ -44117,6 +44586,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
44117
44586
  snoozed_at AS "snoozedAt",
44118
44587
  pinned_at AS "pinnedAt",
44119
44588
  pin_order_key AS "pinOrderKey",
44589
+ parent_thread_id AS "parentThreadId",
44120
44590
  latest_user_message_at AS "latestUserMessageAt",
44121
44591
  pending_approval_count AS "pendingApprovalCount",
44122
44592
  pending_user_input_count AS "pendingUserInputCount",
@@ -44731,9 +45201,11 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
44731
45201
  listLatestTurnRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listLatestTurns:query", "ProjectionSnapshotQuery.getSnapshot:listLatestTurns:decodeRows"))),
44732
45202
  listProjectionStateRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listProjectionState:query", "ProjectionSnapshotQuery.getSnapshot:listProjectionState:decodeRows"))),
44733
45203
  listThreadPairRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listThreadPairs:query", "ProjectionSnapshotQuery.getSnapshot:listThreadPairs:decodeRows"))),
44734
- listThreadScheduledTaskRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listThreadScheduledTasks:query", "ProjectionSnapshotQuery.getSnapshot:listThreadScheduledTasks:decodeRows")))
44735
- ])).pipe(Effect.flatMap(([projectRows, threadRows, messageRows, proposedPlanRows, activityRows, sessionRows, checkpointRows, turnRows, latestTurnRows, stateRows, threadPairRows, scheduledTaskRows]) => Effect.gen(function* () {
45204
+ listThreadScheduledTaskRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listThreadScheduledTasks:query", "ProjectionSnapshotQuery.getSnapshot:listThreadScheduledTasks:decodeRows"))),
45205
+ listThreadQueuedMessageRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listThreadQueuedMessages:query", "ProjectionSnapshotQuery.getSnapshot:listThreadQueuedMessages:decodeRows")))
45206
+ ])).pipe(Effect.flatMap(([projectRows, threadRows, messageRows, proposedPlanRows, activityRows, sessionRows, checkpointRows, turnRows, latestTurnRows, stateRows, threadPairRows, scheduledTaskRows, queuedMessageRows]) => Effect.gen(function* () {
44736
45207
  const scheduledTasksByThread = groupScheduledTasksByThread(scheduledTaskRows);
45208
+ const queuedMessagesByThread = groupQueuedMessagesByThread(queuedMessageRows);
44737
45209
  const messagesByThread = /* @__PURE__ */ new Map();
44738
45210
  const proposedPlansByThread = /* @__PURE__ */ new Map();
44739
45211
  const activitiesByThread = /* @__PURE__ */ new Map();
@@ -44862,10 +45334,12 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
44862
45334
  snoozedAt: row.snoozedAt,
44863
45335
  pinnedAt: row.pinnedAt,
44864
45336
  pinOrderKey: row.pinOrderKey ?? null,
45337
+ parentThreadId: row.parentThreadId ?? null,
44865
45338
  deletedAt: row.deletedAt,
44866
45339
  messages: messagesByThread.get(row.threadId) ?? [],
44867
45340
  proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
44868
45341
  scheduledTasks: scheduledTasksByThread.get(row.threadId) ?? [],
45342
+ queuedMessages: queuedMessagesByThread.get(row.threadId) ?? [],
44869
45343
  activities: activitiesByThread.get(row.threadId) ?? [],
44870
45344
  checkpoints: checkpointsByThread.get(row.threadId) ?? [],
44871
45345
  session: sessionsByThread.get(row.threadId) ?? null
@@ -44977,9 +45451,11 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
44977
45451
  listLatestTurnRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listLatestTurns:query", "ProjectionSnapshotQuery.getCommandReadModel:listLatestTurns:decodeRows"))),
44978
45452
  listProjectionStateRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listProjectionState:query", "ProjectionSnapshotQuery.getCommandReadModel:listProjectionState:decodeRows"))),
44979
45453
  listThreadPairRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listThreadPairs:query", "ProjectionSnapshotQuery.getCommandReadModel:listThreadPairs:decodeRows"))),
44980
- listThreadScheduledTaskRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listThreadScheduledTasks:query", "ProjectionSnapshotQuery.getCommandReadModel:listThreadScheduledTasks:decodeRows")))
44981
- ])).pipe(Effect.flatMap(([projectRows, threadRows, proposedPlanRows, sessionRows, latestTurnRows, stateRows, threadPairRows, scheduledTaskRows]) => Effect.sync(() => {
45454
+ listThreadScheduledTaskRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listThreadScheduledTasks:query", "ProjectionSnapshotQuery.getCommandReadModel:listThreadScheduledTasks:decodeRows"))),
45455
+ listThreadQueuedMessageRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listThreadQueuedMessages:query", "ProjectionSnapshotQuery.getCommandReadModel:listThreadQueuedMessages:decodeRows")))
45456
+ ])).pipe(Effect.flatMap(([projectRows, threadRows, proposedPlanRows, sessionRows, latestTurnRows, stateRows, threadPairRows, scheduledTaskRows, queuedMessageRows]) => Effect.sync(() => {
44982
45457
  const scheduledTasksByThread = groupScheduledTasksByThread(scheduledTaskRows);
45458
+ const queuedMessagesByThread = groupQueuedMessagesByThread(queuedMessageRows);
44983
45459
  let updatedAt = null;
44984
45460
  const projects = [];
44985
45461
  const threads = [];
@@ -45078,10 +45554,12 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45078
45554
  snoozedAt: row.snoozedAt,
45079
45555
  pinnedAt: row.pinnedAt,
45080
45556
  pinOrderKey: row.pinOrderKey ?? null,
45557
+ parentThreadId: row.parentThreadId ?? null,
45081
45558
  deletedAt: row.deletedAt,
45082
45559
  messages: [],
45083
45560
  proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
45084
45561
  scheduledTasks: scheduledTasksByThread.get(row.threadId) ?? [],
45562
+ queuedMessages: queuedMessagesByThread.get(row.threadId) ?? [],
45085
45563
  activities: [],
45086
45564
  checkpoints: [],
45087
45565
  session: sessionByThread.get(row.threadId) ?? null
@@ -45151,6 +45629,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45151
45629
  snoozedAt: row.snoozedAt,
45152
45630
  pinnedAt: row.pinnedAt,
45153
45631
  pinOrderKey: row.pinOrderKey ?? null,
45632
+ parentThreadId: row.parentThreadId ?? null,
45154
45633
  session: sessionByThread.get(row.threadId) ?? null,
45155
45634
  latestUserMessageAt: row.latestUserMessageAt,
45156
45635
  hasPendingApprovals: row.pendingApprovalCount > 0,
@@ -45224,6 +45703,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45224
45703
  snoozedAt: row.snoozedAt,
45225
45704
  pinnedAt: row.pinnedAt,
45226
45705
  pinOrderKey: row.pinOrderKey ?? null,
45706
+ parentThreadId: row.parentThreadId ?? null,
45227
45707
  session: sessionByThread.get(row.threadId) ?? null,
45228
45708
  latestUserMessageAt: row.latestUserMessageAt,
45229
45709
  hasPendingApprovals: row.pendingApprovalCount > 0,
@@ -45367,6 +45847,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45367
45847
  snoozedAt: threadRow.value.snoozedAt,
45368
45848
  pinnedAt: threadRow.value.pinnedAt,
45369
45849
  pinOrderKey: threadRow.value.pinOrderKey ?? null,
45850
+ parentThreadId: threadRow.value.parentThreadId ?? null,
45370
45851
  session: Option.isSome(sessionRow) ? mapSessionRow(sessionRow.value) : null,
45371
45852
  latestUserMessageAt: threadRow.value.latestUserMessageAt,
45372
45853
  hasPendingApprovals: threadRow.value.pendingApprovalCount > 0,
@@ -45398,7 +45879,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45398
45879
  })).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listActivities:query", "ProjectionSnapshotQuery.getThreadDetailById:listActivities:decodeRows")), Effect.map((rows) => rows.map(mapThreadActivityRow)));
45399
45880
  const threadRow = yield* getThreadRowById({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getThread:query", "ProjectionSnapshotQuery.getThreadDetailById:getThread:decodeRow")));
45400
45881
  if (Option.isNone(threadRow)) return Option.none();
45401
- const [messageRows, proposedPlanRows, activities, checkpointRows, turnRows, latestTurnRow, sessionRow, scheduledTaskRows] = yield* Effect.all([
45882
+ const [messageRows, proposedPlanRows, activities, checkpointRows, turnRows, latestTurnRow, sessionRow, scheduledTaskRows, queuedMessageRows] = yield* Effect.all([
45402
45883
  (bounds === void 0 ? listThreadMessageRowsByThread({ threadId }) : listThreadMessageRowsByThreadWindow({
45403
45884
  threadId,
45404
45885
  ...bounds
@@ -45409,7 +45890,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45409
45890
  listTurnSummaryRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listTurnSummaries:query", "ProjectionSnapshotQuery.getThreadDetailById:listTurnSummaries:decodeRows"))),
45410
45891
  getLatestTurnRowByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getLatestTurn:query", "ProjectionSnapshotQuery.getThreadDetailById:getLatestTurn:decodeRow"))),
45411
45892
  getThreadSessionRowByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getSession:query", "ProjectionSnapshotQuery.getThreadDetailById:getSession:decodeRow"))),
45412
- listThreadScheduledTaskRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listScheduledTasks:query", "ProjectionSnapshotQuery.getThreadDetailById:listScheduledTasks:decodeRows")))
45893
+ listThreadScheduledTaskRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listScheduledTasks:query", "ProjectionSnapshotQuery.getThreadDetailById:listScheduledTasks:decodeRows"))),
45894
+ listThreadQueuedMessageRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listQueuedMessages:query", "ProjectionSnapshotQuery.getThreadDetailById:listQueuedMessages:decodeRows")))
45413
45895
  ]);
45414
45896
  const thread = {
45415
45897
  id: threadRow.value.threadId,
@@ -45441,6 +45923,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45441
45923
  snoozedAt: threadRow.value.snoozedAt,
45442
45924
  pinnedAt: threadRow.value.pinnedAt,
45443
45925
  pinOrderKey: threadRow.value.pinOrderKey ?? null,
45926
+ parentThreadId: threadRow.value.parentThreadId ?? null,
45444
45927
  deletedAt: null,
45445
45928
  messages: messageRows.map((row) => {
45446
45929
  const message = {
@@ -45457,6 +45940,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45457
45940
  }),
45458
45941
  proposedPlans: proposedPlanRows.map(mapProposedPlanRow),
45459
45942
  scheduledTasks: scheduledTaskRows.map(mapScheduledTaskRow),
45943
+ queuedMessages: queuedMessageRows.map(mapQueuedMessageRow),
45460
45944
  activities,
45461
45945
  checkpoints: checkpointRows.map((row) => ({
45462
45946
  turnId: row.turnId,
@@ -48273,6 +48757,7 @@ const make$96 = Effect.gen(function* () {
48273
48757
  threadPinning: true,
48274
48758
  threadFork: true,
48275
48759
  threadScheduledTasks: true,
48760
+ threadMessageQueue: true,
48276
48761
  ...serverSelfUpdate === null ? {} : { serverSelfUpdate },
48277
48762
  ...serverServiceSupervised ? { serverServiceSupervised } : {}
48278
48763
  }
@@ -58019,11 +58504,22 @@ const requireThreadControlTarget = Effect.fn("mcp.requireThreadControlTarget")(f
58019
58504
  invocation,
58020
58505
  threadId: invocation.threadId
58021
58506
  };
58022
- if (!invocation.controlThreadIds?.has(requested)) return yield* new ThreadControlNotPermittedError({ threadId: requested });
58023
- return {
58507
+ if (invocation.controlThreadIds?.has(requested)) return {
58508
+ invocation,
58509
+ threadId: requested
58510
+ };
58511
+ if (yield* isChildOfInvocationThread(invocation.threadId, requested)) return {
58024
58512
  invocation,
58025
58513
  threadId: requested
58026
58514
  };
58515
+ return yield* new ThreadControlNotPermittedError({ threadId: requested });
58516
+ });
58517
+ /** Whether `childThreadId` was spawned by `parentThreadId` as a sub-thread. */
58518
+ const isChildOfInvocationThread = Effect.fn("mcp.isChildOfInvocationThread")(function* (parentThreadId, childThreadId) {
58519
+ const query = yield* Effect.serviceOption(ProjectionSnapshotQuery);
58520
+ if (Option.isNone(query)) return false;
58521
+ const shell = yield* query.value.getThreadShellById(childThreadId).pipe(Effect.catch(() => Effect.succeed(Option.none())));
58522
+ return Option.isSome(shell) && shell.value.parentThreadId === parentThreadId;
58027
58523
  });
58028
58524
  /**
58029
58525
  * The guard on starting a thread, which is a different question from acting on
@@ -94144,6 +94640,31 @@ const makeCodexSessionRuntime = (options) => Effect.gen(function* () {
94144
94640
  ...options.fusionPromptSettings ? { fusionPromptSettings: options.fusionPromptSettings } : {},
94145
94641
  ...input.unpromptedSubagents !== void 0 ? { unpromptedSubagents: input.unpromptedSubagents } : {}
94146
94642
  });
94643
+ const activeSession = yield* Ref.get(sessionRef);
94644
+ if (activeSession.status === "running" && activeSession.activeTurnId !== void 0) {
94645
+ const steeredTurnId = activeSession.activeTurnId;
94646
+ const steerExit = yield* client.raw.request("turn/steer", {
94647
+ threadId: providerThreadId,
94648
+ expectedTurnId: steeredTurnId,
94649
+ input: params.input
94650
+ }).pipe(Effect.exit);
94651
+ if (Exit.isSuccess(steerExit)) {
94652
+ const steeredProviderThreadId = currentProviderThreadId(yield* Ref.get(sessionRef));
94653
+ return {
94654
+ threadId: options.threadId,
94655
+ turnId: steeredTurnId,
94656
+ ...steeredProviderThreadId ? { resumeCursor: { threadId: steeredProviderThreadId } } : {}
94657
+ };
94658
+ }
94659
+ if (findActiveCodexTurnId(yield* client.request("thread/read", {
94660
+ threadId: providerThreadId,
94661
+ includeTurns: true
94662
+ })) === steeredTurnId) {
94663
+ yield* Effect.logWarning("Codex turn/steer failed while the turn is still running.", { cause: steerExit.cause });
94664
+ return yield* Effect.failCause(steerExit.cause);
94665
+ }
94666
+ yield* Effect.logInfo("Codex turn ended before the steer landed; starting a new turn.");
94667
+ }
94147
94668
  const { collaborationMode: attachedCollaborationMode, ...paramsWithoutCollaboration } = params;
94148
94669
  const lastCollaborationMode = yield* Ref.get(lastCollaborationModeRef);
94149
94670
  const requestParams = attachedCollaborationMode !== void 0 && lastCollaborationMode !== null && isSameCollaborationMode(attachedCollaborationMode, lastCollaborationMode) ? paramsWithoutCollaboration : params;
@@ -125052,6 +125573,86 @@ const make$36 = Effect.gen(function* () {
125052
125573
  ...cleanup === void 0 ? {} : { cleanup }
125053
125574
  });
125054
125575
  }
125576
+ const finishMergedCleanup = Effect.fn("ThreadWorkspaceLifecycle.finishMergedCleanup")(function* (input) {
125577
+ yield* record({
125578
+ threadIds,
125579
+ lifecycle: {
125580
+ status: "cleanup-pending",
125581
+ detail: "Merged context recorded; workspace cleanup is pending.",
125582
+ pullRequestNumber: input.merged.pullRequestNumber,
125583
+ mergeCommitSha: input.merged.mergeCommitSha,
125584
+ updatedAt: now,
125585
+ ...cleanup === void 0 ? {} : { cleanup }
125586
+ }
125587
+ });
125588
+ yield* cleanGeneratedFiles();
125589
+ if (cleanup === void 0 || cleanup.worktree) yield* gitWorkflow.removeWorktree({
125590
+ cwd: project.workspaceRoot,
125591
+ path: worktreePath,
125592
+ ...cleanup?.forceWorktree ? { force: true } : {}
125593
+ });
125594
+ if (cleanup === void 0 || cleanup.localBranch) yield* git.execute({
125595
+ operation: "ThreadWorkspaceLifecycle.deleteBranch",
125596
+ cwd: project.workspaceRoot,
125597
+ args: [
125598
+ "update-ref",
125599
+ "-d",
125600
+ `refs/heads/${input.branch}`,
125601
+ input.branchCommitSha
125602
+ ]
125603
+ }).pipe(Effect.catchCause((cause) => gitWorkflow.createWorktree({
125604
+ cwd: project.workspaceRoot,
125605
+ refName: input.branch,
125606
+ path: worktreePath
125607
+ }).pipe(Effect.andThen(Effect.failCause(cause)))));
125608
+ const detail = cleanup === void 0 ? "Merged worktree and branch removed; thread context preserved." : "Selected archive resources removed; thread context preserved.";
125609
+ yield* record({
125610
+ threadIds,
125611
+ lifecycle: {
125612
+ status: "cleaned",
125613
+ detail,
125614
+ pullRequestNumber: input.merged.pullRequestNumber,
125615
+ mergeCommitSha: input.merged.mergeCommitSha,
125616
+ updatedAt: DateTime.formatIso(yield* DateTime.now)
125617
+ }
125618
+ });
125619
+ return {
125620
+ outcome: "cleaned",
125621
+ detail
125622
+ };
125623
+ });
125624
+ const parentThread = requestedThread.parentThreadId == null ? void 0 : snapshot.threads.find((thread) => thread.id === requestedThread.parentThreadId);
125625
+ if (parentThread !== void 0 && !cleanup?.remoteBranch) {
125626
+ if (yield* git.execute({
125627
+ operation: "ThreadWorkspaceLifecycle.landedInParent",
125628
+ cwd: parentThread.worktreePath ?? project.workspaceRoot,
125629
+ args: [
125630
+ "merge-base",
125631
+ "--is-ancestor",
125632
+ `refs/heads/${branch}`,
125633
+ "HEAD"
125634
+ ],
125635
+ allowNonZeroExit: true
125636
+ }).pipe(Effect.map((result) => result.exitCode === 0), Effect.orElseSucceed(() => false))) {
125637
+ const branchCommitSha = (yield* git.execute({
125638
+ operation: "ThreadWorkspaceLifecycle.branchCommit",
125639
+ cwd: project.workspaceRoot,
125640
+ args: [
125641
+ "rev-parse",
125642
+ "--verify",
125643
+ `refs/heads/${branch}^{commit}`
125644
+ ]
125645
+ })).stdout.trim();
125646
+ return yield* finishMergedCleanup({
125647
+ branch,
125648
+ branchCommitSha,
125649
+ merged: {
125650
+ mergeCommitSha: branchCommitSha,
125651
+ pullRequestNumber: null
125652
+ }
125653
+ });
125654
+ }
125655
+ }
125055
125656
  const refs = yield* gitWorkflow.listRefs({
125056
125657
  cwd: project.workspaceRoot,
125057
125658
  includeMatchingRemoteRefs: true,
@@ -125164,52 +125765,11 @@ const make$36 = Effect.gen(function* () {
125164
125765
  }).pipe(Effect.result))._tag === "Failure") return yield* workspaceRefusal("Cleanup refused because the remote branch could not be removed.");
125165
125766
  }
125166
125767
  }
125167
- yield* record({
125168
- threadIds,
125169
- lifecycle: {
125170
- status: "cleanup-pending",
125171
- detail: "Merged context recorded; workspace cleanup is pending.",
125172
- pullRequestNumber: merged.pullRequestNumber,
125173
- mergeCommitSha: merged.mergeCommitSha,
125174
- updatedAt: now,
125175
- ...cleanup === void 0 ? {} : { cleanup }
125176
- }
125177
- });
125178
- yield* cleanGeneratedFiles();
125179
- if (cleanup === void 0 || cleanup.worktree) yield* gitWorkflow.removeWorktree({
125180
- cwd: project.workspaceRoot,
125181
- path: worktreePath,
125182
- ...cleanup?.forceWorktree ? { force: true } : {}
125183
- });
125184
- if (cleanup === void 0 || cleanup.localBranch) yield* git.execute({
125185
- operation: "ThreadWorkspaceLifecycle.deleteBranch",
125186
- cwd: project.workspaceRoot,
125187
- args: [
125188
- "update-ref",
125189
- "-d",
125190
- `refs/heads/${branch}`,
125191
- branchCommitResult.stdout.trim()
125192
- ]
125193
- }).pipe(Effect.catchCause((cause) => gitWorkflow.createWorktree({
125194
- cwd: project.workspaceRoot,
125195
- refName: branch,
125196
- path: worktreePath
125197
- }).pipe(Effect.andThen(Effect.failCause(cause)))));
125198
- const detail = cleanup === void 0 ? "Merged worktree and branch removed; thread context preserved." : "Selected archive resources removed; thread context preserved.";
125199
- yield* record({
125200
- threadIds,
125201
- lifecycle: {
125202
- status: "cleaned",
125203
- detail,
125204
- pullRequestNumber: merged.pullRequestNumber,
125205
- mergeCommitSha: merged.mergeCommitSha,
125206
- updatedAt: DateTime.formatIso(yield* DateTime.now)
125207
- }
125768
+ return yield* finishMergedCleanup({
125769
+ branch,
125770
+ branchCommitSha: branchCommitResult.stdout.trim(),
125771
+ merged
125208
125772
  });
125209
- return {
125210
- outcome: "cleaned",
125211
- detail
125212
- };
125213
125773
  });
125214
125774
  const cleanup = (threadId) => cleanupRaw(threadId).pipe(mapLifecycleError);
125215
125775
  return {
@@ -133212,7 +133772,7 @@ function projectSetupScriptCompatibilityDetail(error) {
133212
133772
  }
133213
133773
  }
133214
133774
  function isThreadDetailEvent(event) {
133215
- return event.type === "thread.message-sent" || event.type === "thread.archived" || event.type === "thread.unarchived" || event.type === "thread.deleted" || event.type === "thread.meta-updated" || event.type === "thread.proposed-plan-upserted" || event.type === "thread.scheduled-task.created" || event.type === "thread.scheduled-task.cancelled" || event.type === "thread.scheduled-task.fired" || event.type === "thread.activity-appended" || event.type === "thread.turn-diff-completed" || event.type === "thread.reverted" || event.type === "thread.session-set";
133775
+ return event.type === "thread.message-sent" || event.type === "thread.message-queued" || event.type === "thread.queued-message-removed" || event.type === "thread.archived" || event.type === "thread.unarchived" || event.type === "thread.deleted" || event.type === "thread.meta-updated" || event.type === "thread.proposed-plan-upserted" || event.type === "thread.scheduled-task.created" || event.type === "thread.scheduled-task.cancelled" || event.type === "thread.scheduled-task.fired" || event.type === "thread.activity-appended" || event.type === "thread.turn-diff-completed" || event.type === "thread.reverted" || event.type === "thread.session-set";
133216
133776
  }
133217
133777
  const PROVIDER_STATUS_DEBOUNCE_MS = 200;
133218
133778
  const SHELL_RESUME_MAX_GAP = 1e3;
@@ -135254,7 +135814,7 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
135254
135814
  const next = new Map(records);
135255
135815
  next.set(tokenHash, {
135256
135816
  tokenHash,
135257
- scope: scopeWith(!spawnedThreads.has(threadId), spawnedThreads.get(threadId) === true),
135817
+ scope: scopeWith(!(spawnedThreads.has(threadId) || request.spawned === true), spawnedThreads.get(threadId) === true),
135258
135818
  controlThreadIds: /* @__PURE__ */ new Set(),
135259
135819
  lastAliveAt: issuedAt
135260
135820
  });
@@ -135657,7 +136217,8 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
135657
136217
  issue: "Could not read thread pairs, so the session's Fusion role is unknown. Refusing to issue an MCP credential that would carry no role.",
135658
136218
  cause
135659
136219
  }))) : yield* Effect.logWarning("No projection service, so an MCP credential is issued with no Fusion role. A pair whose event this process never saw will be ungated.", { threadId }).pipe(Effect.as(void 0));
135660
- const threadTitle = Option.isSome(projectionQuery) ? yield* projectionQuery.value.getThreadShellById(threadId).pipe(Effect.map((thread) => Option.isSome(thread) ? thread.value.title : void 0), Effect.catch(() => Effect.succeed(void 0))) : void 0;
136220
+ const shell = Option.isSome(projectionQuery) ? yield* projectionQuery.value.getThreadShellById(threadId).pipe(Effect.map(Option.getOrUndefined), Effect.catch(() => Effect.succeed(void 0))) : void 0;
136221
+ const threadTitle = shell?.title;
135661
136222
  const credential = yield* issueActiveMcpCredential({
135662
136223
  threadId,
135663
136224
  ...threadTitle ? { threadTitle } : {},
@@ -135666,7 +136227,8 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
135666
136227
  computerUseAccess,
135667
136228
  ...watchThreadIds !== void 0 ? { watchThreadIds } : {},
135668
136229
  ...adviseThreadIds !== void 0 ? { adviseThreadIds } : {},
135669
- ...fusionRole !== void 0 ? { fusionRole } : {}
136230
+ ...fusionRole !== void 0 ? { fusionRole } : {},
136231
+ ...shell?.parentThreadId != null ? { spawned: true } : {}
135670
136232
  });
135671
136233
  if (credential) yield* Effect.sync(() => setMcpProviderSession(credential.config));
135672
136234
  return credential;
@@ -138151,6 +138713,196 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
138151
138713
  })
138152
138714
  });
138153
138715
  //#endregion
138716
+ //#region ../../packages/shared/src/threadPendingWork.ts
138717
+ /**
138718
+ * A user message the server has not started a turn for yet, within the window
138719
+ * where adoption is still plausible. Mirrors `hasQueuedTurnStart` in
138720
+ * client-runtime, which owns the same window for settle/snooze decisions.
138721
+ */
138722
+ const QUEUED_TURN_START_GRACE_MS = 120 * 1e3;
138723
+ function hasQueuedTurn(input, nowMs) {
138724
+ if (input.latestUserMessageAt == null) return false;
138725
+ if (input.session?.status === "error") return false;
138726
+ const messageAtMs = Date.parse(input.latestUserMessageAt);
138727
+ if (Number.isNaN(messageAtMs)) return false;
138728
+ if (Math.abs(nowMs - messageAtMs) > QUEUED_TURN_START_GRACE_MS) return false;
138729
+ const turn = input.latestTurn;
138730
+ if (turn === null) return true;
138731
+ return [
138732
+ turn.requestedAt,
138733
+ turn.startedAt,
138734
+ turn.completedAt
138735
+ ].every((candidate) => candidate == null || Date.parse(candidate) < messageAtMs);
138736
+ }
138737
+ /**
138738
+ * What the thread is waiting on, or null when it is genuinely idle. A running
138739
+ * session answers null: the agent is working, which callers already show.
138740
+ */
138741
+ function resolveThreadPendingWork(input, options) {
138742
+ const nowMs = Date.parse(options.now);
138743
+ if (Number.isNaN(nowMs)) return null;
138744
+ if (input.session?.status === "starting" || input.session?.status === "running") return null;
138745
+ if (hasQueuedTurn(input, nowMs)) return "queued";
138746
+ if (input.session === null) return null;
138747
+ if (input.hasBackgroundTasks === true) return "background";
138748
+ if (input.scheduledWakeAt != null) {
138749
+ const wakeAtMs = Date.parse(input.scheduledWakeAt);
138750
+ if (!Number.isNaN(wakeAtMs) && wakeAtMs > nowMs) return "scheduled";
138751
+ }
138752
+ return null;
138753
+ }
138754
+ //#endregion
138755
+ //#region src/mcp/toolkits/threads/subThreadState.ts
138756
+ /**
138757
+ * Where a sub-thread stands, for a parent deciding what to do next. A
138758
+ * question or approval outranks running: the session keeps reporting running
138759
+ * while it waits, but nothing moves until someone answers.
138760
+ */
138761
+ function resolveSubThreadState(shell, now) {
138762
+ if (shell === void 0) return "missing";
138763
+ if (shell.archivedAt !== null) return "completed";
138764
+ if (shell.hasPendingApprovals || shell.hasPendingUserInput) return "needs-input";
138765
+ const status = shell.session?.status;
138766
+ if (status === "running" || status === "starting") return "running";
138767
+ if (resolveThreadPendingWork(shell, { now }) !== null) return "running";
138768
+ if (status === "error" || shell.latestTurn?.state === "error") return "failed";
138769
+ return "completed";
138770
+ }
138771
+ /** Whether a wait in `mode` is over for these states. */
138772
+ function isSubThreadWaitSatisfied(states, mode) {
138773
+ const stopped = (state) => state !== "running";
138774
+ return mode === "any" ? states.some(stopped) : states.every(stopped);
138775
+ }
138776
+ //#endregion
138777
+ //#region src/orchestration/SubThreadWorkspace.ts
138778
+ /**
138779
+ * Git steps for sub-threads: a child gets its own worktree branched from the
138780
+ * parent's current HEAD, and lands by merging back into the parent's worktree.
138781
+ */
138782
+ const repositoryLocks = /* @__PURE__ */ new Map();
138783
+ const withRepositoryLock = (repositoryRoot) => {
138784
+ let lock = repositoryLocks.get(repositoryRoot);
138785
+ if (lock === void 0) {
138786
+ lock = Semaphore.makeUnsafe(1);
138787
+ repositoryLocks.set(repositoryRoot, lock);
138788
+ }
138789
+ return lock.withPermits(1);
138790
+ };
138791
+ const runGit = (operation, cwd, args) => Effect.gen(function* () {
138792
+ return yield* (yield* GitVcsDriver).execute({
138793
+ operation: `SubThreadWorkspace.${operation}`,
138794
+ cwd,
138795
+ args,
138796
+ allowNonZeroExit: true
138797
+ });
138798
+ });
138799
+ /** The branch checked out at `cwd`, or null on a detached HEAD. */
138800
+ const currentBranch = (cwd) => runGit("currentBranch", cwd, [
138801
+ "symbolic-ref",
138802
+ "--quiet",
138803
+ "--short",
138804
+ "HEAD"
138805
+ ]).pipe(Effect.map((result) => result.exitCode === 0 ? result.stdout.trim() || null : null));
138806
+ /** Whether `cwd` has uncommitted changes, untracked files included. */
138807
+ const hasUncommittedChanges = (cwd) => runGit("status", cwd, ["status", "--porcelain"]).pipe(Effect.map((result) => result.stdout.trim().length > 0));
138808
+ const headCommit = (cwd) => runGit("headCommit", cwd, [
138809
+ "rev-parse",
138810
+ "--verify",
138811
+ "HEAD^{commit}"
138812
+ ]).pipe(Effect.map((result) => result.exitCode === 0 ? result.stdout.trim() : null));
138813
+ /** Creates the child's worktree on a new branch at the parent's HEAD commit. */
138814
+ const provisionChildWorktree = Effect.fn("SubThreadWorkspace.provisionChildWorktree")(function* (input) {
138815
+ const gitWorkflow = yield* GitWorkflowService;
138816
+ return yield* Effect.gen(function* () {
138817
+ const baseCommitSha = yield* headCommit(input.parentCwd);
138818
+ if (baseCommitSha === null) return null;
138819
+ const parentBranch = yield* currentBranch(input.parentCwd);
138820
+ const parentDirty = yield* hasUncommittedChanges(input.parentCwd);
138821
+ const created = yield* gitWorkflow.createWorktree({
138822
+ cwd: input.repositoryRoot,
138823
+ refName: baseCommitSha,
138824
+ newRefName: input.branch,
138825
+ ...parentBranch !== null ? { baseRefName: parentBranch } : {},
138826
+ path: null
138827
+ });
138828
+ return {
138829
+ worktreePath: created.worktree.path,
138830
+ branch: created.worktree.refName,
138831
+ baseCommitSha,
138832
+ parentBranch,
138833
+ parentDirty
138834
+ };
138835
+ }).pipe(withRepositoryLock(input.repositoryRoot));
138836
+ });
138837
+ /** Undoes `provisionChildWorktree` when the thread it was made for never appeared. */
138838
+ const discardChildWorktree = Effect.fn("SubThreadWorkspace.discardChildWorktree")(function* (input) {
138839
+ yield* (yield* GitWorkflowService).removeWorktree({
138840
+ cwd: input.repositoryRoot,
138841
+ path: input.worktreePath,
138842
+ force: true
138843
+ }).pipe(Effect.ignore);
138844
+ yield* runGit("deleteBranch", input.repositoryRoot, [
138845
+ "branch",
138846
+ "-D",
138847
+ input.branch
138848
+ ]).pipe(Effect.ignore);
138849
+ });
138850
+ /**
138851
+ * Merges the child's branch into the parent's worktree. A conflict is aborted
138852
+ * so the parent stays clean, and the conflicted files come back for the child
138853
+ * to resolve on its own branch.
138854
+ */
138855
+ const mergeChildIntoParent = Effect.fn("SubThreadWorkspace.mergeChildIntoParent")(function* (input) {
138856
+ const childBranch = yield* currentBranch(input.childCwd);
138857
+ if (childBranch === null) return {
138858
+ outcome: "refused",
138859
+ detail: "The child worktree is on a detached HEAD."
138860
+ };
138861
+ if (yield* hasUncommittedChanges(input.childCwd)) return {
138862
+ outcome: "refused",
138863
+ detail: `The child has uncommitted changes on '${childBranch}'. Ask it to commit first.`
138864
+ };
138865
+ if (yield* hasUncommittedChanges(input.parentCwd)) return {
138866
+ outcome: "refused",
138867
+ detail: "The parent worktree has uncommitted changes. Commit or stash them before merging."
138868
+ };
138869
+ return yield* Effect.gen(function* () {
138870
+ const parentBranch = yield* currentBranch(input.parentCwd);
138871
+ const merge = yield* runGit("merge", input.parentCwd, [
138872
+ "merge",
138873
+ "--no-ff",
138874
+ "--no-edit",
138875
+ `refs/heads/${childBranch}`
138876
+ ]);
138877
+ if (merge.exitCode === 0) {
138878
+ const mergeCommitSha = (yield* headCommit(input.parentCwd)) ?? "";
138879
+ return {
138880
+ outcome: /already up[ -]to[ -]date/i.test(merge.stdout) ? "up-to-date" : "merged",
138881
+ childBranch,
138882
+ parentBranch,
138883
+ mergeCommitSha
138884
+ };
138885
+ }
138886
+ const conflicted = yield* runGit("conflictedFiles", input.parentCwd, [
138887
+ "diff",
138888
+ "--name-only",
138889
+ "--diff-filter=U"
138890
+ ]);
138891
+ yield* runGit("abortMerge", input.parentCwd, ["merge", "--abort"]);
138892
+ const conflictedFiles = conflicted.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
138893
+ if (conflictedFiles.length === 0) return {
138894
+ outcome: "refused",
138895
+ detail: `git merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
138896
+ };
138897
+ return {
138898
+ outcome: "conflicted",
138899
+ childBranch,
138900
+ parentBranch,
138901
+ conflictedFiles
138902
+ };
138903
+ }).pipe(withRepositoryLock(input.repositoryRoot));
138904
+ });
138905
+ //#endregion
138154
138906
  //#region src/orchestration/fusionEvidenceReport.ts
138155
138907
  /**
138156
138908
  * Shared shape of one Fusion review's evidence rows.
@@ -138581,7 +139333,7 @@ const AskUserQuestionTool = Tool.make("ask_user_question", {
138581
139333
  ]
138582
139334
  }).annotate(Tool.Title, "Ask user question").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
138583
139335
  const ThreadSpawnTool = Tool.make("thread_spawn", {
138584
- description: "Start a new agent thread and send it a first message, then return its id. Use it to hand a piece of work to a fresh thread - a subtask you just planned, a job that wants its own transcript. For activation here or a new Fusion in this chat, use thread_pair_create with no watcherThreadId so this chat becomes Supervisor and its new Builder starts empty. Set fusion true only for an explicitly separate pair to create two new threads and send prompt to its supervisor, which plans and directs the builder later, even when this thread already belongs to a pair. Set fusionWatcher true only for legacy current-chat-as-builder pairing. Both use saved Fusion role models unless overridden. Act on the user's request; no special command syntax is required. The new thread inherits this one's project, model, permission mode, interaction mode, compression and subagent policy, and every one of those can be set here instead - what you set is already in force for the first turn, so a thread never has to be corrected after it starts. Set modelSelection to run it on another model: give the model id, and leave instanceId out to keep this thread's provider instance. Once started it can be watched with thread_watch_events and changed later with thread_configure. A thread that was itself started this way cannot start another.",
139336
+ description: "Start a new agent thread and send it a first message, then return its id. Use it to hand a piece of work to a fresh thread - a subtask you just planned, a job that wants its own transcript. For activation here or a new Fusion in this chat, use thread_pair_create with no watcherThreadId so this chat becomes Supervisor and its new Builder starts empty. Set fusion true only for an explicitly separate pair to create two new threads and send prompt to its supervisor, which plans and directs the builder later, even when this thread already belongs to a pair. Set fusionWatcher true only for legacy current-chat-as-builder pairing. Both use saved Fusion role models unless overridden. Act on the user's request; no special command syntax is required. The new thread inherits this one's project, model, permission mode, interaction mode, compression and subagent policy, and every one of those can be set here instead - what you set is already in force for the first turn, so a thread never has to be corrected after it starts. Set modelSelection to run it on another model: give the model id, and leave instanceId out to keep this thread's provider instance. Once started it can be watched with thread_watch_events and changed later with thread_configure. A thread that was itself started this way cannot start another. A plain spawn is a sub-thread: it gets its own git worktree and branch from this thread's current HEAD commit (commit first), so several can work in parallel; wait on them with thread_wait, follow up with thread_message, land each with thread_merge_child, and remove it with thread_cleanup.",
138585
139337
  parameters: ThreadSpawnInput,
138586
139338
  success: ThreadSpawnResult,
138587
139339
  failure: ThreadControlToolError,
@@ -138590,8 +139342,11 @@ const ThreadSpawnTool = Tool.make("thread_spawn", {
138590
139342
  McpSessionRegistry,
138591
139343
  OrchestrationEngineService,
138592
139344
  ProjectionThreadRepository,
139345
+ ProjectionSnapshotQuery,
138593
139346
  ServerSettingsService,
138594
139347
  ProviderRegistry,
139348
+ GitWorkflowService,
139349
+ GitVcsDriver,
138595
139350
  Crypto.Crypto
138596
139351
  ]
138597
139352
  }).annotate(Tool.Title, "Start a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
@@ -138648,6 +139403,7 @@ const ThreadCleanupTool = Tool.make("thread_cleanup", {
138648
139403
  ProjectionThreadRepository,
138649
139404
  ProjectionThreadSessionRepository,
138650
139405
  ThreadWorkspaceLifecycleService,
139406
+ TerminalManager,
138651
139407
  Crypto.Crypto
138652
139408
  ]
138653
139409
  }).annotate(Tool.Title, "Archive and clean up thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, true);
@@ -138736,7 +139492,44 @@ const ThreadContextUpdateTool = Tool.make("thread_context_update", {
138736
139492
  Crypto.Crypto
138737
139493
  ]
138738
139494
  }).annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
138739
- const ThreadToolkit = Toolkit.make(ThreadContextReadTool, ThreadContextUpdateTool, ThreadPlanReadTool, ThreadPlanUpdateTool, AskUserQuestionTool, ThreadSpawnTool, ThreadPairCreateTool, ThreadPairDetachTool, ThreadConfigureTool, ThreadCleanupTool, ThreadSnoozeTool, ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
139495
+ const ThreadWaitTool = Tool.make("thread_wait", {
139496
+ description: "Wait for sub-threads you spawned to stop working, then return each one's state (running, needs-input, completed, failed, missing), branch, worktree and latest reply. mode 'all' (default) waits for every one; 'any' returns at the first, so you can merge or answer it while the others continue. Returns timedOut true after timeoutSeconds (default 45) with the current states; call again to keep waiting. Use this instead of polling thread_watch_events.",
139497
+ parameters: ThreadWaitInput,
139498
+ success: ThreadWaitResult,
139499
+ failure: ThreadControlToolError,
139500
+ dependencies: [
139501
+ McpInvocationContext,
139502
+ OrchestrationEngineService,
139503
+ ProjectionSnapshotQuery,
139504
+ ThreadEventStreamService,
139505
+ ProjectionThreadMessageRepository
139506
+ ]
139507
+ }).annotate(Tool.Title, "Wait for sub-threads").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
139508
+ const ThreadMessageTool = Tool.make("thread_message", {
139509
+ description: "Send a follow-up message to a sub-thread you spawned: a correction, an answer, or a request to resolve merge conflicts. If it is still working, the message waits until its current turn ends.",
139510
+ parameters: ThreadMessageInput,
139511
+ success: ThreadMessageResult,
139512
+ failure: ThreadControlToolError,
139513
+ dependencies: [
139514
+ McpInvocationContext,
139515
+ OrchestrationEngineService,
139516
+ ProjectionSnapshotQuery,
139517
+ Crypto.Crypto
139518
+ ]
139519
+ }).annotate(Tool.Title, "Message a sub-thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
139520
+ const ThreadMergeChildTool = Tool.make("thread_merge_child", {
139521
+ description: "Merge a finished sub-thread's branch into this thread's worktree with a merge commit. Both worktrees must be committed and the sub-thread must have stopped. On conflict the merge is aborted, this worktree is left unchanged, and conflictedFiles lists the files: send them to the sub-thread with thread_message (ask it to merge this thread's branch, resolve, and commit), wait, then merge again. After landing, remove the sub-thread with thread_cleanup.",
139522
+ parameters: ThreadMergeChildInput,
139523
+ success: ThreadMergeChildResult,
139524
+ failure: ThreadControlToolError,
139525
+ dependencies: [
139526
+ McpInvocationContext,
139527
+ ProjectionSnapshotQuery,
139528
+ GitWorkflowService,
139529
+ GitVcsDriver
139530
+ ]
139531
+ }).annotate(Tool.Title, "Merge a sub-thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
139532
+ const ThreadToolkit = Toolkit.make(ThreadContextReadTool, ThreadContextUpdateTool, ThreadPlanReadTool, ThreadPlanUpdateTool, AskUserQuestionTool, ThreadSpawnTool, ThreadWaitTool, ThreadMessageTool, ThreadMergeChildTool, ThreadPairCreateTool, ThreadPairDetachTool, ThreadConfigureTool, ThreadCleanupTool, ThreadSnoozeTool, ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
138740
139533
  //#endregion
138741
139534
  //#region src/orchestration/pendingMcpUserInputs.ts
138742
139535
  /**
@@ -138830,6 +139623,15 @@ const dispatchControl = Effect.fn("mcp.threads.dispatchControl")(function* (comm
138830
139623
  detail: cause.message
138831
139624
  })));
138832
139625
  });
139626
+ /** Events that can change a sub-thread's wait state; streaming deltas cannot. */
139627
+ const SUB_THREAD_WAKE_EVENTS = /* @__PURE__ */ new Set([
139628
+ "thread.session-set",
139629
+ "thread.turn-completed",
139630
+ "thread.turn-start-requested",
139631
+ "thread.activity-appended",
139632
+ "thread.archived",
139633
+ "thread.deleted"
139634
+ ]);
138833
139635
  const newCommandId = Effect.gen(function* () {
138834
139636
  const crypto = yield* Crypto.Crypto;
138835
139637
  return CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
@@ -139146,6 +139948,39 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139146
139948
  const unpromptedSubagents = input.unpromptedSubagents ?? template.unpromptedSubagents === 1;
139147
139949
  const threadId = ThreadId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
139148
139950
  const createdAt = DateTime.formatIso(yield* DateTime.now);
139951
+ const isFusionSpawn = input.fusion === true || input.fusionWatcher === true;
139952
+ if (isFusionSpawn && input.workspace === "worktree") return yield* new ThreadSpawnNotPermittedError({
139953
+ threadId: invocation.threadId,
139954
+ detail: "Fusion spawns share this thread's checkout; omit workspace or pass 'shared'."
139955
+ });
139956
+ let childWorkspace = null;
139957
+ if (!isFusionSpawn && input.workspace !== "shared" && !isChatProject(projectId)) {
139958
+ const project = yield* (yield* ProjectionSnapshotQuery).getProjectShellById(projectId).pipe(Effect.map(Option.getOrUndefined), Effect.mapError((cause) => new ThreadSpawnNotPermittedError({
139959
+ threadId: invocation.threadId,
139960
+ detail: `the project could not be read to create a worktree: ${cause.message}`
139961
+ })));
139962
+ if (project !== void 0) {
139963
+ const repositoryRoot = project.workspaceRoot;
139964
+ const parentCwd = projectId === template.projectId ? template.worktreePath ?? repositoryRoot : repositoryRoot;
139965
+ const hex = (yield* crypto.randomUUIDv4.pipe(Effect.orDie)).replaceAll("-", "");
139966
+ const provisioned = yield* provisionChildWorktree({
139967
+ repositoryRoot,
139968
+ parentCwd,
139969
+ branch: buildTemporaryWorktreeBranchName((bytes) => hex.slice(0, bytes * 2))
139970
+ }).pipe(Effect.mapError((cause) => new ThreadSpawnNotPermittedError({
139971
+ threadId: invocation.threadId,
139972
+ detail: `the sub-thread's worktree could not be created: ${cause.message}`
139973
+ })));
139974
+ if (provisioned !== null) childWorkspace = {
139975
+ ...provisioned,
139976
+ repositoryRoot
139977
+ };
139978
+ }
139979
+ if (childWorkspace === null && input.workspace === "worktree") return yield* new ThreadSpawnNotPermittedError({
139980
+ threadId: invocation.threadId,
139981
+ detail: "the project is not a git repository with a commit, so it cannot have worktrees"
139982
+ });
139983
+ }
139149
139984
  yield* dispatchControl({
139150
139985
  type: "thread.create",
139151
139986
  commandId: yield* newCommandId,
@@ -139157,10 +139992,11 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139157
139992
  interactionMode,
139158
139993
  compressMode,
139159
139994
  unpromptedSubagents,
139160
- branch: null,
139161
- worktreePath: null,
139995
+ branch: childWorkspace?.branch ?? null,
139996
+ worktreePath: childWorkspace?.worktreePath ?? null,
139997
+ ...isFusionSpawn || projectId !== template.projectId ? {} : { parentThreadId: invocation.threadId },
139162
139998
  createdAt
139163
- }, threadId);
139999
+ }, threadId).pipe(Effect.tapError(() => childWorkspace === null ? Effect.void : discardChildWorktree(childWorkspace)));
139164
140000
  yield* registry.recordSpawnedThread({
139165
140001
  providerSessionId: invocation.providerSessionId,
139166
140002
  threadId,
@@ -139229,7 +140065,13 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139229
140065
  threadId,
139230
140066
  projectId,
139231
140067
  title: input.title,
139232
- ...fusionPair
140068
+ ...fusionPair,
140069
+ ...childWorkspace === null ? {} : {
140070
+ branch: childWorkspace.branch,
140071
+ worktreePath: childWorkspace.worktreePath,
140072
+ baseCommitSha: childWorkspace.baseCommitSha,
140073
+ parentDirty: childWorkspace.parentDirty
140074
+ }
139233
140075
  };
139234
140076
  }),
139235
140077
  thread_pair_create: (input) => Effect.gen(function* () {
@@ -139486,6 +140328,11 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139486
140328
  })));
139487
140329
  const archivedThreadIds = result.events?.filter((event) => event.type === "thread.archived").map((event) => event.payload.threadId) ?? [];
139488
140330
  archivedThreadIds.forEach(queueThreadWorkspaceCleanup);
140331
+ const terminals = yield* TerminalManager;
140332
+ yield* Effect.forEach(archivedThreadIds, (archivedThreadId) => terminals.close({ threadId: archivedThreadId }).pipe(Effect.catch((error) => Effect.logWarning("failed to close thread terminals after archive", {
140333
+ threadId: archivedThreadId,
140334
+ error: error.message
140335
+ }))), { discard: true });
139489
140336
  const stopRequested = new Set(result.events?.filter((event) => event.type === "thread.session-stop-requested").map((event) => event.payload.threadId) ?? []);
139490
140337
  const session = yield* sessions.getByThreadId({ threadId }).pipe(Effect.mapError((cause) => new ThreadControlRejectedError({
139491
140338
  threadId,
@@ -139648,6 +140495,126 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139648
140495
  estimatedTokensAfter: estimateTokens(compressed),
139649
140496
  warnings: validation.warnings
139650
140497
  };
140498
+ }),
140499
+ thread_wait: (input) => Effect.gen(function* () {
140500
+ const invocation = yield* requireThreadCapability();
140501
+ const threadIds = [...new Set(input.threadIds)];
140502
+ for (const threadId of threadIds) yield* requireThreadControlTarget(threadId);
140503
+ const query = yield* ProjectionSnapshotQuery;
140504
+ const engine = yield* OrchestrationEngineService;
140505
+ const eventStream = yield* ThreadEventStreamService;
140506
+ const messages = yield* ProjectionThreadMessageRepository;
140507
+ const mode = input.mode ?? "all";
140508
+ const rejected = (detail) => new ThreadControlRejectedError({
140509
+ threadId: invocation.threadId,
140510
+ commandType: "thread_wait",
140511
+ detail
140512
+ });
140513
+ const check = Effect.gen(function* () {
140514
+ const shells = yield* Effect.forEach(threadIds, (threadId) => query.getThreadShellById(threadId).pipe(Effect.map(Option.getOrUndefined)));
140515
+ const now = DateTime.formatIso(yield* DateTime.now);
140516
+ const states = shells.map((shell) => resolveSubThreadState(shell, now));
140517
+ return {
140518
+ shells,
140519
+ states,
140520
+ done: isSubThreadWaitSatisfied(states, mode)
140521
+ };
140522
+ }).pipe(Effect.mapError((cause) => rejected(cause.message)));
140523
+ const head = yield* engine.latestSequence;
140524
+ const initial = yield* check;
140525
+ const settled = initial.done ? Option.some(initial) : yield* Stream.mergeAll(threadIds.map((threadId) => eventStream.watch({
140526
+ threadId,
140527
+ afterSequence: head
140528
+ })), { concurrency: "unbounded" }).pipe(Stream.filter((item) => item.kind === "event" && SUB_THREAD_WAKE_EVENTS.has(item.event.type)), Stream.mapEffect(() => check), Stream.filter((result) => result.done), Stream.runHead, Effect.mapError((cause) => rejected(cause.message)), Effect.timeoutOption(Duration.seconds(input.timeoutSeconds ?? 45)), Effect.map(Option.flatten));
140529
+ const final = Option.isSome(settled) ? settled.value : yield* check;
140530
+ const children = yield* Effect.forEach(threadIds, (threadId, index) => Effect.gen(function* () {
140531
+ const shell = final.shells[index];
140532
+ const assistantMessageId = shell?.latestTurn?.assistantMessageId ?? null;
140533
+ const reply = assistantMessageId === null ? null : yield* messages.getByMessageId({ messageId: assistantMessageId }).pipe(Effect.map((row) => Option.isSome(row) ? row.value.text : null), Effect.catch(() => Effect.succeed(null)));
140534
+ return {
140535
+ threadId,
140536
+ title: shell?.title ?? null,
140537
+ state: final.states[index] ?? "missing",
140538
+ branch: shell?.branch ?? null,
140539
+ worktreePath: shell?.worktreePath ?? null,
140540
+ lastAssistantMessage: reply === null ? null : reply.length > 1500 ? `${reply.slice(0, 1500)}…` : reply,
140541
+ lastError: shell?.session?.lastError ?? null
140542
+ };
140543
+ }));
140544
+ return {
140545
+ timedOut: Option.isNone(settled),
140546
+ children
140547
+ };
140548
+ }),
140549
+ thread_message: (input) => Effect.gen(function* () {
140550
+ const { invocation, threadId } = yield* requireThreadControlTarget(input.threadId);
140551
+ const rejected = (detail) => new ThreadControlRejectedError({
140552
+ threadId,
140553
+ commandType: "thread_message",
140554
+ detail
140555
+ });
140556
+ if (threadId === invocation.threadId) return yield* rejected("Message a sub-thread, not this thread.");
140557
+ const shell = yield* (yield* ProjectionSnapshotQuery).getThreadShellById(threadId).pipe(Effect.map(Option.getOrUndefined), Effect.mapError((cause) => rejected(cause.message)));
140558
+ if (shell === void 0) return yield* rejected("The sub-thread is archived or no longer exists.");
140559
+ const crypto = yield* Crypto.Crypto;
140560
+ const messageId = MessageId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
140561
+ yield* dispatchControl({
140562
+ type: "thread.turn.start",
140563
+ commandId: yield* newCommandId,
140564
+ threadId,
140565
+ message: {
140566
+ messageId,
140567
+ role: "user",
140568
+ text: input.text,
140569
+ attachments: []
140570
+ },
140571
+ modelSelection: shell.modelSelection,
140572
+ runtimeMode: shell.runtimeMode,
140573
+ interactionMode: shell.interactionMode,
140574
+ compressMode: shell.compressMode,
140575
+ unpromptedSubagents: shell.unpromptedSubagents,
140576
+ queueIfBusy: true,
140577
+ createdAt: DateTime.formatIso(yield* DateTime.now)
140578
+ }, threadId);
140579
+ return {
140580
+ threadId,
140581
+ messageId
140582
+ };
140583
+ }),
140584
+ thread_merge_child: (input) => Effect.gen(function* () {
140585
+ const { invocation, threadId } = yield* requireThreadControlTarget(input.threadId);
140586
+ const refused = (detail) => Effect.succeed({
140587
+ threadId,
140588
+ outcome: "refused",
140589
+ detail
140590
+ });
140591
+ const query = yield* ProjectionSnapshotQuery;
140592
+ const readShell = (id) => query.getThreadShellById(id).pipe(Effect.map(Option.getOrUndefined), Effect.mapError((cause) => new ThreadControlRejectedError({
140593
+ threadId,
140594
+ commandType: "thread_merge_child",
140595
+ detail: cause.message
140596
+ })));
140597
+ const child = yield* readShell(threadId);
140598
+ const parent = yield* readShell(invocation.threadId);
140599
+ if (child === void 0 || parent === void 0) return yield* refused("The sub-thread or this thread is archived or no longer exists.");
140600
+ if (child.parentThreadId !== invocation.threadId) return yield* refused("Only a sub-thread this thread spawned can be merged into it.");
140601
+ if (child.worktreePath === null) return yield* refused("The sub-thread shares this checkout, so there is nothing to merge.");
140602
+ const status = child.session?.status;
140603
+ if (status === "running" || status === "starting") return yield* refused("The sub-thread is still working. Wait for it with thread_wait.");
140604
+ const project = yield* query.getProjectShellById(child.projectId).pipe(Effect.map(Option.getOrUndefined), Effect.catch(() => Effect.succeed(void 0)));
140605
+ if (project === void 0) return yield* refused("The project could not be read.");
140606
+ return {
140607
+ threadId,
140608
+ ...yield* mergeChildIntoParent({
140609
+ repositoryRoot: project.workspaceRoot,
140610
+ parentCwd: parent.worktreePath ?? project.workspaceRoot,
140611
+ childCwd: child.worktreePath
140612
+ }).pipe(Effect.mapError((cause) => new ThreadControlRejectedError({
140613
+ threadId,
140614
+ commandType: "thread_merge_child",
140615
+ detail: cause.message
140616
+ })))
140617
+ };
139651
140618
  })
139652
140619
  });
139653
140620
  //#endregion