@p4code/cli 0.6.54 → 0.7.0

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
@@ -52,12 +52,12 @@ import * as SqlClient from "effect/unstable/sql/SqlClient";
52
52
  import * as SqlSchema from "effect/unstable/sql/SqlSchema";
53
53
  import * as PlatformError from "effect/PlatformError";
54
54
  import * as Migrator from "effect/unstable/sql/Migrator";
55
+ import * as Clock from "effect/Clock";
55
56
  import * as NodeUtil from "node:util";
56
57
  import * as Cause from "effect/Cause";
57
58
  import * as Exit from "effect/Exit";
58
59
  import * as NodeReadline from "node:readline";
59
60
  import * as NodeFSP from "node:fs/promises";
60
- import * as Clock from "effect/Clock";
61
61
  import * as Headers$1 from "effect/unstable/http/Headers";
62
62
  import * as HttpRouter$1 from "effect/unstable/http/HttpRouter";
63
63
  import * as NodeStream from "@effect/platform-node/NodeStream";
@@ -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.54";
266
+ var version$1 = "0.7.0";
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,54 @@ 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
18810
+ //#region src/persistence/Migrations/066_OrchestrationEventTypeSequenceIndex.ts
18811
+ var _066_OrchestrationEventTypeSequenceIndex_default = Effect.gen(function* () {
18812
+ yield* (yield* SqlClient.SqlClient)`
18813
+ CREATE INDEX IF NOT EXISTS idx_orchestration_events_type_sequence
18814
+ ON orchestration_events(event_type, sequence)
18815
+ `;
18816
+ });
18817
+ //#endregion
18635
18818
  //#region src/persistence/Migrations/001_OrchestrationEvents.ts
18636
18819
  var _001_OrchestrationEvents_default = Effect.gen(function* () {
18637
18820
  const sql = yield* SqlClient.SqlClient;
@@ -20828,6 +21011,21 @@ const migrationEntries = [
20828
21011
  63,
20829
21012
  "RemoveThreadSettlement",
20830
21013
  _063_RemoveThreadSettlement_default
21014
+ ],
21015
+ [
21016
+ 64,
21017
+ "ProjectionThreadQueuedMessages",
21018
+ _064_ProjectionThreadQueuedMessages_default
21019
+ ],
21020
+ [
21021
+ 65,
21022
+ "ProjectionThreadsParentThread",
21023
+ _065_ProjectionThreadsParentThread_default
21024
+ ],
21025
+ [
21026
+ 66,
21027
+ "OrchestrationEventTypeSequenceIndex",
21028
+ _066_OrchestrationEventTypeSequenceIndex_default
20831
21029
  ]
20832
21030
  ];
20833
21031
  const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
@@ -20889,14 +21087,25 @@ const runGuardedMigrations = Effect.fn("runGuardedMigrations")(function* (input)
20889
21087
  appliedMigrationId: appliedId,
20890
21088
  targetMigrationId: targetId
20891
21089
  }));
21090
+ const snapshotStartedAt = yield* Clock.currentTimeMillis;
20892
21091
  yield* captureDatabaseSnapshot(input.dbPath).pipe(Effect.tapCause((cause) => Effect.logError("Refusing to migrate without a database snapshot").pipe(Effect.annotateLogs({
20893
21092
  dbPath: input.dbPath,
20894
21093
  cause
20895
21094
  }))));
21095
+ yield* Effect.logInfo("database migration snapshot completed", {
21096
+ durationMs: (yield* Clock.currentTimeMillis) - snapshotStartedAt,
21097
+ appliedMigrationId: appliedId,
21098
+ targetMigrationId: targetId
21099
+ });
21100
+ const migrationStartedAt = yield* Clock.currentTimeMillis;
20896
21101
  const executed = yield* runMigrations(input.options).pipe(Effect.tapCause(() => markDatabaseRestorePending(input.dbPath).pipe(Effect.andThen(Effect.logError("Migration failed; the database will be restored on the next start").pipe(Effect.annotateLogs({
20897
21102
  appliedMigrationId: appliedId,
20898
21103
  targetMigrationId: targetId
20899
21104
  }))), Effect.tapError((cause) => Effect.logError("Could not mark the database for restore").pipe(Effect.annotateLogs({ cause }))), Effect.ignore)));
21105
+ yield* Effect.logInfo("database migrations completed", {
21106
+ durationMs: (yield* Clock.currentTimeMillis) - migrationStartedAt,
21107
+ executed: executed.length
21108
+ });
20900
21109
  yield* discardSnapshotQuietly(input.dbPath);
20901
21110
  return executed;
20902
21111
  });
@@ -36027,15 +36236,18 @@ const OrchestrationEventPersistedRowSchema = Schema$1.Struct({
36027
36236
  });
36028
36237
  const ReadFromSequenceRequestSchema = Schema$1.Struct({
36029
36238
  sequenceExclusive: NonNegativeInt,
36239
+ throughSequence: NonNegativeInt,
36030
36240
  limit: Schema$1.Number
36031
36241
  });
36032
36242
  const ReadFromSequenceOfTypesRequestSchema = Schema$1.Struct({
36033
36243
  sequenceExclusive: NonNegativeInt,
36244
+ throughSequence: NonNegativeInt,
36034
36245
  limit: Schema$1.Number,
36035
36246
  eventTypes: Schema$1.Array(OrchestrationEventType)
36036
36247
  });
36037
36248
  const ReadFromSequenceOfThreadRequestSchema = Schema$1.Struct({
36038
36249
  sequenceExclusive: NonNegativeInt,
36250
+ throughSequence: NonNegativeInt,
36039
36251
  limit: Schema$1.Number,
36040
36252
  threadId: Schema$1.String
36041
36253
  });
@@ -36155,6 +36367,7 @@ const makeEventStore = Effect.gen(function* () {
36155
36367
  metadata_json AS "metadata"
36156
36368
  FROM orchestration_events
36157
36369
  WHERE sequence > ${request.sequenceExclusive}
36370
+ AND sequence <= ${request.throughSequence}
36158
36371
  ORDER BY sequence ASC
36159
36372
  LIMIT ${request.limit}
36160
36373
  `
@@ -36176,10 +36389,21 @@ const makeEventStore = Effect.gen(function* () {
36176
36389
  payload_json AS "payload",
36177
36390
  metadata_json AS "metadata"
36178
36391
  FROM orchestration_events
36179
- WHERE sequence > ${request.sequenceExclusive}
36180
- AND ${sql.in("event_type", request.eventTypes)}
36392
+ WHERE sequence IN (
36393
+ SELECT sequence FROM (
36394
+ ${sql.join(" UNION ALL ")(request.eventTypes.map((eventType) => sql`
36395
+ SELECT sequence FROM (
36396
+ SELECT sequence FROM orchestration_events
36397
+ WHERE event_type = ${eventType}
36398
+ AND sequence > ${request.sequenceExclusive}
36399
+ AND sequence <= ${request.throughSequence}
36400
+ ORDER BY sequence ASC LIMIT ${request.limit}
36401
+ )
36402
+ `))}
36403
+ )
36404
+ ORDER BY sequence ASC LIMIT ${request.limit}
36405
+ )
36181
36406
  ORDER BY sequence ASC
36182
- LIMIT ${request.limit}
36183
36407
  `
36184
36408
  });
