@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 +1275 -92
- package/dist/client/assets/{DiffPanel-DIcYfI9A.js → DiffPanel-Q2wpcGXf.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-CTIZxzEb.js → FilePreviewPanel-Cyst20U8.js} +2 -2
- package/dist/client/assets/{PreviewPanel-B6lxJLwo.js → PreviewPanel-D1Pve7sy.js} +2 -2
- package/dist/client/assets/{PullRequestCodeTab-6J4aC250.js → PullRequestCodeTab-LaUWqYLN.js} +2 -2
- package/dist/client/assets/{fileCommentAnnotations-TYSP_6yS.js → fileCommentAnnotations-BPbcgWjJ.js} +2 -2
- package/dist/client/assets/{index-Bzq9Yppm.js → index-CgIIi3gc.js} +255 -244
- package/dist/client/assets/{index-o7sBVQed.css → index-DRONsSry.css} +1 -1
- package/dist/client/assets/previewAssetResource-6mhfd1nG.js +48 -0
- package/dist/client/assets/{renderFileChildren-CnwjClLE.js → renderFileChildren-DsQhidLI.js} +2 -2
- package/dist/client/assets/{toggle-group-btEvGcuC.js → toggle-group-BY2-0xBw.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
- package/dist/client/assets/previewAssetResource-CgRbTpIo.js +0 -48
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.
|
|
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
|
|
36180
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
42379
|
-
|
|
42380
|
-
|
|
42381
|
-
|
|
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
|
-
|
|
42391
|
-
|
|
42392
|
-
|
|
42393
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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: (
|
|
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: (
|
|
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*
|
|
125168
|
-
|
|
125169
|
-
|
|
125170
|
-
|
|
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
|
|
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
|
|
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*
|
|
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",
|