36185
36409
  const readEventRowsFromSequenceOfThread = SqlSchema.findAll({
@@ -36202,6 +36426,7 @@ const makeEventStore = Effect.gen(function* () {
36202
36426
  WHERE aggregate_kind = 'thread'
36203
36427
  AND stream_id = ${request.threadId}
36204
36428
  AND sequence > ${request.sequenceExclusive}
36429
+ AND sequence <= ${request.throughSequence}
36205
36430
  ORDER BY sequence ASC
36206
36431
  LIMIT ${request.limit}
36207
36432
  `
@@ -36264,6 +36489,7 @@ const makeEventStore = Effect.gen(function* () {
36264
36489
  });
36265
36490
  const readPage = (cursor, remaining) => Stream.fromEffect(readRows({
36266
36491
  sequenceExclusive: cursor,
36492
+ throughSequence: options?.throughSequence ?? Number.MAX_SAFE_INTEGER,
36267
36493
  limit: Math.min(remaining, READ_PAGE_SIZE)
36268
36494
  }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$4("OrchestrationEventStore.readFromSequence:query", "OrchestrationEventStore.readFromSequence:decodeRows")), Effect.flatMap((rows) => Effect.forEach(rows, (row) => decodeEvent(row).pipe(Effect.mapError(toPersistenceDecodeError("OrchestrationEventStore.readFromSequence:rowToEvent"))))))).pipe(Stream.flatMap((events) => {
36269
36495
  if (events.length === 0) return Stream.empty;
@@ -37223,6 +37449,8 @@ const ThreadUnpinnedPayload = ThreadUnpinnedPayload$1;
37223
37449
  const ThreadSnoozedPayload = ThreadSnoozedPayload$1;
37224
37450
  const ThreadUnsnoozedPayload = ThreadUnsnoozedPayload$1;
37225
37451
  const MessageSentPayloadSchema = ThreadMessageSentPayload;
37452
+ const ThreadMessageQueuedPayload = ThreadMessageQueuedPayload$1;
37453
+ const ThreadQueuedMessageRemovedPayload = ThreadQueuedMessageRemovedPayload$1;
37226
37454
  const ThreadProposedPlanUpsertedPayload = ThreadProposedPlanUpsertedPayload$1;
37227
37455
  const ThreadScheduledTaskCreatedPayload = ThreadScheduledTaskCreatedPayload$1;
37228
37456
  const ThreadScheduledTaskCancelledPayload = ThreadScheduledTaskCancelledPayload$1;
@@ -37440,6 +37668,7 @@ function projectEvent(model, event) {
37440
37668
  computerUseHostId: payload.computerUseHostId ?? null,
37441
37669
  branch: payload.branch,
37442
37670
  worktreePath: payload.worktreePath,
37671
+ parentThreadId: payload.parentThreadId ?? null,
37443
37672
  latestTurn: null,
37444
37673
  createdAt: payload.createdAt,
37445
37674
  updatedAt: payload.updatedAt,
@@ -37641,6 +37870,24 @@ function projectEvent(model, event) {
37641
37870
  })
37642
37871
  };
37643
37872
  });
37873
+ case "thread.message-queued": return Effect.gen(function* () {
37874
+ const payload = yield* decodeForEvent(ThreadMessageQueuedPayload, event.payload, event.type, "payload");
37875
+ const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
37876
+ if (!thread) return nextBase;
37877
+ return {
37878
+ ...nextBase,
37879
+ threads: updateThread(nextBase.threads, payload.threadId, { queuedMessages: [...(thread.queuedMessages ?? []).filter((entry) => entry.messageId !== payload.message.messageId), payload.message] })
37880
+ };
37881
+ });
37882
+ case "thread.queued-message-removed": return Effect.gen(function* () {
37883
+ const payload = yield* decodeForEvent(ThreadQueuedMessageRemovedPayload, event.payload, event.type, "payload");
37884
+ const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
37885
+ if (!thread) return nextBase;
37886
+ return {
37887
+ ...nextBase,
37888
+ threads: updateThread(nextBase.threads, payload.threadId, { queuedMessages: (thread.queuedMessages ?? []).filter((entry) => entry.messageId !== payload.messageId) })
37889
+ };
37890
+ });
37644
37891
  case "thread.scheduled-task.created": return Effect.gen(function* () {
37645
37892
  const payload = yield* decodeForEvent(ThreadScheduledTaskCreatedPayload, event.payload, event.type, "payload");
37646
37893
  const thread = nextBase.threads.find((entry) => entry.id === payload.threadId);
@@ -37788,7 +38035,7 @@ function archiveWorkspaceLifecycle(cleanup, updatedAt) {
37788
38035
  function findActiveThreadPair(readModel, threadId) {
37789
38036
  return (readModel.threadPairs ?? []).find((pair) => pair.detachedAt === null && (pair.implementerThreadId === threadId || pair.watcherThreadId === threadId));
37790
38037
  }
37791
- const QUEUED_TURN_START_GRACE_MS = 120 * 1e3;
38038
+ const QUEUED_TURN_START_GRACE_MS$1 = 120 * 1e3;
37792
38039
  /**
37793
38040
  * Blocked-on-you work derived from the thread's retained activities: an
37794
38041
  * approval or user-input request with no later resolution for the same
@@ -37842,7 +38089,16 @@ function threadHasQueuedTurnStart(thread, occurredAt) {
37842
38089
  thread.latestTurn.completedAt
37843
38090
  ].map((candidate) => candidate == null ? Number.NEGATIVE_INFINITY : Date.parse(candidate)));
37844
38091
  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;
38092
+ return thread.session?.status !== "error" && Number.isFinite(latestUserMessageAtMs) && latestUserMessageAtMs > latestTurnAtMs && Math.abs(queuedAgeMs) <= QUEUED_TURN_START_GRACE_MS$1;
38093
+ }
38094
+ /**
38095
+ * A thread is busy while a turn runs or one is about to: the session is
38096
+ * starting or running, or a sent message has not been adopted by a turn yet.
38097
+ * Queued messages wait for this to clear.
38098
+ */
38099
+ function isThreadBusy(thread, occurredAt) {
38100
+ const status = thread.session?.status;
38101
+ return status === "running" || status === "starting" || threadHasQueuedTurnStart(thread, occurredAt);
37846
38102
  }
37847
38103
  function withEventBase(input) {
37848
38104
  return Crypto.Crypto.pipe(Effect.flatMap((crypto) => crypto.randomUUIDv4.pipe(Effect.map((eventId) => ({
@@ -38017,6 +38273,15 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
38017
38273
  commandType: command.type,
38018
38274
  detail: `Sidechat parent '${sidechatParent.id}' belongs to a different project.`
38019
38275
  });
38276
+ const spawningParent = command.parentThreadId === void 0 ? null : yield* requireThreadNotArchived({
38277
+ readModel,
38278
+ command,
38279
+ threadId: command.parentThreadId
38280
+ });
38281
+ if (spawningParent && spawningParent.projectId !== command.projectId) return yield* new OrchestrationCommandInvariantError({
38282
+ commandType: command.type,
38283
+ detail: `Parent thread '${spawningParent.id}' belongs to a different project.`
38284
+ });
38020
38285
  return {
38021
38286
  ...yield* withEventBase({
38022
38287
  aggregateKind: "thread",
@@ -38037,6 +38302,7 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
38037
38302
  computerUseHostId: sidechatParent ? null : command.computerUseHostId ?? null,
38038
38303
  branch: sidechatParent ? sidechatParent.branch : command.branch,
38039
38304
  worktreePath: sidechatParent ? sidechatParent.worktreePath : command.worktreePath,
38305
+ ...spawningParent ? { parentThreadId: spawningParent.id } : {},
38040
38306
  createdAt: command.createdAt,
38041
38307
  updatedAt: command.createdAt
38042
38308
  }
@@ -39013,6 +39279,25 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
39013
39279
  commandType: command.type,
39014
39280
  detail: budgetBlock
39015
39281
  });
39282
+ if (command.queueIfBusy === true && isThreadBusy(targetThread, command.createdAt)) return {
39283
+ ...yield* withEventBase({
39284
+ aggregateKind: "thread",
39285
+ aggregateId: command.threadId,
39286
+ occurredAt: command.createdAt,
39287
+ commandId: command.commandId
39288
+ }),
39289
+ type: "thread.message-queued",
39290
+ payload: {
39291
+ threadId: command.threadId,
39292
+ message: {
39293
+ messageId: command.message.messageId,
39294
+ text: command.message.text,
39295
+ attachments: command.message.attachments,
39296
+ ...command.modelSelection !== void 0 ? { modelSelection: command.modelSelection } : {},
39297
+ createdAt: command.createdAt
39298
+ }
39299
+ }
39300
+ };
39016
39301
  if (activePair?.watcherThreadId === command.threadId && targetThread.interactionMode !== "default") return yield* decideCommandSequence({
39017
39302
  readModel,
39018
39303
  commands: [{
@@ -39101,6 +39386,56 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
39101
39386
  turnStartRequestedEvent
39102
39387
  ];
39103
39388
  }
39389
+ case "thread.queued-message.send":
39390
+ case "thread.queued-message.remove": {
39391
+ const thread = yield* requireThread({
39392
+ readModel,
39393
+ command,
39394
+ threadId: command.threadId
39395
+ });
39396
+ const queued = (thread.queuedMessages ?? []).find((entry) => entry.messageId === command.messageId);
39397
+ if (queued === void 0) return yield* new OrchestrationCommandInvariantError({
39398
+ commandType: command.type,
39399
+ detail: `Message '${command.messageId}' is not queued on thread '${command.threadId}'.`
39400
+ });
39401
+ const removedEvent = {
39402
+ ...yield* withEventBase({
39403
+ aggregateKind: "thread",
39404
+ aggregateId: command.threadId,
39405
+ occurredAt: command.createdAt,
39406
+ commandId: command.commandId
39407
+ }),
39408
+ type: "thread.queued-message-removed",
39409
+ payload: {
39410
+ threadId: command.threadId,
39411
+ messageId: command.messageId,
39412
+ reason: command.type === "thread.queued-message.send" ? "sent" : "removed",
39413
+ removedAt: command.createdAt
39414
+ }
39415
+ };
39416
+ if (command.type === "thread.queued-message.remove") return removedEvent;
39417
+ const turnStart = yield* decideCommand({
39418
+ readModel,
39419
+ command: {
39420
+ type: "thread.turn.start",
39421
+ commandId: command.commandId,
39422
+ threadId: command.threadId,
39423
+ message: {
39424
+ messageId: queued.messageId,
39425
+ role: "user",
39426
+ text: queued.text,
39427
+ attachments: queued.attachments
39428
+ },
39429
+ ...queued.modelSelection !== void 0 ? { modelSelection: queued.modelSelection } : {},
39430
+ runtimeMode: thread.runtimeMode,
39431
+ interactionMode: thread.interactionMode,
39432
+ compressMode: thread.compressMode,
39433
+ unpromptedSubagents: thread.unpromptedSubagents,
39434
+ createdAt: command.createdAt
39435
+ }
39436
+ });
39437
+ return [removedEvent, ...Array.isArray(turnStart) ? turnStart : [turnStart]];
39438
+ }
39104
39439
  case "thread.turn.interrupt":
39105
39440
  yield* requireThread({
39106
39441
  readModel,
@@ -39297,13 +39632,13 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
39297
39632
  });
39298
39633
  return events;
39299
39634
  }
39300
- case "thread.session.set":
39301
- yield* requireThread({
39635
+ case "thread.session.set": {
39636
+ const thread = yield* requireThread({
39302
39637
  readModel,
39303
39638
  command,
39304
39639
  threadId: command.threadId
39305
39640
  });
39306
- return {
39641
+ const sessionSetEvent = {
39307
39642
  ...yield* withEventBase({
39308
39643
  aggregateKind: "thread",
39309
39644
  aggregateId: command.threadId,
@@ -39317,6 +39652,26 @@ const decideCommand = Effect.fn("decideCommand")(function* ({ command, readModel
39317
39652
  session: command.session
39318
39653
  }
39319
39654
  };
39655
+ const queuedHead = thread.queuedMessages?.[0];
39656
+ const previousStatus = thread.session?.status;
39657
+ if (queuedHead === void 0 || command.session.status !== "ready" && command.session.status !== "stopped" || previousStatus !== "running" && previousStatus !== "starting") return sessionSetEvent;
39658
+ const settledReadModel = yield* projectEvent(readModel, {
39659
+ ...sessionSetEvent,
39660
+ sequence: readModel.snapshotSequence + 1
39661
+ }).pipe(Effect.orDie);
39662
+ const settledThread = settledReadModel.threads.find((entry) => entry.id === thread.id);
39663
+ if (settledThread === void 0 || isThreadBusy(settledThread, command.createdAt)) return sessionSetEvent;
39664
+ return [sessionSetEvent, ...yield* decideCommandSequence({
39665
+ readModel: settledReadModel,
39666
+ commands: [{
39667
+ type: "thread.queued-message.send",
39668
+ commandId: CommandId.make(`queued-message:${queuedHead.messageId}`),
39669
+ threadId: command.threadId,
39670
+ messageId: queuedHead.messageId,
39671
+ createdAt: command.createdAt
39672
+ }]
39673
+ }).pipe(Effect.catchTag("OrchestrationCommandInvariantError", () => Effect.succeed([])))];
39674
+ }
39320
39675
  case "thread.message.assistant.delta":
39321
39676
  yield* requireThread({
39322
39677
  readModel,
@@ -39679,7 +40034,12 @@ const makeOrchestrationEngine = Effect.gen(function* () {
39679
40034
  })));
39680
40035
  };
39681
40036
  yield* projectionPipeline.bootstrap;
40037
+ const readModelStartedAt = yield* Clock.currentTimeMillis;
39682
40038
  commandReadModel = yield* projectionSnapshotQuery.getCommandReadModel();
40039
+ yield* Effect.logInfo("orchestration command read model loaded", {
40040
+ durationMs: (yield* Clock.currentTimeMillis) - readModelStartedAt,
40041
+ sequence: commandReadModel.snapshotSequence
40042
+ });
39683
40043
  const worker = Effect.forever(Queue.take(commandQueue).pipe(Effect.flatMap(processEnvelope)));
39684
40044
  yield* Effect.forkScoped(worker);
39685
40045
  yield* Effect.logDebug("orchestration engine started").pipe(Effect.annotateLogs({ sequence: commandReadModel.snapshotSequence }));
@@ -39863,6 +40223,19 @@ const GetProjectionThreadScheduledTaskInput = Schema$1.Struct({ taskId: Orchestr
39863
40223
  const ListProjectionThreadScheduledTasksInput = Schema$1.Struct({ threadId: ThreadId });
39864
40224
  var ProjectionThreadScheduledTaskRepository = class extends Context.Service()("@p4code/cli/persistence/Services/ProjectionThreadScheduledTasks/ProjectionThreadScheduledTaskRepository") {};
39865
40225
  //#endregion
40226
+ //#region src/persistence/Services/ProjectionThreadQueuedMessages.ts
40227
+ const ProjectionThreadQueuedMessage = Schema$1.Struct({
40228
+ messageId: MessageId,
40229
+ threadId: ThreadId,
40230
+ text: Schema$1.String,
40231
+ attachments: Schema$1.fromJsonString(Schema$1.Array(ChatAttachment)),
40232
+ modelSelection: Schema$1.NullOr(Schema$1.fromJsonString(ModelSelection)),
40233
+ createdAt: IsoDateTime
40234
+ });
40235
+ const DeleteProjectionThreadQueuedMessageInput = Schema$1.Struct({ messageId: MessageId });
40236
+ const DeleteProjectionThreadQueuedMessagesByThreadInput = Schema$1.Struct({ threadId: ThreadId });
40237
+ var ProjectionThreadQueuedMessageRepository = class extends Context.Service()("@p4code/cli/persistence/Services/ProjectionThreadQueuedMessages/ProjectionThreadQueuedMessageRepository") {};
40238
+ //#endregion
39866
40239
  //#region src/persistence/Services/ProjectionThreadSessions.ts
39867
40240
  /**
39868
40241
  * ProjectionThreadSessionRepository - Repository interface for thread sessions.
@@ -39993,6 +40366,7 @@ const ProjectionThread = Schema$1.Struct({
39993
40366
  snoozedAt: Schema$1.NullOr(IsoDateTime),
39994
40367
  pinnedAt: Schema$1.NullOr(IsoDateTime),
39995
40368
  pinOrderKey: Schema$1.NullOr(Schema$1.String),
40369
+ parentThreadId: Schema$1.optional(Schema$1.NullOr(ThreadId)),
39996
40370
  latestUserMessageAt: Schema$1.NullOr(IsoDateTime),
39997
40371
  pendingApprovalCount: NonNegativeInt,
39998
40372
  pendingUserInputCount: NonNegativeInt,
@@ -40646,6 +41020,62 @@ const makeProjectionThreadScheduledTaskRepository = Effect.gen(function* () {
40646
41020
  });
40647
41021
  const ProjectionThreadScheduledTaskRepositoryLive = Layer.effect(ProjectionThreadScheduledTaskRepository, makeProjectionThreadScheduledTaskRepository);
40648
41022
  //#endregion
41023
+ //#region src/persistence/Layers/ProjectionThreadQueuedMessages.ts
41024
+ const makeProjectionThreadQueuedMessageRepository = Effect.gen(function* () {
41025
+ const sql = yield* SqlClient.SqlClient;
41026
+ const upsertRow = SqlSchema.void({
41027
+ Request: ProjectionThreadQueuedMessage,
41028
+ execute: (row) => sql`
41029
+ INSERT INTO projection_thread_queued_messages (
41030
+ message_id,
41031
+ thread_id,
41032
+ text,
41033
+ attachments_json,
41034
+ model_selection_json,
41035
+ created_at
41036
+ )
41037
+ VALUES (
41038
+ ${row.messageId},
41039
+ ${row.threadId},
41040
+ ${row.text},
41041
+ ${row.attachments},
41042
+ ${row.modelSelection},
41043
+ ${row.createdAt}
41044
+ )
41045
+ ON CONFLICT (message_id)
41046
+ DO UPDATE SET
41047
+ thread_id = excluded.thread_id,
41048
+ text = excluded.text,
41049
+ attachments_json = excluded.attachments_json,
41050
+ model_selection_json = excluded.model_selection_json,
41051
+ created_at = excluded.created_at
41052
+ `
41053
+ });
41054
+ const deleteRow = SqlSchema.void({
41055
+ Request: DeleteProjectionThreadQueuedMessageInput,
41056
+ execute: ({ messageId }) => sql`
41057
+ DELETE FROM projection_thread_queued_messages
41058
+ WHERE message_id = ${messageId}
41059
+ `
41060
+ });
41061
+ const deleteThreadRows = SqlSchema.void({
41062
+ Request: DeleteProjectionThreadQueuedMessagesByThreadInput,
41063
+ execute: ({ threadId }) => sql`
41064
+ DELETE FROM projection_thread_queued_messages
41065
+ WHERE thread_id = ${threadId}
41066
+ `
41067
+ });
41068
+ const upsert = (row) => upsertRow(row).pipe(Effect.mapError(toPersistenceSqlError("ProjectionThreadQueuedMessageRepository.upsert:query")));
41069
+ const deleteByMessageId = (input) => deleteRow(input).pipe(Effect.mapError(toPersistenceSqlError("ProjectionThreadQueuedMessageRepository.deleteByMessageId:query")));
41070
+ const deleteByThreadId = (input) => deleteThreadRows(input).pipe(Effect.mapError(toPersistenceSqlError("ProjectionThreadQueuedMessageRepository.deleteByThreadId:query")));
41071
+ return {
41072
+ upsert,
41073
+ deleteByMessageId,
41074
+ deleteByThreadId
41075
+ };
41076
+ });
41077
+ const ProjectionThreadQueuedMessageRepositoryLive = Layer.effect(ProjectionThreadQueuedMessageRepository, makeProjectionThreadQueuedMessageRepository);
41078
+ //#endregion
40649
41079
  //#region src/persistence/Layers/ProjectionThreadSessions.ts
40650
41080
  const makeProjectionThreadSessionRepository = Effect.gen(function* () {
40651
41081
  const sql = yield* SqlClient.SqlClient;
@@ -40984,6 +41414,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
40984
41414
  snoozed_at,
40985
41415
  pinned_at,
40986
41416
  pin_order_key,
41417
+ parent_thread_id,
40987
41418
  latest_user_message_at,
40988
41419
  pending_approval_count,
40989
41420
  pending_user_input_count,
@@ -41020,6 +41451,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
41020
41451
  ${row.snoozedAt},
41021
41452
  ${row.pinnedAt},
41022
41453
  ${row.pinOrderKey},
41454
+ ${row.parentThreadId ?? null},
41023
41455
  ${row.latestUserMessageAt},
41024
41456
  ${row.pendingApprovalCount},
41025
41457
  ${row.pendingUserInputCount},
@@ -41056,6 +41488,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
41056
41488
  snoozed_at = excluded.snoozed_at,
41057
41489
  pinned_at = excluded.pinned_at,
41058
41490
  pin_order_key = excluded.pin_order_key,
41491
+ parent_thread_id = COALESCE(excluded.parent_thread_id, projection_threads.parent_thread_id),
41059
41492
  latest_user_message_at = excluded.latest_user_message_at,
41060
41493
  pending_approval_count = excluded.pending_approval_count,
41061
41494
  pending_user_input_count = excluded.pending_user_input_count,
@@ -41097,6 +41530,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
41097
41530
  snoozed_at AS "snoozedAt",
41098
41531
  pinned_at AS "pinnedAt",
41099
41532
  pin_order_key AS "pinOrderKey",
41533
+ parent_thread_id AS "parentThreadId",
41100
41534
  latest_user_message_at AS "latestUserMessageAt",
41101
41535
  pending_approval_count AS "pendingApprovalCount",
41102
41536
  pending_user_input_count AS "pendingUserInputCount",
@@ -41140,6 +41574,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
41140
41574
  snoozed_at AS "snoozedAt",
41141
41575
  pinned_at AS "pinnedAt",
41142
41576
  pin_order_key AS "pinOrderKey",
41577
+ parent_thread_id AS "parentThreadId",
41143
41578
  latest_user_message_at AS "latestUserMessageAt",
41144
41579
  pending_approval_count AS "pendingApprovalCount",
41145
41580
  pending_user_input_count AS "pendingUserInputCount",
@@ -41189,6 +41624,7 @@ const ORCHESTRATION_PROJECTOR_NAMES = {
41189
41624
  threadMessages: "projection.thread-messages",
41190
41625
  threadProposedPlans: "projection.thread-proposed-plans",
41191
41626
  threadScheduledTasks: "projection.thread-scheduled-tasks",
41627
+ threadQueuedMessages: "projection.thread-queued-messages",
41192
41628
  threadActivities: "projection.thread-activities",
41193
41629
  threadSessions: "projection.thread-sessions",
41194
41630
  threadTurns: "projection.thread-turns",
@@ -41400,6 +41836,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
41400
41836
  const projectionThreadMessageRepository = yield* ProjectionThreadMessageRepository;
41401
41837
  const projectionThreadProposedPlanRepository = yield* ProjectionThreadProposedPlanRepository;
41402
41838
  const projectionThreadScheduledTaskRepository = yield* ProjectionThreadScheduledTaskRepository;
41839
+ const projectionThreadQueuedMessageRepository = yield* ProjectionThreadQueuedMessageRepository;
41403
41840
  const projectionThreadActivityRepository = yield* ProjectionThreadActivityRepository;
41404
41841
  const projectionThreadSessionRepository = yield* ProjectionThreadSessionRepository;
41405
41842
  const projectionTurnRepository = yield* ProjectionTurnRepository;
@@ -41619,6 +42056,7 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
41619
42056
  snoozedAt: null,
41620
42057
  pinnedAt: null,
41621
42058
  pinOrderKey: null,
42059
+ parentThreadId: event.payload.parentThreadId ?? null,
41622
42060
  latestUserMessageAt: null,
41623
42061
  pendingApprovalCount: 0,
41624
42062
  pendingUserInputCount: 0,
@@ -41936,6 +42374,27 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
41936
42374
  default: return;
41937
42375
  }
41938
42376
  });
42377
+ const applyThreadQueuedMessagesProjection = Effect.fn("applyThreadQueuedMessagesProjection")(function* (event) {
42378
+ switch (event.type) {
42379
+ case "thread.message-queued":
42380
+ yield* projectionThreadQueuedMessageRepository.upsert({
42381
+ messageId: event.payload.message.messageId,
42382
+ threadId: event.payload.threadId,
42383
+ text: event.payload.message.text,
42384
+ attachments: event.payload.message.attachments,
42385
+ modelSelection: event.payload.message.modelSelection ?? null,
42386
+ createdAt: event.payload.message.createdAt
42387
+ });
42388
+ return;
42389
+ case "thread.queued-message-removed":
42390
+ yield* projectionThreadQueuedMessageRepository.deleteByMessageId({ messageId: event.payload.messageId });
42391
+ return;
42392
+ case "thread.deleted":
42393
+ yield* projectionThreadQueuedMessageRepository.deleteByThreadId({ threadId: event.payload.threadId });
42394
+ return;
42395
+ default: return;
42396
+ }
42397
+ });
41939
42398
  const applyThreadScheduledTasksProjection = Effect.fn("applyThreadScheduledTasksProjection")(function* (event) {
41940
42399
  switch (event.type) {
41941
42400
  case "thread.scheduled-task.created":
@@ -42327,74 +42786,214 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
42327
42786
  const projectors = [
42328
42787
  {
42329
42788
  name: ORCHESTRATION_PROJECTOR_NAMES.projects,
42789
+ eventTypes: [
42790
+ "project.created",
42791
+ "project.meta-updated",
42792
+ "project.deleted"
42793
+ ],
42330
42794
  apply: applyProjectsProjection
42331
42795
  },
42332
42796
  {
42333
42797
  name: ORCHESTRATION_PROJECTOR_NAMES.threadPairs,
42798
+ eventTypes: [
42799
+ "thread.message-sent",
42800
+ "thread-pair.created",
42801
+ "thread-pair.gate-opened",
42802
+ "thread-pair.gate-advanced",
42803
+ "thread-pair.gate-resolved",
42804
+ "thread-pair.detached",
42805
+ "thread-pair.cursor-advanced",
42806
+ "thread-pair.triage-advanced"
42807
+ ],
42334
42808
  apply: applyThreadPairsProjection
42335
42809
  },
42336
42810
  {
42337
42811
  name: ORCHESTRATION_PROJECTOR_NAMES.threadMessages,
42812
+ eventTypes: [
42813
+ "thread.created",
42814
+ "thread.message-sent",
42815
+ "thread.reverted"
42816
+ ],
42338
42817
  apply: applyThreadMessagesProjection
42339
42818
  },
42340
42819
  {
42341
42820
  name: ORCHESTRATION_PROJECTOR_NAMES.threadProposedPlans,
42821
+ eventTypes: ["thread.proposed-plan-upserted", "thread.reverted"],
42342
42822
  apply: applyThreadProposedPlansProjection
42343
42823
  },
42344
42824
  {
42345
42825
  name: ORCHESTRATION_PROJECTOR_NAMES.threadScheduledTasks,
42826
+ eventTypes: [
42827
+ "thread.scheduled-task.created",
42828
+ "thread.scheduled-task.cancelled",
42829
+ "thread.scheduled-task.fired"
42830
+ ],
42346
42831
  apply: applyThreadScheduledTasksProjection
42347
42832
  },
42833
+ {
42834
+ name: ORCHESTRATION_PROJECTOR_NAMES.threadQueuedMessages,
42835
+ eventTypes: [
42836
+ "thread.message-queued",
42837
+ "thread.queued-message-removed",
42838
+ "thread.deleted"
42839
+ ],
42840
+ apply: applyThreadQueuedMessagesProjection
42841
+ },
42348
42842
  {
42349
42843
  name: ORCHESTRATION_PROJECTOR_NAMES.threadActivities,
42844
+ eventTypes: ["thread.activity-appended", "thread.reverted"],
42350
42845
  apply: applyThreadActivitiesProjection
42351
42846
  },
42352
42847
  {
42353
42848
  name: ORCHESTRATION_PROJECTOR_NAMES.threadSessions,
42849
+ eventTypes: ["thread.session-set"],
42354
42850
  apply: applyThreadSessionsProjection
42355
42851
  },
42356
42852
  {
42357
42853
  name: ORCHESTRATION_PROJECTOR_NAMES.threadTurns,
42854
+ eventTypes: [
42855
+ "thread.turn-start-requested",
42856
+ "thread.activity-appended",
42857
+ "thread.session-set",
42858
+ "thread.message-sent",
42859
+ "thread.turn-interrupt-requested",
42860
+ "thread.turn-diff-completed",
42861
+ "thread.reverted"
42862
+ ],
42358
42863
  apply: applyThreadTurnsProjection
42359
42864
  },
42360
42865
  {
42361
42866
  name: ORCHESTRATION_PROJECTOR_NAMES.checkpoints,
42867
+ eventTypes: [],
42362
42868
  apply: applyCheckpointsProjection
42363
42869
  },
42364
42870
  {
42365
42871
  name: ORCHESTRATION_PROJECTOR_NAMES.pendingApprovals,
42872
+ eventTypes: ["thread.activity-appended", "thread.approval-response-requested"],
42366
42873
  apply: applyPendingApprovalsProjection
42367
42874
  },
42368
42875
  {
42369
42876
  name: ORCHESTRATION_PROJECTOR_NAMES.threads,
42877
+ eventTypes: [
42878
+ "thread.created",
42879
+ "thread.archived",
42880
+ "thread.unarchived",
42881
+ "thread.settled",
42882
+ "thread.unsettled",
42883
+ "thread.snoozed",
42884
+ "thread.unsnoozed",
42885
+ "thread.pinned",
42886
+ "thread.unpinned",
42887
+ "thread.pin-reordered",
42888
+ "thread.meta-updated",
42889
+ "thread.runtime-mode-set",
42890
+ "thread.interaction-mode-set",
42891
+ "thread.compress-mode-set",
42892
+ "thread.unprompted-subagents-set",
42893
+ "thread.pull-requests-updated",
42894
+ "thread.computer-use-grant-set",
42895
+ "thread.deleted",
42896
+ "thread.message-sent",
42897
+ "thread.proposed-plan-upserted",
42898
+ "thread.activity-appended",
42899
+ "thread.approval-response-requested",
42900
+ "thread.user-input-response-requested",
42901
+ "thread.session-set",
42902
+ "thread.turn-diff-completed",
42903
+ "thread.reverted"
42904
+ ],
42370
42905
  apply: applyThreadsProjection
42371
42906
  }
42372
42907
  ];
42373
- const runProjectorForEvent = Effect.fn("runProjectorForEvent")(function* (projector, event) {
42908
+ const runProjectorBatch = Effect.fn("runProjectorBatch")(function* (projector, events) {
42909
+ const lastEvent = events.at(-1);
42910
+ if (lastEvent === void 0) return;
42374
42911
  const attachmentSideEffects = {
42375
42912
  deletedThreadIds: /* @__PURE__ */ new Set(),
42376
42913
  prunedThreadRelativePaths: /* @__PURE__ */ new Map()
42377
42914
  };
42378
- yield* sql.withTransaction(projector.apply(event, attachmentSideEffects).pipe(Effect.flatMap(() => projectionStateRepository.upsert({
42379
- projector: projector.name,
42380
- lastAppliedSequence: event.sequence,
42381
- updatedAt: event.occurredAt
42382
- }))));
42915
+ yield* sql.withTransaction(Effect.gen(function* () {
42916
+ for (const event of events) yield* projector.apply(event, attachmentSideEffects);
42917
+ yield* projectionStateRepository.upsert({
42918
+ projector: projector.name,
42919
+ lastAppliedSequence: lastEvent.sequence,
42920
+ updatedAt: lastEvent.occurredAt
42921
+ });
42922
+ }));
42383
42923
  yield* runAttachmentSideEffects(attachmentSideEffects).pipe(Effect.catch((cause) => Effect.logWarning("failed to apply projected attachment side-effects", {
42384
42924
  projector: projector.name,
42925
+ sequence: lastEvent.sequence,
42926
+ cause
42927
+ })));
42928
+ });
42929
+ const bootstrapProjector = Effect.fn("bootstrapProjector")(function* (projector, head) {
42930
+ const stateRow = yield* projectionStateRepository.getByProjector({ projector: projector.name });
42931
+ const cursor = Option.isSome(stateRow) ? stateRow.value.lastAppliedSequence : 0;
42932
+ if (cursor >= head.sequence) return;
42933
+ const startedAt = yield* Clock.currentTimeMillis;
42934
+ let replayedEvents = 0;
42935
+ yield* eventStore.readFromSequence(cursor, Number.MAX_SAFE_INTEGER, {
42936
+ eventTypes: projector.eventTypes,
42937
+ throughSequence: head.sequence
42938
+ }).pipe(Stream.grouped(500), Stream.runForEach((events) => Effect.gen(function* () {
42939
+ let batch = [];
42940
+ for (const event of events) if (event.type === "thread.created" || event.type === "thread.archived" || event.type === "thread.reverted" || event.type === "thread.deleted") {
42941
+ yield* runProjectorBatch(projector, batch);
42942
+ batch = [];
42943
+ yield* runProjectorBatch(projector, [event]);
42944
+ } else batch.push(event);
42945
+ yield* runProjectorBatch(projector, batch);
42946
+ replayedEvents += events.length;
42947
+ })), Effect.withTracerEnabled(false));
42948
+ yield* projectionStateRepository.upsert({
42949
+ projector: projector.name,
42950
+ lastAppliedSequence: head.sequence,
42951
+ updatedAt: head.occurredAt
42952
+ });
42953
+ yield* Effect.logInfo("orchestration projection replay completed", {
42954
+ projector: projector.name,
42955
+ fromSequence: cursor,
42956
+ throughSequence: head.sequence,
42957
+ replayedEvents,
42958
+ durationMs: (yield* Clock.currentTimeMillis) - startedAt
42959
+ });
42960
+ });
42961
+ const projectEvent = (event) => Effect.gen(function* () {
42962
+ const attachmentSideEffects = {
42963
+ deletedThreadIds: /* @__PURE__ */ new Set(),
42964
+ prunedThreadRelativePaths: /* @__PURE__ */ new Map()
42965
+ };
42966
+ yield* sql.withTransaction(Effect.gen(function* () {
42967
+ for (const projector of projectors) if (projector.eventTypes.includes(event.type)) yield* projector.apply(event, attachmentSideEffects);
42968
+ yield* sql`
42969
+ INSERT INTO projection_state ${sql.insert(projectors.map((projector) => ({
42970
+ projector: projector.name,
42971
+ last_applied_sequence: event.sequence,
42972
+ updated_at: event.occurredAt
42973
+ })))}
42974
+ ON CONFLICT(projector) DO UPDATE SET
42975
+ last_applied_sequence = excluded.last_applied_sequence,
42976
+ updated_at = excluded.updated_at
42977
+ `;
42978
+ }));
42979
+ yield* runAttachmentSideEffects(attachmentSideEffects).pipe(Effect.catch((cause) => Effect.logWarning("failed to apply projected attachment side-effects", {
42385
42980
  sequence: event.sequence,
42386
42981
  eventType: event.type,
42387
42982
  cause
42388
42983
  })));
42389
- });
42390
- const bootstrapProjector = (projector) => projectionStateRepository.getByProjector({ projector: projector.name }).pipe(Effect.flatMap((stateRow) => Stream.runForEach(eventStore.readFromSequence(Option.isSome(stateRow) ? stateRow.value.lastAppliedSequence : 0, Number.MAX_SAFE_INTEGER), (event) => runProjectorForEvent(projector, event))));
42391
- const projectEvent = (event) => Effect.forEach(projectors, (projector) => runProjectorForEvent(projector, event), { concurrency: 1 }).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path), Effect.provideService(ServerConfig$1, serverConfig), Effect.asVoid, Effect.catchTag("SqlError", (sqlError) => Effect.fail(toPersistenceSqlError("ProjectionPipeline.projectEvent:query")(sqlError))));
42392
- return {
42393
- bootstrap: Effect.forEach(projectors, bootstrapProjector, { concurrency: 1 }).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path), Effect.provideService(ServerConfig$1, serverConfig), Effect.asVoid, Effect.tap(() => Effect.logDebug("orchestration projection pipeline bootstrapped").pipe(Effect.annotateLogs({ projectors: projectors.length }))), Effect.catchTag("SqlError", (sqlError) => Effect.fail(toPersistenceSqlError("ProjectionPipeline.bootstrap:query")(sqlError)))),
42984
+ }).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path), Effect.provideService(ServerConfig$1, serverConfig), Effect.catchTag("SqlError", (sqlError) => Effect.fail(toPersistenceSqlError("ProjectionPipeline.projectEvent:query")(sqlError))));
42985
+ return {
42986
+ bootstrap: Effect.gen(function* () {
42987
+ const head = (yield* sql`
42988
+ SELECT sequence, occurred_at AS "occurredAt"
42989
+ FROM orchestration_events ORDER BY sequence DESC LIMIT 1
42990
+ `)[0];
42991
+ if (head !== void 0) for (const projector of projectors) yield* bootstrapProjector(projector, head);
42992
+ }).pipe(Effect.provideService(FileSystem.FileSystem, fileSystem), Effect.provideService(Path$1.Path, path), Effect.provideService(ServerConfig$1, serverConfig), Effect.tap(() => Effect.logDebug("orchestration projection pipeline bootstrapped", { projectors: projectors.length })), Effect.catchTag("SqlError", (sqlError) => Effect.fail(toPersistenceSqlError("ProjectionPipeline.bootstrap:query")(sqlError)))),
42394
42993
  projectEvent
42395
42994
  };
42396
42995
  });
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));
42996
+ 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
42997
  //#endregion
42399
42998
  //#region src/persistence/Services/ProjectionCheckpoints.ts
42400
42999
  /**
@@ -42496,6 +43095,10 @@ function deriveLocalBranchNameFromRemoteRef$1(branchName) {
42496
43095
  if (firstSeparatorIndex <= 0 || firstSeparatorIndex === branchName.length - 1) return branchName;
42497
43096
  return branchName.slice(firstSeparatorIndex + 1);
42498
43097
  }
43098
+ function buildTemporaryWorktreeBranchName(randomHex) {
43099
+ const token = randomHex(4).toLowerCase().replace(/[^0-9a-f]/g, "").slice(0, 8);
43100
+ return `${WORKTREE_BRANCH_PREFIX}/${token}`;
43101
+ }
42499
43102
  function isTemporaryWorktreeBranch(refName) {
42500
43103
  return TEMP_WORKTREE_BRANCH_PATTERN.test(refName.trim().toLowerCase());
42501
43104
  }
@@ -43170,6 +43773,24 @@ function mapScheduledTaskRow(row) {
43170
43773
  failure: row.failure
43171
43774
  };
43172
43775
  }
43776
+ function mapQueuedMessageRow(row) {
43777
+ return {
43778
+ messageId: row.messageId,
43779
+ text: row.text,
43780
+ attachments: row.attachments,
43781
+ ...row.modelSelection !== null ? { modelSelection: row.modelSelection } : {},
43782
+ createdAt: row.createdAt
43783
+ };
43784
+ }
43785
+ function groupQueuedMessagesByThread(rows) {
43786
+ const byThread = /* @__PURE__ */ new Map();
43787
+ for (const row of rows) {
43788
+ const messages = byThread.get(row.threadId) ?? [];
43789
+ messages.push(mapQueuedMessageRow(row));
43790
+ byThread.set(row.threadId, messages);
43791
+ }
43792
+ return byThread;
43793
+ }
43173
43794
  function groupScheduledTasksByThread(rows) {
43174
43795
  const byThread = /* @__PURE__ */ new Map();
43175
43796
  for (const row of rows) {
@@ -43516,6 +44137,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43516
44137
  snoozed_at AS "snoozedAt",
43517
44138
  pinned_at AS "pinnedAt",
43518
44139
  pin_order_key AS "pinOrderKey",
44140
+ parent_thread_id AS "parentThreadId",
43519
44141
  latest_user_message_at AS "latestUserMessageAt",
43520
44142
  pending_approval_count AS "pendingApprovalCount",
43521
44143
  pending_user_input_count AS "pendingUserInputCount",
@@ -43562,6 +44184,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43562
44184
  snoozed_at AS "snoozedAt",
43563
44185
  pinned_at AS "pinnedAt",
43564
44186
  pin_order_key AS "pinOrderKey",
44187
+ parent_thread_id AS "parentThreadId",
43565
44188
  latest_user_message_at AS "latestUserMessageAt",
43566
44189
  pending_approval_count AS "pendingApprovalCount",
43567
44190
  pending_user_input_count AS "pendingUserInputCount",
@@ -43610,6 +44233,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43610
44233
  snoozed_at AS "snoozedAt",
43611
44234
  pinned_at AS "pinnedAt",
43612
44235
  pin_order_key AS "pinOrderKey",
44236
+ parent_thread_id AS "parentThreadId",
43613
44237
  latest_user_message_at AS "latestUserMessageAt",
43614
44238
  pending_approval_count AS "pendingApprovalCount",
43615
44239
  pending_user_input_count AS "pendingUserInputCount",
@@ -43675,6 +44299,33 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
43675
44299
  failure
43676
44300
  FROM projection_thread_scheduled_tasks
43677
44301
  ORDER BY thread_id ASC, run_at ASC, task_id ASC
44302
+ `
44303
+ });
44304
+ const queuedMessageColumns = sql`
44305
+ message_id AS "messageId",
44306
+ thread_id AS "threadId",
44307
+ text,
44308
+ attachments_json AS "attachments",
44309
+ model_selection_json AS "modelSelection",
44310
+ created_at AS "createdAt"
44311
+ `;
44312
+ const listThreadQueuedMessageRows = SqlSchema.findAll({
44313
+ Request: Schema$1.Void,
44314
+ Result: ProjectionThreadQueuedMessage,
44315
+ execute: () => sql`
44316
+ SELECT ${queuedMessageColumns}
44317
+ FROM projection_thread_queued_messages
44318
+ ORDER BY thread_id ASC, rowid ASC
44319
+ `
44320
+ });
44321
+ const listThreadQueuedMessageRowsByThread = SqlSchema.findAll({
44322
+ Request: ThreadIdLookupInput,
44323
+ Result: ProjectionThreadQueuedMessage,
44324
+ execute: ({ threadId }) => sql`
44325
+ SELECT ${queuedMessageColumns}
44326
+ FROM projection_thread_queued_messages
44327
+ WHERE thread_id = ${threadId}
44328
+ ORDER BY rowid ASC
43678
44329
  `
43679
44330
  });
43680
44331
  const listThreadScheduledTaskRowsByThread = SqlSchema.findAll({
@@ -44117,6 +44768,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
44117
44768
  snoozed_at AS "snoozedAt",
44118
44769
  pinned_at AS "pinnedAt",
44119
44770
  pin_order_key AS "pinOrderKey",
44771
+ parent_thread_id AS "parentThreadId",
44120
44772
  latest_user_message_at AS "latestUserMessageAt",
44121
44773
  pending_approval_count AS "pendingApprovalCount",
44122
44774
  pending_user_input_count AS "pendingUserInputCount",
@@ -44731,9 +45383,11 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
44731
45383
  listLatestTurnRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listLatestTurns:query", "ProjectionSnapshotQuery.getSnapshot:listLatestTurns:decodeRows"))),
44732
45384
  listProjectionStateRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listProjectionState:query", "ProjectionSnapshotQuery.getSnapshot:listProjectionState:decodeRows"))),
44733
45385
  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* () {
45386
+ listThreadScheduledTaskRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listThreadScheduledTasks:query", "ProjectionSnapshotQuery.getSnapshot:listThreadScheduledTasks:decodeRows"))),
45387
+ listThreadQueuedMessageRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getSnapshot:listThreadQueuedMessages:query", "ProjectionSnapshotQuery.getSnapshot:listThreadQueuedMessages:decodeRows")))
45388
+ ])).pipe(Effect.flatMap(([projectRows, threadRows, messageRows, proposedPlanRows, activityRows, sessionRows, checkpointRows, turnRows, latestTurnRows, stateRows, threadPairRows, scheduledTaskRows, queuedMessageRows]) => Effect.gen(function* () {
44736
45389
  const scheduledTasksByThread = groupScheduledTasksByThread(scheduledTaskRows);
45390
+ const queuedMessagesByThread = groupQueuedMessagesByThread(queuedMessageRows);
44737
45391
  const messagesByThread = /* @__PURE__ */ new Map();
44738
45392
  const proposedPlansByThread = /* @__PURE__ */ new Map();
44739
45393
  const activitiesByThread = /* @__PURE__ */ new Map();
@@ -44862,10 +45516,12 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
44862
45516
  snoozedAt: row.snoozedAt,
44863
45517
  pinnedAt: row.pinnedAt,
44864
45518
  pinOrderKey: row.pinOrderKey ?? null,
45519
+ parentThreadId: row.parentThreadId ?? null,
44865
45520
  deletedAt: row.deletedAt,
44866
45521
  messages: messagesByThread.get(row.threadId) ?? [],
44867
45522
  proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
44868
45523
  scheduledTasks: scheduledTasksByThread.get(row.threadId) ?? [],
45524
+ queuedMessages: queuedMessagesByThread.get(row.threadId) ?? [],
44869
45525
  activities: activitiesByThread.get(row.threadId) ?? [],
44870
45526
  checkpoints: checkpointsByThread.get(row.threadId) ?? [],
44871
45527
  session: sessionsByThread.get(row.threadId) ?? null
@@ -44977,9 +45633,11 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
44977
45633
  listLatestTurnRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listLatestTurns:query", "ProjectionSnapshotQuery.getCommandReadModel:listLatestTurns:decodeRows"))),
44978
45634
  listProjectionStateRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listProjectionState:query", "ProjectionSnapshotQuery.getCommandReadModel:listProjectionState:decodeRows"))),
44979
45635
  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(() => {
45636
+ listThreadScheduledTaskRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listThreadScheduledTasks:query", "ProjectionSnapshotQuery.getCommandReadModel:listThreadScheduledTasks:decodeRows"))),
45637
+ listThreadQueuedMessageRows(void 0).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getCommandReadModel:listThreadQueuedMessages:query", "ProjectionSnapshotQuery.getCommandReadModel:listThreadQueuedMessages:decodeRows")))
45638
+ ])).pipe(Effect.flatMap(([projectRows, threadRows, proposedPlanRows, sessionRows, latestTurnRows, stateRows, threadPairRows, scheduledTaskRows, queuedMessageRows]) => Effect.sync(() => {
44982
45639
  const scheduledTasksByThread = groupScheduledTasksByThread(scheduledTaskRows);
45640
+ const queuedMessagesByThread = groupQueuedMessagesByThread(queuedMessageRows);
44983
45641
  let updatedAt = null;
44984
45642
  const projects = [];
44985
45643
  const threads = [];
@@ -45078,10 +45736,12 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45078
45736
  snoozedAt: row.snoozedAt,
45079
45737
  pinnedAt: row.pinnedAt,
45080
45738
  pinOrderKey: row.pinOrderKey ?? null,
45739
+ parentThreadId: row.parentThreadId ?? null,
45081
45740
  deletedAt: row.deletedAt,
45082
45741
  messages: [],
45083
45742
  proposedPlans: proposedPlansByThread.get(row.threadId) ?? [],
45084
45743
  scheduledTasks: scheduledTasksByThread.get(row.threadId) ?? [],
45744
+ queuedMessages: queuedMessagesByThread.get(row.threadId) ?? [],
45085
45745
  activities: [],
45086
45746
  checkpoints: [],
45087
45747
  session: sessionByThread.get(row.threadId) ?? null
@@ -45151,6 +45811,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45151
45811
  snoozedAt: row.snoozedAt,
45152
45812
  pinnedAt: row.pinnedAt,
45153
45813
  pinOrderKey: row.pinOrderKey ?? null,
45814
+ parentThreadId: row.parentThreadId ?? null,
45154
45815
  session: sessionByThread.get(row.threadId) ?? null,
45155
45816
  latestUserMessageAt: row.latestUserMessageAt,
45156
45817
  hasPendingApprovals: row.pendingApprovalCount > 0,
@@ -45224,6 +45885,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45224
45885
  snoozedAt: row.snoozedAt,
45225
45886
  pinnedAt: row.pinnedAt,
45226
45887
  pinOrderKey: row.pinOrderKey ?? null,
45888
+ parentThreadId: row.parentThreadId ?? null,
45227
45889
  session: sessionByThread.get(row.threadId) ?? null,
45228
45890
  latestUserMessageAt: row.latestUserMessageAt,
45229
45891
  hasPendingApprovals: row.pendingApprovalCount > 0,
@@ -45367,6 +46029,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45367
46029
  snoozedAt: threadRow.value.snoozedAt,
45368
46030
  pinnedAt: threadRow.value.pinnedAt,
45369
46031
  pinOrderKey: threadRow.value.pinOrderKey ?? null,
46032
+ parentThreadId: threadRow.value.parentThreadId ?? null,
45370
46033
  session: Option.isSome(sessionRow) ? mapSessionRow(sessionRow.value) : null,
45371
46034
  latestUserMessageAt: threadRow.value.latestUserMessageAt,
45372
46035
  hasPendingApprovals: threadRow.value.pendingApprovalCount > 0,
@@ -45398,7 +46061,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45398
46061
  })).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listActivities:query", "ProjectionSnapshotQuery.getThreadDetailById:listActivities:decodeRows")), Effect.map((rows) => rows.map(mapThreadActivityRow)));
45399
46062
  const threadRow = yield* getThreadRowById({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getThread:query", "ProjectionSnapshotQuery.getThreadDetailById:getThread:decodeRow")));
45400
46063
  if (Option.isNone(threadRow)) return Option.none();
45401
- const [messageRows, proposedPlanRows, activities, checkpointRows, turnRows, latestTurnRow, sessionRow, scheduledTaskRows] = yield* Effect.all([
46064
+ const [messageRows, proposedPlanRows, activities, checkpointRows, turnRows, latestTurnRow, sessionRow, scheduledTaskRows, queuedMessageRows] = yield* Effect.all([
45402
46065
  (bounds === void 0 ? listThreadMessageRowsByThread({ threadId }) : listThreadMessageRowsByThreadWindow({
45403
46066
  threadId,
45404
46067
  ...bounds
@@ -45409,7 +46072,8 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45409
46072
  listTurnSummaryRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listTurnSummaries:query", "ProjectionSnapshotQuery.getThreadDetailById:listTurnSummaries:decodeRows"))),
45410
46073
  getLatestTurnRowByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getLatestTurn:query", "ProjectionSnapshotQuery.getThreadDetailById:getLatestTurn:decodeRow"))),
45411
46074
  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")))
46075
+ listThreadScheduledTaskRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listScheduledTasks:query", "ProjectionSnapshotQuery.getThreadDetailById:listScheduledTasks:decodeRows"))),
46076
+ listThreadQueuedMessageRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listQueuedMessages:query", "ProjectionSnapshotQuery.getThreadDetailById:listQueuedMessages:decodeRows")))
45413
46077
  ]);
45414
46078
  const thread = {
45415
46079
  id: threadRow.value.threadId,
@@ -45441,6 +46105,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45441
46105
  snoozedAt: threadRow.value.snoozedAt,
45442
46106
  pinnedAt: threadRow.value.pinnedAt,
45443
46107
  pinOrderKey: threadRow.value.pinOrderKey ?? null,
46108
+ parentThreadId: threadRow.value.parentThreadId ?? null,
45444
46109
  deletedAt: null,
45445
46110
  messages: messageRows.map((row) => {
45446
46111
  const message = {
@@ -45457,6 +46122,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
45457
46122
  }),
45458
46123
  proposedPlans: proposedPlanRows.map(mapProposedPlanRow),
45459
46124
  scheduledTasks: scheduledTaskRows.map(mapScheduledTaskRow),
46125
+ queuedMessages: queuedMessageRows.map(mapQueuedMessageRow),
45460
46126
  activities,
45461
46127
  checkpoints: checkpointRows.map((row) => ({
45462
46128
  turnId: row.turnId,
@@ -48273,6 +48939,7 @@ const make$96 = Effect.gen(function* () {
48273
48939
  threadPinning: true,
48274
48940
  threadFork: true,
48275
48941
  threadScheduledTasks: true,
48942
+ threadMessageQueue: true,
48276
48943
  ...serverSelfUpdate === null ? {} : { serverSelfUpdate },
48277
48944
  ...serverServiceSupervised ? { serverServiceSupervised } : {}
48278
48945
  }
@@ -58019,11 +58686,22 @@ const requireThreadControlTarget = Effect.fn("mcp.requireThreadControlTarget")(f
58019
58686
  invocation,
58020
58687
  threadId: invocation.threadId
58021
58688
  };
58022
- if (!invocation.controlThreadIds?.has(requested)) return yield* new ThreadControlNotPermittedError({ threadId: requested });
58023
- return {
58689
+ if (invocation.controlThreadIds?.has(requested)) return {
58024
58690
  invocation,
58025
58691
  threadId: requested
58026
58692
  };
58693
+ if (yield* isChildOfInvocationThread(invocation.threadId, requested)) return {
58694
+ invocation,
58695
+ threadId: requested
58696
+ };
58697
+ return yield* new ThreadControlNotPermittedError({ threadId: requested });
58698
+ });
58699
+ /** Whether `childThreadId` was spawned by `parentThreadId` as a sub-thread. */
58700
+ const isChildOfInvocationThread = Effect.fn("mcp.isChildOfInvocationThread")(function* (parentThreadId, childThreadId) {
58701
+ const query = yield* Effect.serviceOption(ProjectionSnapshotQuery);
58702
+ if (Option.isNone(query)) return false;
58703
+ const shell = yield* query.value.getThreadShellById(childThreadId).pipe(Effect.catch(() => Effect.succeed(Option.none())));
58704
+ return Option.isSome(shell) && shell.value.parentThreadId === parentThreadId;
58027
58705
  });
58028
58706
  /**
58029
58707
  * The guard on starting a thread, which is a different question from acting on
@@ -67235,7 +67913,7 @@ function parseCpaModelDefinitions(json, provider) {
67235
67913
  id,
67236
67914
  name: stringOr(raw.display_name) ?? id,
67237
67915
  family: familyOf(id),
67238
- ...created ? { releaseDate: (/* @__PURE__ */ new Date(created * 1e3)).toISOString() } : {},
67916
+ ...created ? { releaseDate: DateTime.formatIso(DateTime.makeUnsafe(created * 1e3)) } : {},
67239
67917
  ...contextTokens ? { contextTokens } : {},
67240
67918
  ...Array.isArray(levels) ? { effort: levels.filter((level) => typeof level === "string") } : {}
67241
67919
  }];
@@ -67294,7 +67972,7 @@ function parseOpenRouter(json) {
67294
67972
  id,
67295
67973
  name: (stringOr(raw.name) ?? id).replace(/^[^:]+:\s*/, ""),
67296
67974
  family: familyOf(id),
67297
- ...created ? { releaseDate: (/* @__PURE__ */ new Date(created * 1e3)).toISOString() } : {},
67975
+ ...created ? { releaseDate: DateTime.formatIso(DateTime.makeUnsafe(created * 1e3)) } : {},
67298
67976
  ...contextTokens ? { contextTokens } : {}
67299
67977
  });
67300
67978
  }
@@ -94144,6 +94822,31 @@ const makeCodexSessionRuntime = (options) => Effect.gen(function* () {
94144
94822
  ...options.fusionPromptSettings ? { fusionPromptSettings: options.fusionPromptSettings } : {},
94145
94823
  ...input.unpromptedSubagents !== void 0 ? { unpromptedSubagents: input.unpromptedSubagents } : {}
94146
94824
  });
94825
+ const activeSession = yield* Ref.get(sessionRef);
94826
+ if (activeSession.status === "running" && activeSession.activeTurnId !== void 0) {
94827
+ const steeredTurnId = activeSession.activeTurnId;
94828
+ const steerExit = yield* client.raw.request("turn/steer", {
94829
+ threadId: providerThreadId,
94830
+ expectedTurnId: steeredTurnId,
94831
+ input: params.input
94832
+ }).pipe(Effect.exit);
94833
+ if (Exit.isSuccess(steerExit)) {
94834
+ const steeredProviderThreadId = currentProviderThreadId(yield* Ref.get(sessionRef));
94835
+ return {
94836
+ threadId: options.threadId,
94837
+ turnId: steeredTurnId,
94838
+ ...steeredProviderThreadId ? { resumeCursor: { threadId: steeredProviderThreadId } } : {}
94839
+ };
94840
+ }
94841
+ if (findActiveCodexTurnId(yield* client.request("thread/read", {
94842
+ threadId: providerThreadId,
94843
+ includeTurns: true
94844
+ })) === steeredTurnId) {
94845
+ yield* Effect.logWarning("Codex turn/steer failed while the turn is still running.", { cause: steerExit.cause });
94846
+ return yield* Effect.failCause(steerExit.cause);
94847
+ }
94848
+ yield* Effect.logInfo("Codex turn ended before the steer landed; starting a new turn.");
94849
+ }
94147
94850
  const { collaborationMode: attachedCollaborationMode, ...paramsWithoutCollaboration } = params;
94148
94851
  const lastCollaborationMode = yield* Ref.get(lastCollaborationModeRef);
94149
94852
  const requestParams = attachedCollaborationMode !== void 0 && lastCollaborationMode !== null && isSameCollaborationMode(attachedCollaborationMode, lastCollaborationMode) ? paramsWithoutCollaboration : params;
@@ -125052,6 +125755,86 @@ const make$36 = Effect.gen(function* () {
125052
125755
  ...cleanup === void 0 ? {} : { cleanup }
125053
125756
  });
125054
125757
  }
125758
+ const finishMergedCleanup = Effect.fn("ThreadWorkspaceLifecycle.finishMergedCleanup")(function* (input) {
125759
+ yield* record({
125760
+ threadIds,
125761
+ lifecycle: {
125762
+ status: "cleanup-pending",
125763
+ detail: "Merged context recorded; workspace cleanup is pending.",
125764
+ pullRequestNumber: input.merged.pullRequestNumber,
125765
+ mergeCommitSha: input.merged.mergeCommitSha,
125766
+ updatedAt: now,
125767
+ ...cleanup === void 0 ? {} : { cleanup }
125768
+ }
125769
+ });
125770
+ yield* cleanGeneratedFiles();
125771
+ if (cleanup === void 0 || cleanup.worktree) yield* gitWorkflow.removeWorktree({
125772
+ cwd: project.workspaceRoot,
125773
+ path: worktreePath,
125774
+ ...cleanup?.forceWorktree ? { force: true } : {}
125775
+ });
125776
+ if (cleanup === void 0 || cleanup.localBranch) yield* git.execute({
125777
+ operation: "ThreadWorkspaceLifecycle.deleteBranch",
125778
+ cwd: project.workspaceRoot,
125779
+ args: [
125780
+ "update-ref",
125781
+ "-d",
125782
+ `refs/heads/${input.branch}`,
125783
+ input.branchCommitSha
125784
+ ]
125785
+ }).pipe(Effect.catchCause((cause) => gitWorkflow.createWorktree({
125786
+ cwd: project.workspaceRoot,
125787
+ refName: input.branch,
125788
+ path: worktreePath
125789
+ }).pipe(Effect.andThen(Effect.failCause(cause)))));
125790
+ const detail = cleanup === void 0 ? "Merged worktree and branch removed; thread context preserved." : "Selected archive resources removed; thread context preserved.";
125791
+ yield* record({
125792
+ threadIds,
125793
+ lifecycle: {
125794
+ status: "cleaned",
125795
+ detail,
125796
+ pullRequestNumber: input.merged.pullRequestNumber,
125797
+ mergeCommitSha: input.merged.mergeCommitSha,
125798
+ updatedAt: DateTime.formatIso(yield* DateTime.now)
125799
+ }
125800
+ });
125801
+ return {
125802
+ outcome: "cleaned",
125803
+ detail
125804
+ };
125805
+ });
125806
+ const parentThread = requestedThread.parentThreadId == null ? void 0 : snapshot.threads.find((thread) => thread.id === requestedThread.parentThreadId);
125807
+ if (parentThread !== void 0 && !cleanup?.remoteBranch) {
125808
+ if (yield* git.execute({
125809
+ operation: "ThreadWorkspaceLifecycle.landedInParent",
125810
+ cwd: parentThread.worktreePath ?? project.workspaceRoot,
125811
+ args: [
125812
+ "merge-base",
125813
+ "--is-ancestor",
125814
+ `refs/heads/${branch}`,
125815
+ "HEAD"
125816
+ ],
125817
+ allowNonZeroExit: true
125818
+ }).pipe(Effect.map((result) => result.exitCode === 0), Effect.orElseSucceed(() => false))) {
125819
+ const branchCommitSha = (yield* git.execute({
125820
+ operation: "ThreadWorkspaceLifecycle.branchCommit",
125821
+ cwd: project.workspaceRoot,
125822
+ args: [
125823
+ "rev-parse",
125824
+ "--verify",
125825
+ `refs/heads/${branch}^{commit}`
125826
+ ]
125827
+ })).stdout.trim();
125828
+ return yield* finishMergedCleanup({
125829
+ branch,
125830
+ branchCommitSha,
125831
+ merged: {
125832
+ mergeCommitSha: branchCommitSha,
125833
+ pullRequestNumber: null
125834
+ }
125835
+ });
125836
+ }
125837
+ }
125055
125838
  const refs = yield* gitWorkflow.listRefs({
125056
125839
  cwd: project.workspaceRoot,
125057
125840
  includeMatchingRemoteRefs: true,
@@ -125164,52 +125947,11 @@ const make$36 = Effect.gen(function* () {
125164
125947
  }).pipe(Effect.result))._tag === "Failure") return yield* workspaceRefusal("Cleanup refused because the remote branch could not be removed.");
125165
125948
  }
125166
125949
  }
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
- }
125950
+ return yield* finishMergedCleanup({
125951
+ branch,
125952
+ branchCommitSha: branchCommitResult.stdout.trim(),
125953
+ merged
125208
125954
  });
125209
- return {
125210
- outcome: "cleaned",
125211
- detail
125212
- };
125213
125955
  });
125214
125956
  const cleanup = (threadId) => cleanupRaw(threadId).pipe(mapLifecycleError);
125215
125957
  return {
@@ -133212,7 +133954,7 @@ function projectSetupScriptCompatibilityDetail(error) {
133212
133954
  }
133213
133955
  }
133214
133956
  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";
133957
+ 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
133958
  }
133217
133959
  const PROVIDER_STATUS_DEBOUNCE_MS = 200;
133218
133960
  const SHELL_RESUME_MAX_GAP = 1e3;
@@ -135254,7 +135996,7 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
135254
135996
  const next = new Map(records);
135255
135997
  next.set(tokenHash, {
135256
135998
  tokenHash,
135257
- scope: scopeWith(!spawnedThreads.has(threadId), spawnedThreads.get(threadId) === true),
135999
+ scope: scopeWith(!(spawnedThreads.has(threadId) || request.spawned === true), spawnedThreads.get(threadId) === true),
135258
136000
  controlThreadIds: /* @__PURE__ */ new Set(),
135259
136001
  lastAliveAt: issuedAt
135260
136002
  });
@@ -135657,7 +136399,8 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
135657
136399
  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
136400
  cause
135659
136401
  }))) : 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;
136402
+ const shell = Option.isSome(projectionQuery) ? yield* projectionQuery.value.getThreadShellById(threadId).pipe(Effect.map(Option.getOrUndefined), Effect.catch(() => Effect.succeed(void 0))) : void 0;
136403
+ const threadTitle = shell?.title;
135661
136404
  const credential = yield* issueActiveMcpCredential({
135662
136405
  threadId,
135663
136406
  ...threadTitle ? { threadTitle } : {},
@@ -135666,7 +136409,8 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
135666
136409
  computerUseAccess,
135667
136410
  ...watchThreadIds !== void 0 ? { watchThreadIds } : {},
135668
136411
  ...adviseThreadIds !== void 0 ? { adviseThreadIds } : {},
135669
- ...fusionRole !== void 0 ? { fusionRole } : {}
136412
+ ...fusionRole !== void 0 ? { fusionRole } : {},
136413
+ ...shell?.parentThreadId != null ? { spawned: true } : {}
135670
136414
  });
135671
136415
  if (credential) yield* Effect.sync(() => setMcpProviderSession(credential.config));
135672
136416
  return credential;
@@ -138151,6 +138895,196 @@ const TaskToolkitHandlersLive = TaskToolkit.toLayer({
138151
138895
  })
138152
138896
  });
138153
138897
  //#endregion
138898
+ //#region ../../packages/shared/src/threadPendingWork.ts
138899
+ /**
138900
+ * A user message the server has not started a turn for yet, within the window
138901
+ * where adoption is still plausible. Mirrors `hasQueuedTurnStart` in
138902
+ * client-runtime, which owns the same window for settle/snooze decisions.
138903
+ */
138904
+ const QUEUED_TURN_START_GRACE_MS = 120 * 1e3;
138905
+ function hasQueuedTurn(input, nowMs) {
138906
+ if (input.latestUserMessageAt == null) return false;
138907
+ if (input.session?.status === "error") return false;
138908
+ const messageAtMs = Date.parse(input.latestUserMessageAt);
138909
+ if (Number.isNaN(messageAtMs)) return false;
138910
+ if (Math.abs(nowMs - messageAtMs) > QUEUED_TURN_START_GRACE_MS) return false;
138911
+ const turn = input.latestTurn;
138912
+ if (turn === null) return true;
138913
+ return [
138914
+ turn.requestedAt,
138915
+ turn.startedAt,
138916
+ turn.completedAt
138917
+ ].every((candidate) => candidate == null || Date.parse(candidate) < messageAtMs);
138918
+ }
138919
+ /**
138920
+ * What the thread is waiting on, or null when it is genuinely idle. A running
138921
+ * session answers null: the agent is working, which callers already show.
138922
+ */
138923
+ function resolveThreadPendingWork(input, options) {
138924
+ const nowMs = Date.parse(options.now);
138925
+ if (Number.isNaN(nowMs)) return null;
138926
+ if (input.session?.status === "starting" || input.session?.status === "running") return null;
138927
+ if (hasQueuedTurn(input, nowMs)) return "queued";
138928
+ if (input.session === null) return null;
138929
+ if (input.hasBackgroundTasks === true) return "background";
138930
+ if (input.scheduledWakeAt != null) {
138931
+ const wakeAtMs = Date.parse(input.scheduledWakeAt);
138932
+ if (!Number.isNaN(wakeAtMs) && wakeAtMs > nowMs) return "scheduled";
138933
+ }
138934
+ return null;
138935
+ }
138936
+ //#endregion
138937
+ //#region src/mcp/toolkits/threads/subThreadState.ts
138938
+ /**
138939
+ * Where a sub-thread stands, for a parent deciding what to do next. A
138940
+ * question or approval outranks running: the session keeps reporting running
138941
+ * while it waits, but nothing moves until someone answers.
138942
+ */
138943
+ function resolveSubThreadState(shell, now) {
138944
+ if (shell === void 0) return "missing";
138945
+ if (shell.archivedAt !== null) return "completed";
138946
+ if (shell.hasPendingApprovals || shell.hasPendingUserInput) return "needs-input";
138947
+ const status = shell.session?.status;
138948
+ if (status === "running" || status === "starting") return "running";
138949
+ if (resolveThreadPendingWork(shell, { now }) !== null) return "running";
138950
+ if (status === "error" || shell.latestTurn?.state === "error") return "failed";
138951
+ return "completed";
138952
+ }
138953
+ /** Whether a wait in `mode` is over for these states. */
138954
+ function isSubThreadWaitSatisfied(states, mode) {
138955
+ const stopped = (state) => state !== "running";
138956
+ return mode === "any" ? states.some(stopped) : states.every(stopped);
138957
+ }
138958
+ //#endregion
138959
+ //#region src/orchestration/SubThreadWorkspace.ts
138960
+ /**
138961
+ * Git steps for sub-threads: a child gets its own worktree branched from the
138962
+ * parent's current HEAD, and lands by merging back into the parent's worktree.
138963
+ */
138964
+ const repositoryLocks = /* @__PURE__ */ new Map();
138965
+ const withRepositoryLock = (repositoryRoot) => {
138966
+ let lock = repositoryLocks.get(repositoryRoot);
138967
+ if (lock === void 0) {
138968
+ lock = Semaphore.makeUnsafe(1);
138969
+ repositoryLocks.set(repositoryRoot, lock);
138970
+ }
138971
+ return lock.withPermits(1);
138972
+ };
138973
+ const runGit = (operation, cwd, args) => Effect.gen(function* () {
138974
+ return yield* (yield* GitVcsDriver).execute({
138975
+ operation: `SubThreadWorkspace.${operation}`,
138976
+ cwd,
138977
+ args,
138978
+ allowNonZeroExit: true
138979
+ });
138980
+ });
138981
+ /** The branch checked out at `cwd`, or null on a detached HEAD. */
138982
+ const currentBranch = (cwd) => runGit("currentBranch", cwd, [
138983
+ "symbolic-ref",
138984
+ "--quiet",
138985
+ "--short",
138986
+ "HEAD"
138987
+ ]).pipe(Effect.map((result) => result.exitCode === 0 ? result.stdout.trim() || null : null));
138988
+ /** Whether `cwd` has uncommitted changes, untracked files included. */
138989
+ const hasUncommittedChanges = (cwd) => runGit("status", cwd, ["status", "--porcelain"]).pipe(Effect.map((result) => result.stdout.trim().length > 0));
138990
+ const headCommit = (cwd) => runGit("headCommit", cwd, [
138991
+ "rev-parse",
138992
+ "--verify",
138993
+ "HEAD^{commit}"
138994
+ ]).pipe(Effect.map((result) => result.exitCode === 0 ? result.stdout.trim() : null));
138995
+ /** Creates the child's worktree on a new branch at the parent's HEAD commit. */
138996
+ const provisionChildWorktree = Effect.fn("SubThreadWorkspace.provisionChildWorktree")(function* (input) {
138997
+ const gitWorkflow = yield* GitWorkflowService;
138998
+ return yield* Effect.gen(function* () {
138999
+ const baseCommitSha = yield* headCommit(input.parentCwd);
139000
+ if (baseCommitSha === null) return null;
139001
+ const parentBranch = yield* currentBranch(input.parentCwd);
139002
+ const parentDirty = yield* hasUncommittedChanges(input.parentCwd);
139003
+ const created = yield* gitWorkflow.createWorktree({
139004
+ cwd: input.repositoryRoot,
139005
+ refName: baseCommitSha,
139006
+ newRefName: input.branch,
139007
+ ...parentBranch !== null ? { baseRefName: parentBranch } : {},
139008
+ path: null
139009
+ });
139010
+ return {
139011
+ worktreePath: created.worktree.path,
139012
+ branch: created.worktree.refName,
139013
+ baseCommitSha,
139014
+ parentBranch,
139015
+ parentDirty
139016
+ };
139017
+ }).pipe(withRepositoryLock(input.repositoryRoot));
139018
+ });
139019
+ /** Undoes `provisionChildWorktree` when the thread it was made for never appeared. */
139020
+ const discardChildWorktree = Effect.fn("SubThreadWorkspace.discardChildWorktree")(function* (input) {
139021
+ yield* (yield* GitWorkflowService).removeWorktree({
139022
+ cwd: input.repositoryRoot,
139023
+ path: input.worktreePath,
139024
+ force: true
139025
+ }).pipe(Effect.ignore);
139026
+ yield* runGit("deleteBranch", input.repositoryRoot, [
139027
+ "branch",
139028
+ "-D",
139029
+ input.branch
139030
+ ]).pipe(Effect.ignore);
139031
+ });
139032
+ /**
139033
+ * Merges the child's branch into the parent's worktree. A conflict is aborted
139034
+ * so the parent stays clean, and the conflicted files come back for the child
139035
+ * to resolve on its own branch.
139036
+ */
139037
+ const mergeChildIntoParent = Effect.fn("SubThreadWorkspace.mergeChildIntoParent")(function* (input) {
139038
+ const childBranch = yield* currentBranch(input.childCwd);
139039
+ if (childBranch === null) return {
139040
+ outcome: "refused",
139041
+ detail: "The child worktree is on a detached HEAD."
139042
+ };
139043
+ if (yield* hasUncommittedChanges(input.childCwd)) return {
139044
+ outcome: "refused",
139045
+ detail: `The child has uncommitted changes on '${childBranch}'. Ask it to commit first.`
139046
+ };
139047
+ if (yield* hasUncommittedChanges(input.parentCwd)) return {
139048
+ outcome: "refused",
139049
+ detail: "The parent worktree has uncommitted changes. Commit or stash them before merging."
139050
+ };
139051
+ return yield* Effect.gen(function* () {
139052
+ const parentBranch = yield* currentBranch(input.parentCwd);
139053
+ const merge = yield* runGit("merge", input.parentCwd, [
139054
+ "merge",
139055
+ "--no-ff",
139056
+ "--no-edit",
139057
+ `refs/heads/${childBranch}`
139058
+ ]);
139059
+ if (merge.exitCode === 0) {
139060
+ const mergeCommitSha = (yield* headCommit(input.parentCwd)) ?? "";
139061
+ return {
139062
+ outcome: /already up[ -]to[ -]date/i.test(merge.stdout) ? "up-to-date" : "merged",
139063
+ childBranch,
139064
+ parentBranch,
139065
+ mergeCommitSha
139066
+ };
139067
+ }
139068
+ const conflicted = yield* runGit("conflictedFiles", input.parentCwd, [
139069
+ "diff",
139070
+ "--name-only",
139071
+ "--diff-filter=U"
139072
+ ]);
139073
+ yield* runGit("abortMerge", input.parentCwd, ["merge", "--abort"]);
139074
+ const conflictedFiles = conflicted.stdout.split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
139075
+ if (conflictedFiles.length === 0) return {
139076
+ outcome: "refused",
139077
+ detail: `git merge failed: ${merge.stderr.trim() || merge.stdout.trim()}`
139078
+ };
139079
+ return {
139080
+ outcome: "conflicted",
139081
+ childBranch,
139082
+ parentBranch,
139083
+ conflictedFiles
139084
+ };
139085
+ }).pipe(withRepositoryLock(input.repositoryRoot));
139086
+ });
139087
+ //#endregion
138154
139088
  //#region src/orchestration/fusionEvidenceReport.ts
138155
139089
  /**
138156
139090
  * Shared shape of one Fusion review's evidence rows.
@@ -138581,7 +139515,7 @@ const AskUserQuestionTool = Tool.make("ask_user_question", {
138581
139515
  ]
138582
139516
  }).annotate(Tool.Title, "Ask user question").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
138583
139517
  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.",
139518
+ 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
139519
  parameters: ThreadSpawnInput,
138586
139520
  success: ThreadSpawnResult,
138587
139521
  failure: ThreadControlToolError,
@@ -138590,8 +139524,11 @@ const ThreadSpawnTool = Tool.make("thread_spawn", {
138590
139524
  McpSessionRegistry,
138591
139525
  OrchestrationEngineService,
138592
139526
  ProjectionThreadRepository,
139527
+ ProjectionSnapshotQuery,
138593
139528
  ServerSettingsService,
138594
139529
  ProviderRegistry,
139530
+ GitWorkflowService,
139531
+ GitVcsDriver,
138595
139532
  Crypto.Crypto
138596
139533
  ]
138597
139534
  }).annotate(Tool.Title, "Start a thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
@@ -138648,6 +139585,7 @@ const ThreadCleanupTool = Tool.make("thread_cleanup", {
138648
139585
  ProjectionThreadRepository,
138649
139586
  ProjectionThreadSessionRepository,
138650
139587
  ThreadWorkspaceLifecycleService,
139588
+ TerminalManager,
138651
139589
  Crypto.Crypto
138652
139590
  ]
138653
139591
  }).annotate(Tool.Title, "Archive and clean up thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, true).annotate(Tool.Idempotent, true);
@@ -138736,7 +139674,44 @@ const ThreadContextUpdateTool = Tool.make("thread_context_update", {
138736
139674
  Crypto.Crypto
138737
139675
  ]
138738
139676
  }).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);
139677
+ const ThreadWaitTool = Tool.make("thread_wait", {
139678
+ 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.",
139679
+ parameters: ThreadWaitInput,
139680
+ success: ThreadWaitResult,
139681
+ failure: ThreadControlToolError,
139682
+ dependencies: [
139683
+ McpInvocationContext,
139684
+ OrchestrationEngineService,
139685
+ ProjectionSnapshotQuery,
139686
+ ThreadEventStreamService,
139687
+ ProjectionThreadMessageRepository
139688
+ ]
139689
+ }).annotate(Tool.Title, "Wait for sub-threads").annotate(Tool.Readonly, true).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, true);
139690
+ const ThreadMessageTool = Tool.make("thread_message", {
139691
+ 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.",
139692
+ parameters: ThreadMessageInput,
139693
+ success: ThreadMessageResult,
139694
+ failure: ThreadControlToolError,
139695
+ dependencies: [
139696
+ McpInvocationContext,
139697
+ OrchestrationEngineService,
139698
+ ProjectionSnapshotQuery,
139699
+ Crypto.Crypto
139700
+ ]
139701
+ }).annotate(Tool.Title, "Message a sub-thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
139702
+ const ThreadMergeChildTool = Tool.make("thread_merge_child", {
139703
+ 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.",
139704
+ parameters: ThreadMergeChildInput,
139705
+ success: ThreadMergeChildResult,
139706
+ failure: ThreadControlToolError,
139707
+ dependencies: [
139708
+ McpInvocationContext,
139709
+ ProjectionSnapshotQuery,
139710
+ GitWorkflowService,
139711
+ GitVcsDriver
139712
+ ]
139713
+ }).annotate(Tool.Title, "Merge a sub-thread").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
139714
+ const ThreadToolkit = Toolkit.make(ThreadContextReadTool, ThreadContextUpdateTool, ThreadPlanReadTool, ThreadPlanUpdateTool, AskUserQuestionTool, ThreadSpawnTool, ThreadWaitTool, ThreadMessageTool, ThreadMergeChildTool, ThreadPairCreateTool, ThreadPairDetachTool, ThreadConfigureTool, ThreadCleanupTool, ThreadSnoozeTool, ThreadRenameTool, MemoryAppendTool, AssetCompressTool);
138740
139715
  //#endregion
138741
139716
  //#region src/orchestration/pendingMcpUserInputs.ts
138742
139717
  /**
@@ -138830,6 +139805,15 @@ const dispatchControl = Effect.fn("mcp.threads.dispatchControl")(function* (comm
138830
139805
  detail: cause.message
138831
139806
  })));
138832
139807
  });
139808
+ /** Events that can change a sub-thread's wait state; streaming deltas cannot. */
139809
+ const SUB_THREAD_WAKE_EVENTS = /* @__PURE__ */ new Set([
139810
+ "thread.session-set",
139811
+ "thread.turn-completed",
139812
+ "thread.turn-start-requested",
139813
+ "thread.activity-appended",
139814
+ "thread.archived",
139815
+ "thread.deleted"
139816
+ ]);
138833
139817
  const newCommandId = Effect.gen(function* () {
138834
139818
  const crypto = yield* Crypto.Crypto;
138835
139819
  return CommandId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
@@ -139146,6 +140130,39 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139146
140130
  const unpromptedSubagents = input.unpromptedSubagents ?? template.unpromptedSubagents === 1;
139147
140131
  const threadId = ThreadId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
139148
140132
  const createdAt = DateTime.formatIso(yield* DateTime.now);
140133
+ const isFusionSpawn = input.fusion === true || input.fusionWatcher === true;
140134
+ if (isFusionSpawn && input.workspace === "worktree") return yield* new ThreadSpawnNotPermittedError({
140135
+ threadId: invocation.threadId,
140136
+ detail: "Fusion spawns share this thread's checkout; omit workspace or pass 'shared'."
140137
+ });
140138
+ let childWorkspace = null;
140139
+ if (!isFusionSpawn && input.workspace !== "shared" && !isChatProject(projectId)) {
140140
+ const project = yield* (yield* ProjectionSnapshotQuery).getProjectShellById(projectId).pipe(Effect.map(Option.getOrUndefined), Effect.mapError((cause) => new ThreadSpawnNotPermittedError({
140141
+ threadId: invocation.threadId,
140142
+ detail: `the project could not be read to create a worktree: ${cause.message}`
140143
+ })));
140144
+ if (project !== void 0) {
140145
+ const repositoryRoot = project.workspaceRoot;
140146
+ const parentCwd = projectId === template.projectId ? template.worktreePath ?? repositoryRoot : repositoryRoot;
140147
+ const hex = (yield* crypto.randomUUIDv4.pipe(Effect.orDie)).replaceAll("-", "");
140148
+ const provisioned = yield* provisionChildWorktree({
140149
+ repositoryRoot,
140150
+ parentCwd,
140151
+ branch: buildTemporaryWorktreeBranchName((bytes) => hex.slice(0, bytes * 2))
140152
+ }).pipe(Effect.mapError((cause) => new ThreadSpawnNotPermittedError({
140153
+ threadId: invocation.threadId,
140154
+ detail: `the sub-thread's worktree could not be created: ${cause.message}`
140155
+ })));
140156
+ if (provisioned !== null) childWorkspace = {
140157
+ ...provisioned,
140158
+ repositoryRoot
140159
+ };
140160
+ }
140161
+ if (childWorkspace === null && input.workspace === "worktree") return yield* new ThreadSpawnNotPermittedError({
140162
+ threadId: invocation.threadId,
140163
+ detail: "the project is not a git repository with a commit, so it cannot have worktrees"
140164
+ });
140165
+ }
139149
140166
  yield* dispatchControl({
139150
140167
  type: "thread.create",
139151
140168
  commandId: yield* newCommandId,
@@ -139157,10 +140174,11 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139157
140174
  interactionMode,
139158
140175
  compressMode,
139159
140176
  unpromptedSubagents,
139160
- branch: null,
139161
- worktreePath: null,
140177
+ branch: childWorkspace?.branch ?? null,
140178
+ worktreePath: childWorkspace?.worktreePath ?? null,
140179
+ ...isFusionSpawn || projectId !== template.projectId ? {} : { parentThreadId: invocation.threadId },
139162
140180
  createdAt
139163
- }, threadId);
140181
+ }, threadId).pipe(Effect.tapError(() => childWorkspace === null ? Effect.void : discardChildWorktree(childWorkspace)));
139164
140182
  yield* registry.recordSpawnedThread({
139165
140183
  providerSessionId: invocation.providerSessionId,
139166
140184
  threadId,
@@ -139229,7 +140247,13 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139229
140247
  threadId,
139230
140248
  projectId,
139231
140249
  title: input.title,
139232
- ...fusionPair
140250
+ ...fusionPair,
140251
+ ...childWorkspace === null ? {} : {
140252
+ branch: childWorkspace.branch,
140253
+ worktreePath: childWorkspace.worktreePath,
140254
+ baseCommitSha: childWorkspace.baseCommitSha,
140255
+ parentDirty: childWorkspace.parentDirty
140256
+ }
139233
140257
  };
139234
140258
  }),
139235
140259
  thread_pair_create: (input) => Effect.gen(function* () {
@@ -139486,6 +140510,11 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139486
140510
  })));
139487
140511
  const archivedThreadIds = result.events?.filter((event) => event.type === "thread.archived").map((event) => event.payload.threadId) ?? [];
139488
140512
  archivedThreadIds.forEach(queueThreadWorkspaceCleanup);
140513
+ const terminals = yield* TerminalManager;
140514
+ yield* Effect.forEach(archivedThreadIds, (archivedThreadId) => terminals.close({ threadId: archivedThreadId }).pipe(Effect.catch((error) => Effect.logWarning("failed to close thread terminals after archive", {
140515
+ threadId: archivedThreadId,
140516
+ error: error.message
140517
+ }))), { discard: true });
139489
140518
  const stopRequested = new Set(result.events?.filter((event) => event.type === "thread.session-stop-requested").map((event) => event.payload.threadId) ?? []);
139490
140519
  const session = yield* sessions.getByThreadId({ threadId }).pipe(Effect.mapError((cause) => new ThreadControlRejectedError({
139491
140520
  threadId,
@@ -139648,6 +140677,126 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
139648
140677
  estimatedTokensAfter: estimateTokens(compressed),
139649
140678
  warnings: validation.warnings
139650
140679
  };
140680
+ }),
140681
+ thread_wait: (input) => Effect.gen(function* () {
140682
+ const invocation = yield* requireThreadCapability();
140683
+ const threadIds = [...new Set(input.threadIds)];
140684
+ for (const threadId of threadIds) yield* requireThreadControlTarget(threadId);
140685
+ const query = yield* ProjectionSnapshotQuery;
140686
+ const engine = yield* OrchestrationEngineService;
140687
+ const eventStream = yield* ThreadEventStreamService;
140688
+ const messages = yield* ProjectionThreadMessageRepository;
140689
+ const mode = input.mode ?? "all";
140690
+ const rejected = (detail) => new ThreadControlRejectedError({
140691
+ threadId: invocation.threadId,
140692
+ commandType: "thread_wait",
140693
+ detail
140694
+ });
140695
+ const check = Effect.gen(function* () {
140696
+ const shells = yield* Effect.forEach(threadIds, (threadId) => query.getThreadShellById(threadId).pipe(Effect.map(Option.getOrUndefined)));
140697
+ const now = DateTime.formatIso(yield* DateTime.now);
140698
+ const states = shells.map((shell) => resolveSubThreadState(shell, now));
140699
+ return {
140700
+ shells,
140701
+ states,
140702
+ done: isSubThreadWaitSatisfied(states, mode)
140703
+ };
140704
+ }).pipe(Effect.mapError((cause) => rejected(cause.message)));
140705
+ const head = yield* engine.latestSequence;
140706
+ const initial = yield* check;
140707
+ const settled = initial.done ? Option.some(initial) : yield* Stream.mergeAll(threadIds.map((threadId) => eventStream.watch({
140708
+ threadId,
140709
+ afterSequence: head
140710
+ })), { 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));
140711
+ const final = Option.isSome(settled) ? settled.value : yield* check;
140712
+ const children = yield* Effect.forEach(threadIds, (threadId, index) => Effect.gen(function* () {
140713
+ const shell = final.shells[index];
140714
+ const assistantMessageId = shell?.latestTurn?.assistantMessageId ?? null;
140715
+ 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)));
140716
+ return {
140717
+ threadId,
140718
+ title: shell?.title ?? null,
140719
+ state: final.states[index] ?? "missing",
140720
+ branch: shell?.branch ?? null,
140721
+ worktreePath: shell?.worktreePath ?? null,
140722
+ lastAssistantMessage: reply === null ? null : reply.length > 1500 ? `${reply.slice(0, 1500)}…` : reply,
140723
+ lastError: shell?.session?.lastError ?? null
140724
+ };
140725
+ }));
140726
+ return {
140727
+ timedOut: Option.isNone(settled),
140728
+ children
140729
+ };
140730
+ }),
140731
+ thread_message: (input) => Effect.gen(function* () {
140732
+ const { invocation, threadId } = yield* requireThreadControlTarget(input.threadId);
140733
+ const rejected = (detail) => new ThreadControlRejectedError({
140734
+ threadId,
140735
+ commandType: "thread_message",
140736
+ detail
140737
+ });
140738
+ if (threadId === invocation.threadId) return yield* rejected("Message a sub-thread, not this thread.");
140739
+ const shell = yield* (yield* ProjectionSnapshotQuery).getThreadShellById(threadId).pipe(Effect.map(Option.getOrUndefined), Effect.mapError((cause) => rejected(cause.message)));
140740
+ if (shell === void 0) return yield* rejected("The sub-thread is archived or no longer exists.");
140741
+ const crypto = yield* Crypto.Crypto;
140742
+ const messageId = MessageId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
140743
+ yield* dispatchControl({
140744
+ type: "thread.turn.start",
140745
+ commandId: yield* newCommandId,
140746
+ threadId,
140747
+ message: {
140748
+ messageId,
140749
+ role: "user",
140750
+ text: input.text,
140751
+ attachments: []
140752
+ },
140753
+ modelSelection: shell.modelSelection,
140754
+ runtimeMode: shell.runtimeMode,
140755
+ interactionMode: shell.interactionMode,
140756
+ compressMode: shell.compressMode,
140757
+ unpromptedSubagents: shell.unpromptedSubagents,
140758
+ queueIfBusy: true,
140759
+ createdAt: DateTime.formatIso(yield* DateTime.now)
140760
+ }, threadId);
140761
+ return {
140762
+ threadId,
140763
+ messageId
140764
+ };
140765
+ }),
140766
+ thread_merge_child: (input) => Effect.gen(function* () {
140767
+ const { invocation, threadId } = yield* requireThreadControlTarget(input.threadId);
140768
+ const refused = (detail) => Effect.succeed({
140769
+ threadId,
140770
+ outcome: "refused",
140771
+ detail
140772
+ });
140773
+ const query = yield* ProjectionSnapshotQuery;
140774
+ const readShell = (id) => query.getThreadShellById(id).pipe(Effect.map(Option.getOrUndefined), Effect.mapError((cause) => new ThreadControlRejectedError({
140775
+ threadId,
140776
+ commandType: "thread_merge_child",
140777
+ detail: cause.message
140778
+ })));
140779
+ const child = yield* readShell(threadId);
140780
+ const parent = yield* readShell(invocation.threadId);
140781
+ if (child === void 0 || parent === void 0) return yield* refused("The sub-thread or this thread is archived or no longer exists.");
140782
+ if (child.parentThreadId !== invocation.threadId) return yield* refused("Only a sub-thread this thread spawned can be merged into it.");
140783
+ if (child.worktreePath === null) return yield* refused("The sub-thread shares this checkout, so there is nothing to merge.");
140784
+ const status = child.session?.status;
140785
+ if (status === "running" || status === "starting") return yield* refused("The sub-thread is still working. Wait for it with thread_wait.");
140786
+ const project = yield* query.getProjectShellById(child.projectId).pipe(Effect.map(Option.getOrUndefined), Effect.catch(() => Effect.succeed(void 0)));
140787
+ if (project === void 0) return yield* refused("The project could not be read.");
140788
+ return {
140789
+ threadId,
140790
+ ...yield* mergeChildIntoParent({
140791
+ repositoryRoot: project.workspaceRoot,
140792
+ parentCwd: parent.worktreePath ?? project.workspaceRoot,
140793
+ childCwd: child.worktreePath
140794
+ }).pipe(Effect.mapError((cause) => new ThreadControlRejectedError({
140795
+ threadId,
140796
+ commandType: "thread_merge_child",
140797
+ detail: cause.message
140798
+ })))
140799
+ };
139651
140800
  })
139652
140801
  });
139653
140802
  //#endregion
@@ -142904,12 +144053,30 @@ Layer.effect(RuntimeReceiptBus, makeRuntimeReceiptBusTest);
142904
144053
  * the scope closes. A finalizer shuts down the queue.
142905
144054
  *
142906
144055
  * @param process - The effect to run for each queued item.
144056
+ * @param mergeAdjacent - Optional pure merge of adjacent queued items. Returning
144057
+ * undefined preserves the next item as a processing boundary. No item waits
144058
+ * for another to arrive; each dequeue merges at most 64 already queued items.
142907
144059
  * @returns A `DrainableWorker` with `queue` and `drain`.
142908
144060
  */
142909
- const makeDrainableWorker = (process) => Effect.gen(function* () {
144061
+ const makeDrainableWorker = (process, mergeAdjacent) => Effect.gen(function* () {
142910
144062
  const queue = yield* Effect.acquireRelease(TxQueue.unbounded(), TxQueue.shutdown);
142911
144063
  const outstanding = yield* TxRef.make(0);
142912
- yield* TxQueue.take(queue).pipe(Effect.tap((a) => Effect.ensuring(process(a), TxRef.update(outstanding, (n) => n - 1))), Effect.forever, Effect.forkScoped);
144064
+ yield* Effect.gen(function* () {
144065
+ let item = yield* TxQueue.take(queue);
144066
+ let count = 1;
144067
+ if (mergeAdjacent !== void 0) while (count < 64 && !(yield* TxQueue.isEmpty(queue))) {
144068
+ const next = yield* TxQueue.peek(queue);
144069
+ const merged = mergeAdjacent(item, next);
144070
+ if (merged === void 0) break;
144071
+ yield* TxQueue.take(queue);
144072
+ item = merged;
144073
+ count += 1;
144074
+ }
144075
+ return {
144076
+ item,
144077
+ count
144078
+ };
144079
+ }).pipe(Effect.tx).pipe(Effect.tap(({ item, count }) => Effect.ensuring(process(item), TxRef.update(outstanding, (n) => n - count))), Effect.forever, Effect.forkScoped);
142913
144080
  const drain = TxRef.get(outstanding).pipe(Effect.tap((n) => n > 0 ? Effect.txRetry : Effect.void), Effect.tx);
142914
144081
  const enqueue = (element) => TxQueue.offer(queue, element).pipe(Effect.tap(() => TxRef.update(outstanding, (n) => n + 1)), Effect.tx);
142915
144082
  return {
@@ -142997,6 +144164,22 @@ const COMPRESS_MODE_BY_THREAD_CACHE_CAPACITY = 1e4;
142997
144164
  const COMPRESS_MODE_BY_THREAD_TTL = Duration.minutes(120);
142998
144165
  const MAX_BUFFERED_ASSISTANT_CHARS = 24e3;
142999
144166
  const STRICT_PROVIDER_LIFECYCLE_GUARD = process.env.P4CODE_STRICT_PROVIDER_LIFECYCLE_GUARD !== "0";
144167
+ function coalesceAssistantDeltas(left, right) {
144168
+ if (left.source !== "runtime" || right.source !== "runtime") return void 0;
144169
+ const a = left.event;
144170
+ const b = right.event;
144171
+ if (a.type !== "content.delta" || b.type !== "content.delta" || a.payload.streamKind !== "assistant_text" || b.payload.streamKind !== "assistant_text" || a.turnId === void 0 || a.itemId === void 0 || a.provider !== b.provider || a.providerInstanceId !== b.providerInstanceId || a.threadId !== b.threadId || a.turnId !== b.turnId || a.itemId !== b.itemId || a.requestId !== b.requestId || a.payload.contentIndex !== b.payload.contentIndex || a.payload.summaryIndex !== b.payload.summaryIndex || a.providerRefs?.providerTurnId !== b.providerRefs?.providerTurnId || a.providerRefs?.providerItemId !== b.providerRefs?.providerItemId || a.providerRefs?.providerRequestId !== b.providerRefs?.providerRequestId || a.payload.delta.length + b.payload.delta.length > MAX_BUFFERED_ASSISTANT_CHARS) return void 0;
144172
+ return {
144173
+ source: "runtime",
144174
+ event: {
144175
+ ...a,
144176
+ payload: {
144177
+ ...a.payload,
144178
+ delta: a.payload.delta + b.payload.delta
144179
+ }
144180
+ }
144181
+ };
144182
+ }
143000
144183
  function toTurnId$1(value) {
143001
144184
  return value === void 0 ? void 0 : TurnId.make(String(value));
143002
144185
  }
@@ -144035,7 +145218,7 @@ const make$9 = Effect.gen(function* () {
144035
145218
  cause: Cause.pretty(cause)
144036
145219
  });
144037
145220
  }));
144038
- const worker = yield* makeDrainableWorker(processInputSafely);
145221
+ const worker = yield* makeDrainableWorker(processInputSafely, coalesceAssistantDeltas);
144039
145222
  const start = () => Effect.gen(function* () {
144040
145223
  yield* Effect.forkScoped(Stream.runForEach(providerService.streamEvents, (event) => worker.enqueue({
144041
145224
  source: "runtime",