@p4code/cli 0.2.11 → 0.2.12
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 +1260 -45
- package/dist/client/assets/{DiffPanel-cPwXdsgr.js → DiffPanel-Cc4tqqZT.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-C3qsh6o7.js → FilePreviewPanel-CqwCr4lx.js} +2 -2
- package/dist/client/assets/{PreviewPanel-CbUDx85o.js → PreviewPanel-DkrHhAa5.js} +2 -2
- package/dist/client/assets/{PullRequestCodeTab-DExVeZb2.js → PullRequestCodeTab-DolKx4TI.js} +2 -2
- package/dist/client/assets/arrow-right-B-VCk65x.js +2 -0
- package/dist/client/assets/{fileCommentAnnotations-DzV_o0S3.js → fileCommentAnnotations-Dkj57NZi.js} +2 -2
- package/dist/client/assets/index-BTY4oum9.css +1 -0
- package/dist/client/assets/index-DbfMC-eh.js +2417 -0
- package/dist/client/assets/previewAssetResource-aHb2X--j.js +47 -0
- package/dist/client/assets/{renderFileChildren-BX1mXwW6.js → renderFileChildren-BtU77ahV.js} +2 -2
- package/dist/client/assets/{toggle-group-YruS56ni.js → toggle-group-DE1Bw2SX.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
- package/dist/client/assets/arrow-right-BDr00byn.js +0 -2
- package/dist/client/assets/index-D37C-To4.js +0 -2408
- package/dist/client/assets/index-IMgZzHMh.css +0 -1
- package/dist/client/assets/previewAssetResource-DoobZMvx.js +0 -47
package/dist/bin.mjs
CHANGED
|
@@ -237,7 +237,7 @@ const make$89 = () => {
|
|
|
237
237
|
const layer$80 = Layer.sync(NetService, make$89);
|
|
238
238
|
//#endregion
|
|
239
239
|
//#region package.json
|
|
240
|
-
var version = "0.2.
|
|
240
|
+
var version = "0.2.12";
|
|
241
241
|
//#endregion
|
|
242
242
|
//#region src/config.ts
|
|
243
243
|
/**
|
|
@@ -1833,6 +1833,10 @@ const OrchestrationMessage = Schema$1.Struct({
|
|
|
1833
1833
|
});
|
|
1834
1834
|
const FUSION_REVIEW_PROMPT_PREFIX = "[fusion-review]";
|
|
1835
1835
|
const FUSION_NO_OBJECTION_TEXT = "[fusion-no-objection]";
|
|
1836
|
+
const FUSION_ADVICE_PROMPT_PREFIX = "[fusion-advice]";
|
|
1837
|
+
const FUSION_GATE_PROMPT_PREFIX = "[fusion-gate]";
|
|
1838
|
+
const FUSION_GATE_UNWATCHED_SUMMARY = "Gate passed unwatched";
|
|
1839
|
+
const FUSION_GATE_DEFAULT_TIMEOUT_MS = 12e4;
|
|
1836
1840
|
const OrchestrationProposedPlanId = TrimmedNonEmptyString;
|
|
1837
1841
|
const OrchestrationProposedPlan = Schema$1.Struct({
|
|
1838
1842
|
id: OrchestrationProposedPlanId,
|
|
@@ -1950,12 +1954,57 @@ const OrchestrationThread = Schema$1.Struct({
|
|
|
1950
1954
|
checkpoints: Schema$1.Array(OrchestrationCheckpointSummary),
|
|
1951
1955
|
session: Schema$1.NullOr(OrchestrationSession)
|
|
1952
1956
|
});
|
|
1957
|
+
const ThreadPairGateKind = Schema$1.Literals([
|
|
1958
|
+
"plan",
|
|
1959
|
+
"command-approval",
|
|
1960
|
+
"repeated-failure"
|
|
1961
|
+
]);
|
|
1962
|
+
/**
|
|
1963
|
+
* Where an open gate stands. `awaiting-watcher` is the only state the fail-open
|
|
1964
|
+
* timeout runs against: a watcher that never answers must not brick the run,
|
|
1965
|
+
* while an implementer revising or a human deciding may take as long as needed.
|
|
1966
|
+
*/
|
|
1967
|
+
const ThreadPairGateState = Schema$1.Literals([
|
|
1968
|
+
"awaiting-watcher",
|
|
1969
|
+
"awaiting-implementer",
|
|
1970
|
+
"escalated"
|
|
1971
|
+
]);
|
|
1972
|
+
const ThreadPairGateId = TrimmedNonEmptyString;
|
|
1973
|
+
const ThreadPairGateOutcome = Schema$1.Literals([
|
|
1974
|
+
"approved",
|
|
1975
|
+
"unwatched",
|
|
1976
|
+
"human",
|
|
1977
|
+
"cancelled"
|
|
1978
|
+
]);
|
|
1979
|
+
const ThreadPairGateResolver = Schema$1.Literals([
|
|
1980
|
+
"watcher",
|
|
1981
|
+
"timeout",
|
|
1982
|
+
"human",
|
|
1983
|
+
"system"
|
|
1984
|
+
]);
|
|
1985
|
+
const OrchestrationThreadPairGate = Schema$1.Struct({
|
|
1986
|
+
id: ThreadPairGateId,
|
|
1987
|
+
kind: ThreadPairGateKind,
|
|
1988
|
+
state: ThreadPairGateState,
|
|
1989
|
+
/** Watcher objections spent so far. The pair's roundCap bounds this. */
|
|
1990
|
+
round: NonNegativeInt,
|
|
1991
|
+
requestId: Schema$1.NullOr(ApprovalRequestId),
|
|
1992
|
+
planId: Schema$1.NullOr(OrchestrationProposedPlanId),
|
|
1993
|
+
commandTitle: Schema$1.NullOr(TrimmedNonEmptyString),
|
|
1994
|
+
openedAt: IsoDateTime,
|
|
1995
|
+
/** Set whenever the gate is waiting on the watcher; the timeout base. */
|
|
1996
|
+
awaitingWatcherSince: Schema$1.NullOr(IsoDateTime),
|
|
1997
|
+
updatedAt: IsoDateTime
|
|
1998
|
+
});
|
|
1953
1999
|
/** Persisted relationship between two otherwise ordinary threads. */
|
|
1954
2000
|
const OrchestrationThreadPair = Schema$1.Struct({
|
|
1955
2001
|
id: ThreadPairId,
|
|
1956
2002
|
implementerThreadId: ThreadId,
|
|
1957
2003
|
watcherThreadId: ThreadId,
|
|
1958
2004
|
lastReviewedImplementerSequence: NonNegativeInt,
|
|
2005
|
+
roundCap: NonNegativeInt.pipe(Schema$1.withDecodingDefault(Effect.succeed(2))),
|
|
2006
|
+
gateTimeoutMs: NonNegativeInt.pipe(Schema$1.withDecodingDefault(Effect.succeed(FUSION_GATE_DEFAULT_TIMEOUT_MS))),
|
|
2007
|
+
activeGate: Schema$1.NullOr(OrchestrationThreadPairGate).pipe(Schema$1.withDecodingDefault(Effect.succeed(null))),
|
|
1959
2008
|
createdAt: IsoDateTime,
|
|
1960
2009
|
detachedAt: Schema$1.NullOr(IsoDateTime)
|
|
1961
2010
|
});
|
|
@@ -2312,6 +2361,8 @@ const ThreadPairCreateCommand = Schema$1.Struct({
|
|
|
2312
2361
|
pairId: ThreadPairId,
|
|
2313
2362
|
implementerThreadId: ThreadId,
|
|
2314
2363
|
watcherThreadId: ThreadId,
|
|
2364
|
+
roundCap: Schema$1.optional(NonNegativeInt),
|
|
2365
|
+
gateTimeoutMs: Schema$1.optional(NonNegativeInt),
|
|
2315
2366
|
createdAt: IsoDateTime
|
|
2316
2367
|
});
|
|
2317
2368
|
const ThreadPairDetachCommand = Schema$1.Struct({
|
|
@@ -2320,6 +2371,20 @@ const ThreadPairDetachCommand = Schema$1.Struct({
|
|
|
2320
2371
|
pairId: ThreadPairId,
|
|
2321
2372
|
createdAt: IsoDateTime
|
|
2322
2373
|
});
|
|
2374
|
+
/**
|
|
2375
|
+
* Resolves the pair's open gate. Client-dispatchable because "Skip the gate"
|
|
2376
|
+
* and "Answer it yourself" are human overrides; the watcher's own resolutions
|
|
2377
|
+
* arrive through the advise toolkit, which dispatches this same command.
|
|
2378
|
+
*/
|
|
2379
|
+
const ThreadPairGateResolveCommand = Schema$1.Struct({
|
|
2380
|
+
type: Schema$1.Literal("thread-pair.gate.resolve"),
|
|
2381
|
+
commandId: CommandId,
|
|
2382
|
+
pairId: ThreadPairId,
|
|
2383
|
+
gateId: ThreadPairGateId,
|
|
2384
|
+
outcome: ThreadPairGateOutcome,
|
|
2385
|
+
resolvedBy: ThreadPairGateResolver,
|
|
2386
|
+
resolvedAt: IsoDateTime
|
|
2387
|
+
});
|
|
2323
2388
|
const DispatchableClientOrchestrationCommand = Schema$1.Union([
|
|
2324
2389
|
ProjectCreateCommand,
|
|
2325
2390
|
ProjectMetaUpdateCommand,
|
|
@@ -2344,7 +2409,8 @@ const DispatchableClientOrchestrationCommand = Schema$1.Union([
|
|
|
2344
2409
|
ThreadCheckpointRevertCommand,
|
|
2345
2410
|
ThreadSessionStopCommand,
|
|
2346
2411
|
ThreadPairCreateCommand,
|
|
2347
|
-
ThreadPairDetachCommand
|
|
2412
|
+
ThreadPairDetachCommand,
|
|
2413
|
+
ThreadPairGateResolveCommand
|
|
2348
2414
|
]);
|
|
2349
2415
|
const ClientOrchestrationCommand = Schema$1.Union([
|
|
2350
2416
|
ProjectCreateCommand,
|
|
@@ -2370,7 +2436,8 @@ const ClientOrchestrationCommand = Schema$1.Union([
|
|
|
2370
2436
|
ThreadCheckpointRevertCommand,
|
|
2371
2437
|
ThreadSessionStopCommand,
|
|
2372
2438
|
ThreadPairCreateCommand,
|
|
2373
|
-
ThreadPairDetachCommand
|
|
2439
|
+
ThreadPairDetachCommand,
|
|
2440
|
+
ThreadPairGateResolveCommand
|
|
2374
2441
|
]);
|
|
2375
2442
|
const ThreadSessionSetCommand = Schema$1.Struct({
|
|
2376
2443
|
type: Schema$1.Literal("thread.session.set"),
|
|
@@ -2399,6 +2466,31 @@ const ThreadPairCursorAdvanceCommand = Schema$1.Struct({
|
|
|
2399
2466
|
implementerSequence: NonNegativeInt,
|
|
2400
2467
|
advancedAt: IsoDateTime
|
|
2401
2468
|
});
|
|
2469
|
+
/** Server-only: gates are opened by detection reactors, never by clients. */
|
|
2470
|
+
const ThreadPairGateOpenCommand = Schema$1.Struct({
|
|
2471
|
+
type: Schema$1.Literal("thread-pair.gate.open"),
|
|
2472
|
+
commandId: CommandId,
|
|
2473
|
+
pairId: ThreadPairId,
|
|
2474
|
+
gateId: ThreadPairGateId,
|
|
2475
|
+
kind: ThreadPairGateKind,
|
|
2476
|
+
requestId: Schema$1.optional(ApprovalRequestId),
|
|
2477
|
+
planId: Schema$1.optional(OrchestrationProposedPlanId),
|
|
2478
|
+
commandTitle: Schema$1.optional(TrimmedNonEmptyString),
|
|
2479
|
+
openedAt: IsoDateTime
|
|
2480
|
+
});
|
|
2481
|
+
/** Server-only: moves the open gate between its states and counts rounds. */
|
|
2482
|
+
const ThreadPairGateAdvanceCommand = Schema$1.Struct({
|
|
2483
|
+
type: Schema$1.Literal("thread-pair.gate.advance"),
|
|
2484
|
+
commandId: CommandId,
|
|
2485
|
+
pairId: ThreadPairId,
|
|
2486
|
+
gateId: ThreadPairGateId,
|
|
2487
|
+
transition: Schema$1.Literals([
|
|
2488
|
+
"objection",
|
|
2489
|
+
"implementer-responded",
|
|
2490
|
+
"escalate"
|
|
2491
|
+
]),
|
|
2492
|
+
advancedAt: IsoDateTime
|
|
2493
|
+
});
|
|
2402
2494
|
const ThreadMessageAssistantDeltaCommand = Schema$1.Struct({
|
|
2403
2495
|
type: Schema$1.Literal("thread.message.assistant.delta"),
|
|
2404
2496
|
commandId: CommandId,
|
|
@@ -2454,6 +2546,8 @@ const InternalOrchestrationCommand = Schema$1.Union([
|
|
|
2454
2546
|
ThreadSessionSetCommand,
|
|
2455
2547
|
ThreadTurnCompleteCommand,
|
|
2456
2548
|
ThreadPairCursorAdvanceCommand,
|
|
2549
|
+
ThreadPairGateOpenCommand,
|
|
2550
|
+
ThreadPairGateAdvanceCommand,
|
|
2457
2551
|
ThreadMessageAssistantDeltaCommand,
|
|
2458
2552
|
ThreadMessageAssistantCompleteCommand,
|
|
2459
2553
|
ThreadProposedPlanUpsertCommand,
|
|
@@ -2494,7 +2588,10 @@ const OrchestrationEventType = Schema$1.Literals([
|
|
|
2494
2588
|
"thread.turn-completed",
|
|
2495
2589
|
"thread-pair.created",
|
|
2496
2590
|
"thread-pair.detached",
|
|
2497
|
-
"thread-pair.cursor-advanced"
|
|
2591
|
+
"thread-pair.cursor-advanced",
|
|
2592
|
+
"thread-pair.gate-opened",
|
|
2593
|
+
"thread-pair.gate-advanced",
|
|
2594
|
+
"thread-pair.gate-resolved"
|
|
2498
2595
|
]);
|
|
2499
2596
|
const OrchestrationAggregateKind = Schema$1.Literals([
|
|
2500
2597
|
"project",
|
|
@@ -2683,6 +2780,8 @@ const ThreadPairCreatedPayload$1 = Schema$1.Struct({
|
|
|
2683
2780
|
implementerThreadId: ThreadId,
|
|
2684
2781
|
watcherThreadId: ThreadId,
|
|
2685
2782
|
lastReviewedImplementerSequence: NonNegativeInt,
|
|
2783
|
+
roundCap: Schema$1.optional(NonNegativeInt),
|
|
2784
|
+
gateTimeoutMs: Schema$1.optional(NonNegativeInt),
|
|
2686
2785
|
createdAt: IsoDateTime
|
|
2687
2786
|
});
|
|
2688
2787
|
const ThreadPairDetachedPayload$1 = Schema$1.Struct({
|
|
@@ -2694,6 +2793,36 @@ const ThreadPairCursorAdvancedPayload$1 = Schema$1.Struct({
|
|
|
2694
2793
|
implementerSequence: NonNegativeInt,
|
|
2695
2794
|
advancedAt: IsoDateTime
|
|
2696
2795
|
});
|
|
2796
|
+
/**
|
|
2797
|
+
* Gate events carry both thread ids so reactors and clients can act on them
|
|
2798
|
+
* without a pair lookup - the wake, the activity fan-out and the gated pill
|
|
2799
|
+
* all key on the halves, not the pair id.
|
|
2800
|
+
*/
|
|
2801
|
+
const ThreadPairGateOpenedPayload$1 = Schema$1.Struct({
|
|
2802
|
+
pairId: ThreadPairId,
|
|
2803
|
+
implementerThreadId: ThreadId,
|
|
2804
|
+
watcherThreadId: ThreadId,
|
|
2805
|
+
gate: OrchestrationThreadPairGate,
|
|
2806
|
+
openedAt: IsoDateTime
|
|
2807
|
+
});
|
|
2808
|
+
const ThreadPairGateAdvancedPayload$1 = Schema$1.Struct({
|
|
2809
|
+
pairId: ThreadPairId,
|
|
2810
|
+
implementerThreadId: ThreadId,
|
|
2811
|
+
watcherThreadId: ThreadId,
|
|
2812
|
+
gate: OrchestrationThreadPairGate,
|
|
2813
|
+
advancedAt: IsoDateTime
|
|
2814
|
+
});
|
|
2815
|
+
const ThreadPairGateResolvedPayload$1 = Schema$1.Struct({
|
|
2816
|
+
pairId: ThreadPairId,
|
|
2817
|
+
implementerThreadId: ThreadId,
|
|
2818
|
+
watcherThreadId: ThreadId,
|
|
2819
|
+
gateId: ThreadPairGateId,
|
|
2820
|
+
kind: ThreadPairGateKind,
|
|
2821
|
+
requestId: Schema$1.NullOr(ApprovalRequestId),
|
|
2822
|
+
outcome: ThreadPairGateOutcome,
|
|
2823
|
+
resolvedBy: ThreadPairGateResolver,
|
|
2824
|
+
resolvedAt: IsoDateTime
|
|
2825
|
+
});
|
|
2697
2826
|
const ThreadProposedPlanUpsertedPayload$1 = Schema$1.Struct({
|
|
2698
2827
|
threadId: ThreadId,
|
|
2699
2828
|
proposedPlan: OrchestrationProposedPlan
|
|
@@ -2894,6 +3023,21 @@ const OrchestrationEvent = Schema$1.Union([
|
|
|
2894
3023
|
...EventBaseFields,
|
|
2895
3024
|
type: Schema$1.Literal("thread-pair.cursor-advanced"),
|
|
2896
3025
|
payload: ThreadPairCursorAdvancedPayload$1
|
|
3026
|
+
}),
|
|
3027
|
+
Schema$1.Struct({
|
|
3028
|
+
...EventBaseFields,
|
|
3029
|
+
type: Schema$1.Literal("thread-pair.gate-opened"),
|
|
3030
|
+
payload: ThreadPairGateOpenedPayload$1
|
|
3031
|
+
}),
|
|
3032
|
+
Schema$1.Struct({
|
|
3033
|
+
...EventBaseFields,
|
|
3034
|
+
type: Schema$1.Literal("thread-pair.gate-advanced"),
|
|
3035
|
+
payload: ThreadPairGateAdvancedPayload$1
|
|
3036
|
+
}),
|
|
3037
|
+
Schema$1.Struct({
|
|
3038
|
+
...EventBaseFields,
|
|
3039
|
+
type: Schema$1.Literal("thread-pair.gate-resolved"),
|
|
3040
|
+
payload: ThreadPairGateResolvedPayload$1
|
|
2897
3041
|
})
|
|
2898
3042
|
]);
|
|
2899
3043
|
const OrchestrationThreadStreamItem = Schema$1.Union([
|
|
@@ -9261,6 +9405,82 @@ var TrackerApiKeyError = class extends Schema$1.TaggedErrorClass()("TrackerApiKe
|
|
|
9261
9405
|
}
|
|
9262
9406
|
};
|
|
9263
9407
|
//#endregion
|
|
9408
|
+
//#region ../../packages/contracts/src/threadAdvise.ts
|
|
9409
|
+
/**
|
|
9410
|
+
* Thread advising over MCP - the cross-thread write surface behind the advise
|
|
9411
|
+
* capability.
|
|
9412
|
+
*
|
|
9413
|
+
* Watching (`threadWatch`) is read-only by design; this module is the change
|
|
9414
|
+
* that makes cross-thread access write-capable, which is why advise is a
|
|
9415
|
+
* separate capability rather than a wider watch. Read access never implies the
|
|
9416
|
+
* ability to steer: a session holding `watch` alone cannot call anything here,
|
|
9417
|
+
* and a session holding `advise` reaches exactly the threads in its explicit
|
|
9418
|
+
* `adviseThreadIds` grant - in practice the paired implementer and nothing
|
|
9419
|
+
* else. A request for any other thread fails closed.
|
|
9420
|
+
*
|
|
9421
|
+
* @module contracts/threadAdvise
|
|
9422
|
+
*/
|
|
9423
|
+
const ThreadAdviseInput = Schema$1.Struct({
|
|
9424
|
+
threadId: ThreadId.annotate({ description: "The paired builder thread to advise. Must be explicitly granted." }),
|
|
9425
|
+
message: TrimmedNonEmptyString.annotate({ description: "The advice to post into the builder thread. The builder sees it as supervisor advice and is expected to respond." }),
|
|
9426
|
+
interrupt: Schema$1.optional(Schema$1.Boolean.annotate({ description: "Cancel the builder's running turn before posting, so the advice is read now rather than at the next boundary. Use only when continuing would waste or damage work." }))
|
|
9427
|
+
});
|
|
9428
|
+
const ThreadAdviseResult = Schema$1.Struct({
|
|
9429
|
+
threadId: ThreadId,
|
|
9430
|
+
messageId: MessageId,
|
|
9431
|
+
interrupted: Schema$1.Boolean
|
|
9432
|
+
});
|
|
9433
|
+
const ThreadGateRespondInput = Schema$1.Struct({
|
|
9434
|
+
threadId: ThreadId.annotate({ description: "The paired builder thread whose open gate this answers." }),
|
|
9435
|
+
gateId: ThreadPairGateId.annotate({ description: "The open gate's id, given in the gate review prompt." }),
|
|
9436
|
+
decision: Schema$1.Literals(["approve", "object"]).annotate({ description: "approve clears the gate and lets the builder proceed. object sends the message to the builder as an objection and spends one exchange round; when the rounds are used up the gate escalates to the human instead." }),
|
|
9437
|
+
message: Schema$1.optional(TrimmedNonEmptyString.annotate({ description: "The objection to deliver. Required when decision is object." }))
|
|
9438
|
+
});
|
|
9439
|
+
const ThreadGateRespondResult = Schema$1.Struct({
|
|
9440
|
+
threadId: ThreadId,
|
|
9441
|
+
gateId: ThreadPairGateId,
|
|
9442
|
+
kind: ThreadPairGateKind,
|
|
9443
|
+
outcome: Schema$1.Literals([
|
|
9444
|
+
"approved",
|
|
9445
|
+
"objection-sent",
|
|
9446
|
+
"escalated"
|
|
9447
|
+
])
|
|
9448
|
+
});
|
|
9449
|
+
var AdviseToolUnavailableError = class extends Schema$1.TaggedErrorClass()("AdviseToolUnavailableError", {
|
|
9450
|
+
capability: Schema$1.Literal("advise"),
|
|
9451
|
+
environmentId: EnvironmentId,
|
|
9452
|
+
threadId: ThreadId,
|
|
9453
|
+
providerSessionId: TrimmedNonEmptyString,
|
|
9454
|
+
providerInstanceId: ProviderInstanceId
|
|
9455
|
+
}) {
|
|
9456
|
+
get message() {
|
|
9457
|
+
return `MCP credential does not grant the ${this.capability} capability.`;
|
|
9458
|
+
}
|
|
9459
|
+
};
|
|
9460
|
+
/**
|
|
9461
|
+
* The capability is present but the requested thread is not in the granted
|
|
9462
|
+
* set - the same split `threadWatch` makes, so an adviser can tell "you
|
|
9463
|
+
* cannot advise at all" apart from "not this thread".
|
|
9464
|
+
*/
|
|
9465
|
+
var ThreadAdviseNotPermittedError = class extends Schema$1.TaggedErrorClass()("ThreadAdviseNotPermittedError", { threadId: ThreadId }) {
|
|
9466
|
+
get message() {
|
|
9467
|
+
return `This session was not granted advise access to thread ${this.threadId}.`;
|
|
9468
|
+
}
|
|
9469
|
+
};
|
|
9470
|
+
var ThreadAdviseFailedError = class extends Schema$1.TaggedErrorClass()("ThreadAdviseFailedError", {
|
|
9471
|
+
threadId: ThreadId,
|
|
9472
|
+
detail: Schema$1.String
|
|
9473
|
+
}) {
|
|
9474
|
+
get message() {
|
|
9475
|
+
return `Advising thread ${this.threadId} failed: ${this.detail}`;
|
|
9476
|
+
}
|
|
9477
|
+
};
|
|
9478
|
+
const ThreadAdviseToolError = Schema$1.Union([
|
|
9479
|
+
AdviseToolUnavailableError,
|
|
9480
|
+
ThreadAdviseNotPermittedError,
|
|
9481
|
+
ThreadAdviseFailedError
|
|
9482
|
+
]);
|
|
9483
|
+
//#endregion
|
|
9264
9484
|
//#region ../../packages/contracts/src/threadControl.ts
|
|
9265
9485
|
/**
|
|
9266
9486
|
* Thread control over MCP - the toolkit an agent uses to act on its own thread
|
|
@@ -9332,7 +9552,10 @@ const ThreadSpawnResult = Schema$1.Struct({
|
|
|
9332
9552
|
projectId: ProjectId,
|
|
9333
9553
|
title: TrimmedNonEmptyString
|
|
9334
9554
|
});
|
|
9335
|
-
const ThreadPairCreateInput = Schema$1.Struct({
|
|
9555
|
+
const ThreadPairCreateInput = Schema$1.Struct({
|
|
9556
|
+
watcherThreadId: ThreadId.annotate({ description: "The supervisor thread to pair with this session's own builder thread. It must have been created by this session through thread_spawn." }),
|
|
9557
|
+
gateTimeoutMs: Schema$1.optional(NonNegativeInt.annotate({ description: "How long a gate waits on the supervisor before failing open, in milliseconds. Defaults to 120000." }))
|
|
9558
|
+
});
|
|
9336
9559
|
const ThreadPairCreateResult = Schema$1.Struct({
|
|
9337
9560
|
pairId: ThreadPairId,
|
|
9338
9561
|
implementerThreadId: ThreadId,
|
|
@@ -15176,6 +15399,29 @@ var _046_ThreadPairs_default = Effect.gen(function* () {
|
|
|
15176
15399
|
`;
|
|
15177
15400
|
});
|
|
15178
15401
|
//#endregion
|
|
15402
|
+
//#region src/persistence/Migrations/047_ThreadPairGates.ts
|
|
15403
|
+
/**
|
|
15404
|
+
* Fusion second cut: per-pair gate configuration and the persisted open gate.
|
|
15405
|
+
*
|
|
15406
|
+
* The open gate lives in a JSON column rather than its own table because a
|
|
15407
|
+
* pair holds at most one open gate at a time and gate history already lives in
|
|
15408
|
+
* the event log. Persisting it here is what makes the fail-open timeout
|
|
15409
|
+
* enforceable across a server restart: the sweeper reads this row, not any
|
|
15410
|
+
* in-memory approval state.
|
|
15411
|
+
*/
|
|
15412
|
+
var _047_ThreadPairGates_default = Effect.gen(function* () {
|
|
15413
|
+
const sql = yield* SqlClient.SqlClient;
|
|
15414
|
+
yield* sql`
|
|
15415
|
+
ALTER TABLE thread_pairs ADD COLUMN round_cap INTEGER NOT NULL DEFAULT 2
|
|
15416
|
+
`;
|
|
15417
|
+
yield* sql`
|
|
15418
|
+
ALTER TABLE thread_pairs ADD COLUMN gate_timeout_ms INTEGER NOT NULL DEFAULT 120000
|
|
15419
|
+
`;
|
|
15420
|
+
yield* sql`
|
|
15421
|
+
ALTER TABLE thread_pairs ADD COLUMN active_gate_json TEXT
|
|
15422
|
+
`;
|
|
15423
|
+
});
|
|
15424
|
+
//#endregion
|
|
15179
15425
|
//#region src/persistence/Migrations.ts
|
|
15180
15426
|
/**
|
|
15181
15427
|
* MigrationsLive - Migration runner with inline loader
|
|
@@ -15426,6 +15672,11 @@ const migrationEntries = [
|
|
|
15426
15672
|
46,
|
|
15427
15673
|
"ThreadPairs",
|
|
15428
15674
|
_046_ThreadPairs_default
|
|
15675
|
+
],
|
|
15676
|
+
[
|
|
15677
|
+
47,
|
|
15678
|
+
"ThreadPairGates",
|
|
15679
|
+
_047_ThreadPairGates_default
|
|
15429
15680
|
]
|
|
15430
15681
|
];
|
|
15431
15682
|
const makeMigrationLoader = (throughId) => Migrator.fromRecord(Object.fromEntries(migrationEntries.filter(([id]) => throughId === void 0 || id <= throughId).map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -18094,6 +18345,51 @@ var _009_Feed_default = Effect.gen(function* () {
|
|
|
18094
18345
|
VALUES (${id}, ${name}, ${kind}, ${feedUrl}, ${siteUrl}, 1, ${now}, ${now})`;
|
|
18095
18346
|
});
|
|
18096
18347
|
//#endregion
|
|
18348
|
+
//#region src/hub/Migrations/010_DefaultFeedSources.ts
|
|
18349
|
+
var _010_DefaultFeedSources_default = Effect.gen(function* () {
|
|
18350
|
+
const sql = yield* SqlClient.SqlClient;
|
|
18351
|
+
const now = DateTime.formatIso(yield* DateTime.now);
|
|
18352
|
+
for (const [id, name, kind, feedUrl, siteUrl] of [
|
|
18353
|
+
[
|
|
18354
|
+
"hacker-news",
|
|
18355
|
+
"Hacker News",
|
|
18356
|
+
"hacker_news",
|
|
18357
|
+
"https://hnrss.org/frontpage",
|
|
18358
|
+
"https://news.ycombinator.com"
|
|
18359
|
+
],
|
|
18360
|
+
[
|
|
18361
|
+
"techcrunch",
|
|
18362
|
+
"TechCrunch",
|
|
18363
|
+
"syndication",
|
|
18364
|
+
"https://techcrunch.com/feed/",
|
|
18365
|
+
"https://techcrunch.com"
|
|
18366
|
+
],
|
|
18367
|
+
[
|
|
18368
|
+
"the-verge",
|
|
18369
|
+
"The Verge",
|
|
18370
|
+
"syndication",
|
|
18371
|
+
"https://www.theverge.com/rss/index.xml",
|
|
18372
|
+
"https://www.theverge.com"
|
|
18373
|
+
],
|
|
18374
|
+
[
|
|
18375
|
+
"ars-technica",
|
|
18376
|
+
"Ars Technica",
|
|
18377
|
+
"syndication",
|
|
18378
|
+
"https://feeds.arstechnica.com/arstechnica/index",
|
|
18379
|
+
"https://arstechnica.com"
|
|
18380
|
+
],
|
|
18381
|
+
[
|
|
18382
|
+
"wired",
|
|
18383
|
+
"Wired",
|
|
18384
|
+
"syndication",
|
|
18385
|
+
"https://www.wired.com/feed/rss",
|
|
18386
|
+
"https://www.wired.com"
|
|
18387
|
+
]
|
|
18388
|
+
]) yield* sql`INSERT OR IGNORE INTO feed_sources
|
|
18389
|
+
(id, name, kind, feed_url, site_url, enabled, created_at, updated_at)
|
|
18390
|
+
VALUES (${id}, ${name}, ${kind}, ${feedUrl}, ${siteUrl}, 1, ${now}, ${now})`;
|
|
18391
|
+
});
|
|
18392
|
+
//#endregion
|
|
18097
18393
|
//#region src/hub/Migrations.ts
|
|
18098
18394
|
/**
|
|
18099
18395
|
* Hub migrations.
|
|
@@ -18149,6 +18445,11 @@ const hubMigrationEntries = [
|
|
|
18149
18445
|
9,
|
|
18150
18446
|
"Feed",
|
|
18151
18447
|
_009_Feed_default
|
|
18448
|
+
],
|
|
18449
|
+
[
|
|
18450
|
+
10,
|
|
18451
|
+
"DefaultFeedSources",
|
|
18452
|
+
_010_DefaultFeedSources_default
|
|
18152
18453
|
]
|
|
18153
18454
|
];
|
|
18154
18455
|
const hubMigrationLoader = Migrator.fromRecord(Object.fromEntries(hubMigrationEntries.map(([id, name, migration]) => [`${id}_${name}`, migration])));
|
|
@@ -22889,6 +23190,9 @@ const ThreadActivityAppendedPayload = ThreadActivityAppendedPayload$1;
|
|
|
22889
23190
|
const ThreadPairCreatedPayload = ThreadPairCreatedPayload$1;
|
|
22890
23191
|
const ThreadPairDetachedPayload = ThreadPairDetachedPayload$1;
|
|
22891
23192
|
const ThreadPairCursorAdvancedPayload = ThreadPairCursorAdvancedPayload$1;
|
|
23193
|
+
const ThreadPairGateOpenedPayload = ThreadPairGateOpenedPayload$1;
|
|
23194
|
+
const ThreadPairGateAdvancedPayload = ThreadPairGateAdvancedPayload$1;
|
|
23195
|
+
const ThreadPairGateResolvedPayload = ThreadPairGateResolvedPayload$1;
|
|
22892
23196
|
//#endregion
|
|
22893
23197
|
//#region src/orchestration/projector.ts
|
|
22894
23198
|
function checkpointStatusToLatestTurnState(status) {
|
|
@@ -22980,6 +23284,9 @@ function projectEvent(model, event) {
|
|
|
22980
23284
|
implementerThreadId: payload.implementerThreadId,
|
|
22981
23285
|
watcherThreadId: payload.watcherThreadId,
|
|
22982
23286
|
lastReviewedImplementerSequence: payload.lastReviewedImplementerSequence,
|
|
23287
|
+
roundCap: payload.roundCap ?? 2,
|
|
23288
|
+
gateTimeoutMs: payload.gateTimeoutMs ?? 12e4,
|
|
23289
|
+
activeGate: null,
|
|
22983
23290
|
createdAt: payload.createdAt,
|
|
22984
23291
|
detachedAt: null
|
|
22985
23292
|
}]
|
|
@@ -22998,6 +23305,27 @@ function projectEvent(model, event) {
|
|
|
22998
23305
|
lastReviewedImplementerSequence: payload.implementerSequence
|
|
22999
23306
|
} : pair)
|
|
23000
23307
|
})));
|
|
23308
|
+
case "thread-pair.gate-opened": return decodeForEvent(ThreadPairGateOpenedPayload, event.payload, event.type, "payload").pipe(Effect.map((payload) => ({
|
|
23309
|
+
...nextBase,
|
|
23310
|
+
threadPairs: (nextBase.threadPairs ?? []).map((pair) => pair.id === payload.pairId ? {
|
|
23311
|
+
...pair,
|
|
23312
|
+
activeGate: payload.gate
|
|
23313
|
+
} : pair)
|
|
23314
|
+
})));
|
|
23315
|
+
case "thread-pair.gate-advanced": return decodeForEvent(ThreadPairGateAdvancedPayload, event.payload, event.type, "payload").pipe(Effect.map((payload) => ({
|
|
23316
|
+
...nextBase,
|
|
23317
|
+
threadPairs: (nextBase.threadPairs ?? []).map((pair) => pair.id === payload.pairId ? {
|
|
23318
|
+
...pair,
|
|
23319
|
+
activeGate: payload.gate
|
|
23320
|
+
} : pair)
|
|
23321
|
+
})));
|
|
23322
|
+
case "thread-pair.gate-resolved": return decodeForEvent(ThreadPairGateResolvedPayload, event.payload, event.type, "payload").pipe(Effect.map((payload) => ({
|
|
23323
|
+
...nextBase,
|
|
23324
|
+
threadPairs: (nextBase.threadPairs ?? []).map((pair) => pair.id === payload.pairId ? {
|
|
23325
|
+
...pair,
|
|
23326
|
+
activeGate: null
|
|
23327
|
+
} : pair)
|
|
23328
|
+
})));
|
|
23001
23329
|
case "project.created": return decodeForEvent(ProjectCreatedPayload, event.payload, event.type, "payload").pipe(Effect.map((payload) => {
|
|
23002
23330
|
const existing = nextBase.projects.find((entry) => entry.id === payload.projectId);
|
|
23003
23331
|
const nextProject = {
|
|
@@ -23604,6 +23932,8 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
|
|
|
23604
23932
|
implementerThreadId: command.implementerThreadId,
|
|
23605
23933
|
watcherThreadId: command.watcherThreadId,
|
|
23606
23934
|
lastReviewedImplementerSequence: readModel.snapshotSequence,
|
|
23935
|
+
...command.roundCap !== void 0 ? { roundCap: command.roundCap } : {},
|
|
23936
|
+
...command.gateTimeoutMs !== void 0 ? { gateTimeoutMs: command.gateTimeoutMs } : {},
|
|
23607
23937
|
createdAt: command.createdAt
|
|
23608
23938
|
}
|
|
23609
23939
|
};
|
|
@@ -23674,6 +24004,118 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
|
|
|
23674
24004
|
}
|
|
23675
24005
|
};
|
|
23676
24006
|
}
|
|
24007
|
+
case "thread-pair.gate.open": {
|
|
24008
|
+
const pair = (readModel.threadPairs ?? []).find((candidate) => candidate.id === command.pairId);
|
|
24009
|
+
if (pair === void 0 || pair.detachedAt !== null) return yield* new OrchestrationCommandInvariantError({
|
|
24010
|
+
commandType: command.type,
|
|
24011
|
+
detail: `Active thread pair '${command.pairId}' does not exist.`
|
|
24012
|
+
});
|
|
24013
|
+
if (pair.activeGate !== null) return yield* new OrchestrationCommandInvariantError({
|
|
24014
|
+
commandType: command.type,
|
|
24015
|
+
detail: `Thread pair '${command.pairId}' already has an open gate.`
|
|
24016
|
+
});
|
|
24017
|
+
return {
|
|
24018
|
+
...yield* withEventBase({
|
|
24019
|
+
aggregateKind: "thread-pair",
|
|
24020
|
+
aggregateId: command.pairId,
|
|
24021
|
+
occurredAt: command.openedAt,
|
|
24022
|
+
commandId: command.commandId
|
|
24023
|
+
}),
|
|
24024
|
+
type: "thread-pair.gate-opened",
|
|
24025
|
+
payload: {
|
|
24026
|
+
pairId: command.pairId,
|
|
24027
|
+
implementerThreadId: pair.implementerThreadId,
|
|
24028
|
+
watcherThreadId: pair.watcherThreadId,
|
|
24029
|
+
gate: {
|
|
24030
|
+
id: command.gateId,
|
|
24031
|
+
kind: command.kind,
|
|
24032
|
+
state: "awaiting-watcher",
|
|
24033
|
+
round: 0,
|
|
24034
|
+
requestId: command.requestId ?? null,
|
|
24035
|
+
planId: command.planId ?? null,
|
|
24036
|
+
commandTitle: command.commandTitle ?? null,
|
|
24037
|
+
openedAt: command.openedAt,
|
|
24038
|
+
awaitingWatcherSince: command.openedAt,
|
|
24039
|
+
updatedAt: command.openedAt
|
|
24040
|
+
},
|
|
24041
|
+
openedAt: command.openedAt
|
|
24042
|
+
}
|
|
24043
|
+
};
|
|
24044
|
+
}
|
|
24045
|
+
case "thread-pair.gate.advance": {
|
|
24046
|
+
const pair = (readModel.threadPairs ?? []).find((candidate) => candidate.id === command.pairId);
|
|
24047
|
+
const gate = pair?.detachedAt === null ? pair.activeGate : null;
|
|
24048
|
+
if (pair === void 0 || gate === null || gate === void 0 || gate.id !== command.gateId) return yield* new OrchestrationCommandInvariantError({
|
|
24049
|
+
commandType: command.type,
|
|
24050
|
+
detail: `Open gate '${command.gateId}' does not exist on thread pair '${command.pairId}'.`
|
|
24051
|
+
});
|
|
24052
|
+
const transition = command.transition;
|
|
24053
|
+
if (transition === "objection" && gate.state !== "awaiting-watcher" || transition === "implementer-responded" && gate.state !== "awaiting-implementer" || transition === "escalate" && gate.state === "escalated") return yield* new OrchestrationCommandInvariantError({
|
|
24054
|
+
commandType: command.type,
|
|
24055
|
+
detail: `Gate '${command.gateId}' in state '${gate.state}' cannot take transition '${transition}'.`
|
|
24056
|
+
});
|
|
24057
|
+
const nextGate = transition === "objection" ? {
|
|
24058
|
+
...gate,
|
|
24059
|
+
state: "awaiting-implementer",
|
|
24060
|
+
round: gate.round + 1,
|
|
24061
|
+
awaitingWatcherSince: null,
|
|
24062
|
+
updatedAt: command.advancedAt
|
|
24063
|
+
} : transition === "implementer-responded" ? {
|
|
24064
|
+
...gate,
|
|
24065
|
+
state: "awaiting-watcher",
|
|
24066
|
+
awaitingWatcherSince: command.advancedAt,
|
|
24067
|
+
updatedAt: command.advancedAt
|
|
24068
|
+
} : {
|
|
24069
|
+
...gate,
|
|
24070
|
+
state: "escalated",
|
|
24071
|
+
awaitingWatcherSince: null,
|
|
24072
|
+
updatedAt: command.advancedAt
|
|
24073
|
+
};
|
|
24074
|
+
return {
|
|
24075
|
+
...yield* withEventBase({
|
|
24076
|
+
aggregateKind: "thread-pair",
|
|
24077
|
+
aggregateId: command.pairId,
|
|
24078
|
+
occurredAt: command.advancedAt,
|
|
24079
|
+
commandId: command.commandId
|
|
24080
|
+
}),
|
|
24081
|
+
type: "thread-pair.gate-advanced",
|
|
24082
|
+
payload: {
|
|
24083
|
+
pairId: command.pairId,
|
|
24084
|
+
implementerThreadId: pair.implementerThreadId,
|
|
24085
|
+
watcherThreadId: pair.watcherThreadId,
|
|
24086
|
+
gate: nextGate,
|
|
24087
|
+
advancedAt: command.advancedAt
|
|
24088
|
+
}
|
|
24089
|
+
};
|
|
24090
|
+
}
|
|
24091
|
+
case "thread-pair.gate.resolve": {
|
|
24092
|
+
const pair = (readModel.threadPairs ?? []).find((candidate) => candidate.id === command.pairId);
|
|
24093
|
+
const gate = pair?.detachedAt === null ? pair.activeGate : null;
|
|
24094
|
+
if (pair === void 0 || gate === null || gate === void 0 || gate.id !== command.gateId) return yield* new OrchestrationCommandInvariantError({
|
|
24095
|
+
commandType: command.type,
|
|
24096
|
+
detail: `Open gate '${command.gateId}' does not exist on thread pair '${command.pairId}'.`
|
|
24097
|
+
});
|
|
24098
|
+
return {
|
|
24099
|
+
...yield* withEventBase({
|
|
24100
|
+
aggregateKind: "thread-pair",
|
|
24101
|
+
aggregateId: command.pairId,
|
|
24102
|
+
occurredAt: command.resolvedAt,
|
|
24103
|
+
commandId: command.commandId
|
|
24104
|
+
}),
|
|
24105
|
+
type: "thread-pair.gate-resolved",
|
|
24106
|
+
payload: {
|
|
24107
|
+
pairId: command.pairId,
|
|
24108
|
+
implementerThreadId: pair.implementerThreadId,
|
|
24109
|
+
watcherThreadId: pair.watcherThreadId,
|
|
24110
|
+
gateId: gate.id,
|
|
24111
|
+
kind: gate.kind,
|
|
24112
|
+
requestId: gate.requestId,
|
|
24113
|
+
outcome: command.outcome,
|
|
24114
|
+
resolvedBy: command.resolvedBy,
|
|
24115
|
+
resolvedAt: command.resolvedAt
|
|
24116
|
+
}
|
|
24117
|
+
};
|
|
24118
|
+
}
|
|
23677
24119
|
case "thread.archive": {
|
|
23678
24120
|
yield* requireThreadNotArchived({
|
|
23679
24121
|
readModel,
|
|
@@ -24366,7 +24808,10 @@ function commandToAggregateRef(command) {
|
|
|
24366
24808
|
};
|
|
24367
24809
|
case "thread-pair.create":
|
|
24368
24810
|
case "thread-pair.detach":
|
|
24369
|
-
case "thread-pair.cursor.advance":
|
|
24811
|
+
case "thread-pair.cursor.advance":
|
|
24812
|
+
case "thread-pair.gate.open":
|
|
24813
|
+
case "thread-pair.gate.advance":
|
|
24814
|
+
case "thread-pair.gate.resolve": return {
|
|
24370
24815
|
aggregateKind: "thread-pair",
|
|
24371
24816
|
aggregateId: command.pairId
|
|
24372
24817
|
};
|
|
@@ -26285,6 +26730,9 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
|
|
|
26285
26730
|
implementer_thread_id,
|
|
26286
26731
|
watcher_thread_id,
|
|
26287
26732
|
last_reviewed_implementer_sequence,
|
|
26733
|
+
round_cap,
|
|
26734
|
+
gate_timeout_ms,
|
|
26735
|
+
active_gate_json,
|
|
26288
26736
|
created_at,
|
|
26289
26737
|
detached_at
|
|
26290
26738
|
) VALUES (
|
|
@@ -26292,6 +26740,9 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
|
|
|
26292
26740
|
${event.payload.implementerThreadId},
|
|
26293
26741
|
${event.payload.watcherThreadId},
|
|
26294
26742
|
${event.payload.lastReviewedImplementerSequence},
|
|
26743
|
+
${event.payload.roundCap ?? 2},
|
|
26744
|
+
${event.payload.gateTimeoutMs ?? 12e4},
|
|
26745
|
+
NULL,
|
|
26295
26746
|
${event.payload.createdAt},
|
|
26296
26747
|
NULL
|
|
26297
26748
|
)
|
|
@@ -26299,10 +26750,28 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
|
|
|
26299
26750
|
implementer_thread_id = excluded.implementer_thread_id,
|
|
26300
26751
|
watcher_thread_id = excluded.watcher_thread_id,
|
|
26301
26752
|
last_reviewed_implementer_sequence = excluded.last_reviewed_implementer_sequence,
|
|
26753
|
+
round_cap = excluded.round_cap,
|
|
26754
|
+
gate_timeout_ms = excluded.gate_timeout_ms,
|
|
26755
|
+
active_gate_json = NULL,
|
|
26302
26756
|
created_at = excluded.created_at,
|
|
26303
26757
|
detached_at = NULL
|
|
26304
26758
|
`.pipe(Effect.mapError(toPersistenceSqlError("ProjectionPipeline.threadPairs:create")));
|
|
26305
26759
|
return;
|
|
26760
|
+
case "thread-pair.gate-opened":
|
|
26761
|
+
case "thread-pair.gate-advanced":
|
|
26762
|
+
yield* sql`
|
|
26763
|
+
UPDATE thread_pairs
|
|
26764
|
+
SET active_gate_json = ${JSON.stringify(event.payload.gate)}
|
|
26765
|
+
WHERE pair_id = ${event.payload.pairId}
|
|
26766
|
+
`.pipe(Effect.mapError(toPersistenceSqlError("ProjectionPipeline.threadPairs:gate")));
|
|
26767
|
+
return;
|
|
26768
|
+
case "thread-pair.gate-resolved":
|
|
26769
|
+
yield* sql`
|
|
26770
|
+
UPDATE thread_pairs
|
|
26771
|
+
SET active_gate_json = NULL
|
|
26772
|
+
WHERE pair_id = ${event.payload.pairId}
|
|
26773
|
+
`.pipe(Effect.mapError(toPersistenceSqlError("ProjectionPipeline.threadPairs:gateResolve")));
|
|
26774
|
+
return;
|
|
26306
26775
|
case "thread-pair.detached":
|
|
26307
26776
|
yield* sql`
|
|
26308
26777
|
UPDATE thread_pairs
|
|
@@ -27714,6 +28183,9 @@ const ProjectionThreadPairDbRowSchema = Schema$1.Struct({
|
|
|
27714
28183
|
implementerThreadId: ThreadId,
|
|
27715
28184
|
watcherThreadId: ThreadId,
|
|
27716
28185
|
lastReviewedImplementerSequence: NonNegativeInt,
|
|
28186
|
+
roundCap: NonNegativeInt,
|
|
28187
|
+
gateTimeoutMs: NonNegativeInt,
|
|
28188
|
+
activeGate: Schema$1.NullOr(Schema$1.fromJsonString(OrchestrationThreadPairGate)),
|
|
27717
28189
|
createdAt: IsoDateTime,
|
|
27718
28190
|
detachedAt: Schema$1.NullOr(IsoDateTime)
|
|
27719
28191
|
});
|
|
@@ -27898,10 +28370,31 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
27898
28370
|
implementer_thread_id AS "implementerThreadId",
|
|
27899
28371
|
watcher_thread_id AS "watcherThreadId",
|
|
27900
28372
|
last_reviewed_implementer_sequence AS "lastReviewedImplementerSequence",
|
|
28373
|
+
round_cap AS "roundCap",
|
|
28374
|
+
gate_timeout_ms AS "gateTimeoutMs",
|
|
28375
|
+
active_gate_json AS "activeGate",
|
|
27901
28376
|
created_at AS "createdAt",
|
|
27902
28377
|
detached_at AS "detachedAt"
|
|
27903
28378
|
FROM thread_pairs
|
|
27904
28379
|
ORDER BY created_at ASC, pair_id ASC
|
|
28380
|
+
`
|
|
28381
|
+
});
|
|
28382
|
+
const getThreadPairRow = SqlSchema.findOneOption({
|
|
28383
|
+
Request: ThreadPairId,
|
|
28384
|
+
Result: ProjectionThreadPairDbRowSchema,
|
|
28385
|
+
execute: (pairId) => sql`
|
|
28386
|
+
SELECT
|
|
28387
|
+
pair_id AS "id",
|
|
28388
|
+
implementer_thread_id AS "implementerThreadId",
|
|
28389
|
+
watcher_thread_id AS "watcherThreadId",
|
|
28390
|
+
last_reviewed_implementer_sequence AS "lastReviewedImplementerSequence",
|
|
28391
|
+
round_cap AS "roundCap",
|
|
28392
|
+
gate_timeout_ms AS "gateTimeoutMs",
|
|
28393
|
+
active_gate_json AS "activeGate",
|
|
28394
|
+
created_at AS "createdAt",
|
|
28395
|
+
detached_at AS "detachedAt"
|
|
28396
|
+
FROM thread_pairs
|
|
28397
|
+
WHERE pair_id = ${pairId}
|
|
27905
28398
|
`
|
|
27906
28399
|
});
|
|
27907
28400
|
const listThreadRows = SqlSchema.findAll({
|
|
@@ -29056,6 +29549,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
29056
29549
|
messageCreatedAt: row.messageCreatedAt
|
|
29057
29550
|
})) };
|
|
29058
29551
|
});
|
|
29552
|
+
const getThreadPairById = (pairId) => getThreadPairRow(pairId).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadPairById:query", "ProjectionSnapshotQuery.getThreadPairById:decodeRow")));
|
|
29059
29553
|
const getThreadShellById = (threadId) => Effect.gen(function* () {
|
|
29060
29554
|
const [threadRow, latestTurnRow, sessionRow] = yield* Effect.all([
|
|
29061
29555
|
getActiveThreadRowById({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadShellById:getThread:query", "ProjectionSnapshotQuery.getThreadShellById:getThread:decodeRow"))),
|
|
@@ -29186,6 +29680,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
29186
29680
|
getFirstActiveThreadIdByProjectId,
|
|
29187
29681
|
getThreadCheckpointContext,
|
|
29188
29682
|
getFullThreadDiffContext,
|
|
29683
|
+
getThreadPairById,
|
|
29189
29684
|
getThreadShellById,
|
|
29190
29685
|
getThreadDetailById,
|
|
29191
29686
|
getThreadDetailSnapshot
|
|
@@ -45681,6 +46176,24 @@ const requireWatchCapability = Effect.fn("mcp.requireWatchCapability")(function*
|
|
|
45681
46176
|
if (!invocation.watchThreadIds?.has(watchedThreadId)) return yield* new ThreadWatchNotPermittedError({ threadId: watchedThreadId });
|
|
45682
46177
|
return invocation;
|
|
45683
46178
|
});
|
|
46179
|
+
/**
|
|
46180
|
+
* The advise guard mirrors the watch guard's two checks - capability present
|
|
46181
|
+
* AND thread in the explicit grant - because advising carries strictly more
|
|
46182
|
+
* power than watching. A watch-only credential fails the first check here no
|
|
46183
|
+
* matter what threads it can read.
|
|
46184
|
+
*/
|
|
46185
|
+
const requireAdviseCapability = Effect.fn("mcp.requireAdviseCapability")(function* (advisedThreadId) {
|
|
46186
|
+
const invocation = yield* McpInvocationContext;
|
|
46187
|
+
if (!invocation.capabilities.has("advise")) return yield* new AdviseToolUnavailableError({
|
|
46188
|
+
capability: "advise",
|
|
46189
|
+
environmentId: invocation.environmentId,
|
|
46190
|
+
threadId: invocation.threadId,
|
|
46191
|
+
providerSessionId: invocation.providerSessionId,
|
|
46192
|
+
providerInstanceId: invocation.providerInstanceId
|
|
46193
|
+
});
|
|
46194
|
+
if (!invocation.adviseThreadIds?.has(advisedThreadId)) return yield* new ThreadAdviseNotPermittedError({ threadId: advisedThreadId });
|
|
46195
|
+
return invocation;
|
|
46196
|
+
});
|
|
45684
46197
|
/** Third of the trio, for the same reason the second exists. */
|
|
45685
46198
|
const requireThreadCapability = Effect.fn("mcp.requireThreadCapability")(function* () {
|
|
45686
46199
|
const invocation = yield* McpInvocationContext;
|
|
@@ -60494,7 +61007,6 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
|
|
|
60494
61007
|
const serverSelfUpdate = yield* ServerSelfUpdate;
|
|
60495
61008
|
const textGeneration = yield* TextGeneration;
|
|
60496
61009
|
const config = yield* ServerConfig$1;
|
|
60497
|
-
const allowAbsoluteFileReads = config.mode === "desktop" && !isRemoteReachableHost(config.host);
|
|
60498
61010
|
const lifecycleEvents = yield* ServerLifecycleEvents;
|
|
60499
61011
|
const serverSettings = yield* ServerSettingsService;
|
|
60500
61012
|
const hubLink = yield* HubLink;
|
|
@@ -60612,10 +61124,24 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
|
|
|
60612
61124
|
implementerThreadId: event.payload.implementerThreadId,
|
|
60613
61125
|
watcherThreadId: event.payload.watcherThreadId,
|
|
60614
61126
|
lastReviewedImplementerSequence: event.payload.lastReviewedImplementerSequence,
|
|
61127
|
+
roundCap: event.payload.roundCap ?? 2,
|
|
61128
|
+
gateTimeoutMs: event.payload.gateTimeoutMs ?? 12e4,
|
|
61129
|
+
activeGate: null,
|
|
60615
61130
|
createdAt: event.payload.createdAt,
|
|
60616
61131
|
detachedAt: null
|
|
60617
61132
|
}
|
|
60618
61133
|
}));
|
|
61134
|
+
case "thread-pair.gate-opened":
|
|
61135
|
+
case "thread-pair.gate-advanced":
|
|
61136
|
+
case "thread-pair.gate-resolved":
|
|
61137
|
+
case "thread-pair.cursor-advanced": return projectionSnapshotQuery.getThreadPairById(event.payload.pairId).pipe(Effect.retry({ times: 1 }), Effect.map(Option.map((pair) => ({
|
|
61138
|
+
kind: "thread-pair-upserted",
|
|
61139
|
+
sequence: event.sequence,
|
|
61140
|
+
pair
|
|
61141
|
+
}))), Effect.tapError((error) => Effect.logWarning("orchestration shell pair refetch failed", {
|
|
61142
|
+
pairId: event.payload.pairId,
|
|
61143
|
+
error
|
|
61144
|
+
})), Effect.orElseSucceed(() => Option.none()));
|
|
60619
61145
|
case "thread-pair.detached": return Effect.succeed(Option.some({
|
|
60620
61146
|
kind: "thread-pair-removed",
|
|
60621
61147
|
sequence: event.sequence,
|
|
@@ -61228,17 +61754,11 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
|
|
|
61228
61754
|
...projectEntriesFailureContext(cause),
|
|
61229
61755
|
cause
|
|
61230
61756
|
}))), { "rpc.aggregate": "workspace" }),
|
|
61231
|
-
[WS_METHODS.projectsReadFile]: (input) => observeRpcEffect$1(WS_METHODS.projectsReadFile, Effect.
|
|
61232
|
-
|
|
61233
|
-
|
|
61234
|
-
|
|
61235
|
-
|
|
61236
|
-
return yield* workspaceFileSystem.readFile(input).pipe(Effect.mapError((cause) => new ProjectReadFileError({
|
|
61237
|
-
...input,
|
|
61238
|
-
...projectFileFailureContext(cause),
|
|
61239
|
-
cause
|
|
61240
|
-
})));
|
|
61241
|
-
}), { "rpc.aggregate": "workspace" }),
|
|
61757
|
+
[WS_METHODS.projectsReadFile]: (input) => observeRpcEffect$1(WS_METHODS.projectsReadFile, workspaceFileSystem.readFile(input).pipe(Effect.mapError((cause) => new ProjectReadFileError({
|
|
61758
|
+
...input,
|
|
61759
|
+
...projectFileFailureContext(cause),
|
|
61760
|
+
cause
|
|
61761
|
+
}))), { "rpc.aggregate": "workspace" }),
|
|
61242
61762
|
[WS_METHODS.projectsWriteFile]: (input) => observeRpcEffect$1(WS_METHODS.projectsWriteFile, workspaceFileSystem.writeFile(input).pipe(Effect.mapError((cause) => new ProjectWriteFileError({
|
|
61243
61763
|
cwd: input.cwd,
|
|
61244
61764
|
relativePath: input.relativePath,
|
|
@@ -62125,12 +62645,14 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
62125
62645
|
const rawToken = yield* crypto.randomBytes(32).pipe(Effect.map(tokenFromBytes), Effect.orDie);
|
|
62126
62646
|
const tokenHash = yield* hashToken(rawToken);
|
|
62127
62647
|
const watchThreadIds = new Set((request.watchThreadIds ?? []).map((threadId) => ThreadId.make(threadId)));
|
|
62648
|
+
const adviseThreadIds = new Set((request.adviseThreadIds ?? []).map((threadId) => ThreadId.make(threadId)));
|
|
62128
62649
|
const capabilities = /* @__PURE__ */ new Set([
|
|
62129
62650
|
"preview",
|
|
62130
62651
|
"tasks",
|
|
62131
62652
|
"threads"
|
|
62132
62653
|
]);
|
|
62133
62654
|
if (watchThreadIds.size > 0) capabilities.add("watch");
|
|
62655
|
+
if (adviseThreadIds.size > 0) capabilities.add("advise");
|
|
62134
62656
|
const threadId = ThreadId.make(request.threadId);
|
|
62135
62657
|
const scopeWith = (mayCreateThreads) => ({
|
|
62136
62658
|
environmentId,
|
|
@@ -62139,6 +62661,7 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
62139
62661
|
providerInstanceId: ProviderInstanceId.make(request.providerInstanceId),
|
|
62140
62662
|
capabilities,
|
|
62141
62663
|
...watchThreadIds.size > 0 ? { watchThreadIds } : {},
|
|
62664
|
+
...adviseThreadIds.size > 0 ? { adviseThreadIds } : {},
|
|
62142
62665
|
mayCreateThreads,
|
|
62143
62666
|
issuedAt
|
|
62144
62667
|
});
|
|
@@ -62254,6 +62777,54 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
62254
62777
|
};
|
|
62255
62778
|
});
|
|
62256
62779
|
});
|
|
62780
|
+
const grantAdviseThread = Effect.fn("McpSessionRegistry.grantAdviseThread")(function* ({ watcherThreadId, advisedThreadId }) {
|
|
62781
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
|
|
62782
|
+
const next = new Map(records);
|
|
62783
|
+
for (const [tokenHash, record] of records) {
|
|
62784
|
+
if (record.scope.threadId !== watcherThreadId) continue;
|
|
62785
|
+
next.set(tokenHash, {
|
|
62786
|
+
...record,
|
|
62787
|
+
scope: {
|
|
62788
|
+
...record.scope,
|
|
62789
|
+
capabilities: /* @__PURE__ */ new Set([...record.scope.capabilities, "advise"]),
|
|
62790
|
+
adviseThreadIds: /* @__PURE__ */ new Set([...record.scope.adviseThreadIds ?? [], advisedThreadId])
|
|
62791
|
+
}
|
|
62792
|
+
});
|
|
62793
|
+
}
|
|
62794
|
+
return {
|
|
62795
|
+
records: next,
|
|
62796
|
+
spawnedThreadIds
|
|
62797
|
+
};
|
|
62798
|
+
});
|
|
62799
|
+
});
|
|
62800
|
+
const revokeAdviseThread = Effect.fn("McpSessionRegistry.revokeAdviseThread")(function* ({ watcherThreadId, advisedThreadId }) {
|
|
62801
|
+
yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
|
|
62802
|
+
const next = new Map(records);
|
|
62803
|
+
for (const [tokenHash, record] of records) {
|
|
62804
|
+
if (record.scope.threadId !== watcherThreadId) continue;
|
|
62805
|
+
const adviseThreadIds = new Set(record.scope.adviseThreadIds ?? []);
|
|
62806
|
+
adviseThreadIds.delete(advisedThreadId);
|
|
62807
|
+
const capabilities = new Set(record.scope.capabilities);
|
|
62808
|
+
if (adviseThreadIds.size === 0) capabilities.delete("advise");
|
|
62809
|
+
const { adviseThreadIds: _previousAdviseThreadIds, ...scopeWithoutAdvise } = record.scope;
|
|
62810
|
+
next.set(tokenHash, {
|
|
62811
|
+
...record,
|
|
62812
|
+
scope: adviseThreadIds.size > 0 ? {
|
|
62813
|
+
...scopeWithoutAdvise,
|
|
62814
|
+
capabilities,
|
|
62815
|
+
adviseThreadIds
|
|
62816
|
+
} : {
|
|
62817
|
+
...scopeWithoutAdvise,
|
|
62818
|
+
capabilities
|
|
62819
|
+
}
|
|
62820
|
+
});
|
|
62821
|
+
}
|
|
62822
|
+
return {
|
|
62823
|
+
records: next,
|
|
62824
|
+
spawnedThreadIds
|
|
62825
|
+
};
|
|
62826
|
+
});
|
|
62827
|
+
});
|
|
62257
62828
|
const recordSpawnedThread = Effect.fn("McpSessionRegistry.recordSpawnedThread")(function* (input) {
|
|
62258
62829
|
yield* SynchronizedRef.update(state, ({ records, spawnedThreadIds }) => {
|
|
62259
62830
|
const next = new Map(records);
|
|
@@ -62277,6 +62848,8 @@ const makeWithOptions = Effect.fn("McpSessionRegistry.make")(function* (options
|
|
|
62277
62848
|
touch,
|
|
62278
62849
|
grantWatchThread,
|
|
62279
62850
|
revokeWatchThread,
|
|
62851
|
+
grantAdviseThread,
|
|
62852
|
+
revokeAdviseThread,
|
|
62280
62853
|
recordSpawnedThread,
|
|
62281
62854
|
revokeProviderSession: Effect.fn("McpSessionRegistry.revokeProviderSession")(function* (providerSessionId) {
|
|
62282
62855
|
yield* revokeWhere((record) => record.scope.providerSessionId === providerSessionId);
|
|
@@ -62305,6 +62878,8 @@ const issueActiveMcpCredential = (request) => activeMcpSessionRegistry ? activeM
|
|
|
62305
62878
|
const touchActiveMcpThread = (threadId) => activeMcpSessionRegistry ? activeMcpSessionRegistry.touch(threadId) : Effect.void;
|
|
62306
62879
|
const grantActiveMcpWatchThread = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.grantWatchThread(input) : Effect.void;
|
|
62307
62880
|
const revokeActiveMcpWatchThread = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.revokeWatchThread(input) : Effect.void;
|
|
62881
|
+
const grantActiveMcpAdviseThread = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.grantAdviseThread(input) : Effect.void;
|
|
62882
|
+
const revokeActiveMcpAdviseThread = (input) => activeMcpSessionRegistry ? activeMcpSessionRegistry.revokeAdviseThread(input) : Effect.void;
|
|
62308
62883
|
const revokeActiveMcpThread = (threadId) => activeMcpSessionRegistry ? activeMcpSessionRegistry.revokeThread(threadId) : Effect.void;
|
|
62309
62884
|
const revokeAllActiveMcpCredentials = () => activeMcpSessionRegistry ? activeMcpSessionRegistry.revokeAll : Effect.void;
|
|
62310
62885
|
//#endregion
|
|
@@ -62390,10 +62965,11 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
|
|
|
62390
62965
|
const directory = yield* ProviderSessionDirectory;
|
|
62391
62966
|
const runtimeEventPubSub = yield* PubSub.unbounded();
|
|
62392
62967
|
const nowIso = Effect.map(DateTime.now, DateTime.formatIso);
|
|
62393
|
-
const prepareMcpSession = (threadId, providerInstanceId, watchThreadIds) => issueActiveMcpCredential({
|
|
62968
|
+
const prepareMcpSession = (threadId, providerInstanceId, watchThreadIds, adviseThreadIds) => issueActiveMcpCredential({
|
|
62394
62969
|
threadId,
|
|
62395
62970
|
providerInstanceId,
|
|
62396
|
-
...watchThreadIds !== void 0 ? { watchThreadIds } : {}
|
|
62971
|
+
...watchThreadIds !== void 0 ? { watchThreadIds } : {},
|
|
62972
|
+
...adviseThreadIds !== void 0 ? { adviseThreadIds } : {}
|
|
62397
62973
|
}).pipe(Effect.tap((credential) => credential ? Effect.sync(() => setMcpProviderSession(credential.config)) : Effect.void));
|
|
62398
62974
|
const clearMcpSession = (threadId) => revokeActiveMcpThread(threadId).pipe(Effect.tap(() => Effect.sync(() => clearMcpProviderSession(threadId))));
|
|
62399
62975
|
const publishRuntimeEvent = (event) => Effect.succeed(event).pipe(Effect.tap((canonicalEvent) => canonicalEventLogger ? canonicalEventLogger.write(canonicalEvent, canonicalEvent.threadId) : Effect.void), Effect.flatMap((canonicalEvent) => PubSub.publish(runtimeEventPubSub, canonicalEvent)), Effect.asVoid);
|
|
@@ -62574,7 +63150,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
|
|
|
62574
63150
|
"provider.cwd.effective": effectiveCwd ?? ""
|
|
62575
63151
|
});
|
|
62576
63152
|
const adapter = yield* registry.getByInstance(resolvedInstanceId);
|
|
62577
|
-
yield* prepareMcpSession(threadId, resolvedInstanceId, options?.watchThreadIds);
|
|
63153
|
+
yield* prepareMcpSession(threadId, resolvedInstanceId, options?.watchThreadIds, options?.adviseThreadIds);
|
|
62578
63154
|
const session = yield* adapter.startSession({
|
|
62579
63155
|
...input,
|
|
62580
63156
|
providerInstanceId: resolvedInstanceId,
|
|
@@ -100761,7 +101337,7 @@ const invokeTargeted = (operation, input, timeoutMs) => {
|
|
|
100761
101337
|
const { tabId, ...operationInput } = input;
|
|
100762
101338
|
return invoke(operation, operationInput, timeoutMs, tabId);
|
|
100763
101339
|
};
|
|
100764
|
-
const handlers$
|
|
101340
|
+
const handlers$4 = {
|
|
100765
101341
|
preview_status: (input) => invokeTargeted("status", input ?? {}),
|
|
100766
101342
|
preview_open: (input) => invokeTargeted("open", normalizePreviewOpenInput(input)),
|
|
100767
101343
|
preview_navigate: (input) => invokeTargeted("navigate", input, input.timeoutMs),
|
|
@@ -100777,10 +101353,10 @@ const handlers$3 = {
|
|
|
100777
101353
|
preview_recording_start: (input) => invokeTargeted("recordingStart", input ?? {}),
|
|
100778
101354
|
preview_recording_stop: (input) => invokeTargeted("recordingStop", input ?? {})
|
|
100779
101355
|
};
|
|
100780
|
-
const { preview_snapshot, ...standardHandlers } = handlers$
|
|
101356
|
+
const { preview_snapshot, ...standardHandlers } = handlers$4;
|
|
100781
101357
|
const PreviewStandardToolkitHandlersLive = PreviewStandardToolkit.toLayer(standardHandlers);
|
|
100782
101358
|
const PreviewSnapshotToolkitHandlersLive = PreviewSnapshotToolkit.toLayer({ preview_snapshot });
|
|
100783
|
-
PreviewToolkit.toLayer(handlers$
|
|
101359
|
+
PreviewToolkit.toLayer(handlers$4);
|
|
100784
101360
|
//#endregion
|
|
100785
101361
|
//#region src/mcp/TicketResolver.ts
|
|
100786
101362
|
var TicketResolver = class extends Context.Service()("@p4code/cli/mcp/TicketResolver") {};
|
|
@@ -101751,6 +102327,214 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
|
|
|
101751
102327
|
})
|
|
101752
102328
|
});
|
|
101753
102329
|
//#endregion
|
|
102330
|
+
//#region src/mcp/toolkits/advise/tools.ts
|
|
102331
|
+
/**
|
|
102332
|
+
* The write half of cross-thread access, and the reason advise is its own
|
|
102333
|
+
* capability: `thread_watch_events` reads another thread, these tools steer
|
|
102334
|
+
* one. Both handlers admit only threads in the session's explicit
|
|
102335
|
+
* `adviseThreadIds` grant - in practice the paired implementer - so a
|
|
102336
|
+
* watch-only credential cannot reach them at all and an advise credential
|
|
102337
|
+
* cannot reach past its own pair.
|
|
102338
|
+
*/
|
|
102339
|
+
const ThreadAdviseTool = Tool.make("thread_advise", {
|
|
102340
|
+
description: "Post advice into the paired builder thread. The builder sees the message as supervisor advice and responds in its own pane. Set interrupt to cancel the builder's running turn first, when continuing would waste or damage work. Do not use this while a gate is awaiting your answer - answer the gate with thread_gate_respond instead. After advising, end your turn; you are woken automatically when the builder finishes responding.",
|
|
102341
|
+
parameters: ThreadAdviseInput,
|
|
102342
|
+
success: ThreadAdviseResult,
|
|
102343
|
+
failure: ThreadAdviseToolError,
|
|
102344
|
+
dependencies: [
|
|
102345
|
+
McpInvocationContext,
|
|
102346
|
+
OrchestrationEngineService,
|
|
102347
|
+
ProjectionSnapshotQuery,
|
|
102348
|
+
Crypto.Crypto
|
|
102349
|
+
]
|
|
102350
|
+
}).annotate(Tool.Title, "Advise the paired builder").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
|
|
102351
|
+
const ThreadGateRespondTool = Tool.make("thread_gate_respond", {
|
|
102352
|
+
description: "Answer the open gate on the paired builder thread. approve clears the gate and lets the builder proceed; object delivers your objection to the builder and spends one exchange round - when the rounds are used up the gate escalates to the human instead. The gate id is given in the gate review prompt. After answering, end your turn; you are woken automatically at the next boundary.",
|
|
102353
|
+
parameters: ThreadGateRespondInput,
|
|
102354
|
+
success: ThreadGateRespondResult,
|
|
102355
|
+
failure: ThreadAdviseToolError,
|
|
102356
|
+
dependencies: [
|
|
102357
|
+
McpInvocationContext,
|
|
102358
|
+
OrchestrationEngineService,
|
|
102359
|
+
ProjectionSnapshotQuery,
|
|
102360
|
+
Crypto.Crypto
|
|
102361
|
+
]
|
|
102362
|
+
}).annotate(Tool.Title, "Answer the open gate").annotate(Tool.Readonly, false).annotate(Tool.Destructive, false).annotate(Tool.Idempotent, false);
|
|
102363
|
+
const AdviseToolkit = Toolkit.make(ThreadAdviseTool, ThreadGateRespondTool);
|
|
102364
|
+
//#endregion
|
|
102365
|
+
//#region src/mcp/toolkits/advise/handlers.ts
|
|
102366
|
+
const newId = Effect.gen(function* () {
|
|
102367
|
+
return yield* (yield* Crypto.Crypto).randomUUIDv4.pipe(Effect.orDie);
|
|
102368
|
+
});
|
|
102369
|
+
/** Every dispatch here reports failure as the one error the toolkit declares. */
|
|
102370
|
+
const dispatchAdvise = Effect.fn("mcp.advise.dispatch")(function* (command, threadId) {
|
|
102371
|
+
yield* (yield* OrchestrationEngineService).dispatch(command).pipe(Effect.mapError((cause) => new ThreadAdviseFailedError({
|
|
102372
|
+
threadId,
|
|
102373
|
+
detail: cause.message
|
|
102374
|
+
})));
|
|
102375
|
+
});
|
|
102376
|
+
const readCommandModel = Effect.fn("mcp.advise.readModel")(function* (threadId) {
|
|
102377
|
+
return yield* (yield* ProjectionSnapshotQuery).getCommandReadModel().pipe(Effect.mapError((cause) => new ThreadAdviseFailedError({
|
|
102378
|
+
threadId,
|
|
102379
|
+
detail: cause.message
|
|
102380
|
+
})));
|
|
102381
|
+
});
|
|
102382
|
+
/**
|
|
102383
|
+
* Starts a turn on the advised thread carrying the supervisor's message. The
|
|
102384
|
+
* thread's own modes ride along so advice never changes how the builder runs.
|
|
102385
|
+
*/
|
|
102386
|
+
const sendAdviceTurn = Effect.fn("mcp.advise.sendAdviceTurn")(function* (input) {
|
|
102387
|
+
const messageId = MessageId.make(yield* newId);
|
|
102388
|
+
yield* dispatchAdvise({
|
|
102389
|
+
type: "thread.turn.start",
|
|
102390
|
+
commandId: CommandId.make(yield* newId),
|
|
102391
|
+
threadId: input.thread.id,
|
|
102392
|
+
message: {
|
|
102393
|
+
messageId,
|
|
102394
|
+
role: "user",
|
|
102395
|
+
text: input.text,
|
|
102396
|
+
attachments: []
|
|
102397
|
+
},
|
|
102398
|
+
runtimeMode: input.thread.runtimeMode,
|
|
102399
|
+
interactionMode: input.thread.interactionMode,
|
|
102400
|
+
compressMode: input.thread.compressMode,
|
|
102401
|
+
unpromptedSubagents: input.thread.unpromptedSubagents,
|
|
102402
|
+
createdAt: input.createdAt
|
|
102403
|
+
}, input.thread.id);
|
|
102404
|
+
return messageId;
|
|
102405
|
+
});
|
|
102406
|
+
const AdviseToolkitHandlersLive = AdviseToolkit.toLayer({
|
|
102407
|
+
thread_advise: (input) => Effect.gen(function* () {
|
|
102408
|
+
yield* requireAdviseCapability(input.threadId);
|
|
102409
|
+
const readModel = yield* readCommandModel(input.threadId);
|
|
102410
|
+
const thread = readModel.threads.find((candidate) => candidate.id === input.threadId && candidate.deletedAt === null);
|
|
102411
|
+
if (thread === void 0) return yield* new ThreadAdviseFailedError({
|
|
102412
|
+
threadId: input.threadId,
|
|
102413
|
+
detail: "The advised thread does not exist."
|
|
102414
|
+
});
|
|
102415
|
+
const gatedPair = (readModel.threadPairs ?? []).find((pair) => pair.detachedAt === null && pair.implementerThreadId === input.threadId && pair.activeGate !== null && pair.activeGate.state === "awaiting-watcher");
|
|
102416
|
+
if (gatedPair !== void 0) return yield* new ThreadAdviseFailedError({
|
|
102417
|
+
threadId: input.threadId,
|
|
102418
|
+
detail: `Gate '${gatedPair.activeGate?.id}' is awaiting your answer. Answer it with thread_gate_respond instead of thread_advise.`
|
|
102419
|
+
});
|
|
102420
|
+
const createdAt = DateTime.formatIso(yield* DateTime.now);
|
|
102421
|
+
const interrupted = input.interrupt === true;
|
|
102422
|
+
if (interrupted) yield* dispatchAdvise({
|
|
102423
|
+
type: "thread.turn.interrupt",
|
|
102424
|
+
commandId: CommandId.make(yield* newId),
|
|
102425
|
+
threadId: input.threadId,
|
|
102426
|
+
createdAt
|
|
102427
|
+
}, input.threadId);
|
|
102428
|
+
const messageId = yield* sendAdviceTurn({
|
|
102429
|
+
thread,
|
|
102430
|
+
text: `${FUSION_ADVICE_PROMPT_PREFIX} ${input.message}`,
|
|
102431
|
+
createdAt
|
|
102432
|
+
});
|
|
102433
|
+
return {
|
|
102434
|
+
threadId: input.threadId,
|
|
102435
|
+
messageId,
|
|
102436
|
+
interrupted
|
|
102437
|
+
};
|
|
102438
|
+
}),
|
|
102439
|
+
thread_gate_respond: (input) => Effect.gen(function* () {
|
|
102440
|
+
yield* requireAdviseCapability(input.threadId);
|
|
102441
|
+
const readModel = yield* readCommandModel(input.threadId);
|
|
102442
|
+
const pair = (readModel.threadPairs ?? []).find((candidate) => candidate.detachedAt === null && candidate.implementerThreadId === input.threadId);
|
|
102443
|
+
const gate = pair?.activeGate ?? null;
|
|
102444
|
+
if (pair === void 0 || gate === null || gate.id !== input.gateId) return yield* new ThreadAdviseFailedError({
|
|
102445
|
+
threadId: input.threadId,
|
|
102446
|
+
detail: `Gate '${input.gateId}' is not open on this thread. It may already be resolved.`
|
|
102447
|
+
});
|
|
102448
|
+
if (gate.state !== "awaiting-watcher") return yield* new ThreadAdviseFailedError({
|
|
102449
|
+
threadId: input.threadId,
|
|
102450
|
+
detail: `Gate '${input.gateId}' is not awaiting the supervisor (state: ${gate.state}).`
|
|
102451
|
+
});
|
|
102452
|
+
const thread = readModel.threads.find((candidate) => candidate.id === input.threadId && candidate.deletedAt === null);
|
|
102453
|
+
if (thread === void 0) return yield* new ThreadAdviseFailedError({
|
|
102454
|
+
threadId: input.threadId,
|
|
102455
|
+
detail: "The advised thread does not exist."
|
|
102456
|
+
});
|
|
102457
|
+
const now = DateTime.formatIso(yield* DateTime.now);
|
|
102458
|
+
if (input.decision === "approve") {
|
|
102459
|
+
yield* dispatchAdvise({
|
|
102460
|
+
type: "thread-pair.gate.resolve",
|
|
102461
|
+
commandId: CommandId.make(yield* newId),
|
|
102462
|
+
pairId: pair.id,
|
|
102463
|
+
gateId: gate.id,
|
|
102464
|
+
outcome: "approved",
|
|
102465
|
+
resolvedBy: "watcher",
|
|
102466
|
+
resolvedAt: now
|
|
102467
|
+
}, input.threadId);
|
|
102468
|
+
if (gate.kind === "command-approval" && gate.requestId !== null) yield* dispatchAdvise({
|
|
102469
|
+
type: "thread.approval.respond",
|
|
102470
|
+
commandId: CommandId.make(yield* newId),
|
|
102471
|
+
threadId: input.threadId,
|
|
102472
|
+
requestId: gate.requestId,
|
|
102473
|
+
decision: "accept",
|
|
102474
|
+
createdAt: now
|
|
102475
|
+
}, input.threadId);
|
|
102476
|
+
if (gate.kind === "repeated-failure") yield* sendAdviceTurn({
|
|
102477
|
+
thread,
|
|
102478
|
+
text: `${FUSION_GATE_PROMPT_PREFIX} The supervisor reviewed the repeated command failures and cleared the gate. Continue the work.`,
|
|
102479
|
+
createdAt: now
|
|
102480
|
+
});
|
|
102481
|
+
return {
|
|
102482
|
+
threadId: input.threadId,
|
|
102483
|
+
gateId: gate.id,
|
|
102484
|
+
kind: gate.kind,
|
|
102485
|
+
outcome: "approved"
|
|
102486
|
+
};
|
|
102487
|
+
}
|
|
102488
|
+
if (input.message === void 0) return yield* new ThreadAdviseFailedError({
|
|
102489
|
+
threadId: input.threadId,
|
|
102490
|
+
detail: "An objection requires a message for the builder."
|
|
102491
|
+
});
|
|
102492
|
+
if (gate.round >= pair.roundCap) {
|
|
102493
|
+
yield* dispatchAdvise({
|
|
102494
|
+
type: "thread-pair.gate.advance",
|
|
102495
|
+
commandId: CommandId.make(yield* newId),
|
|
102496
|
+
pairId: pair.id,
|
|
102497
|
+
gateId: gate.id,
|
|
102498
|
+
transition: "escalate",
|
|
102499
|
+
advancedAt: now
|
|
102500
|
+
}, input.threadId);
|
|
102501
|
+
return {
|
|
102502
|
+
threadId: input.threadId,
|
|
102503
|
+
gateId: gate.id,
|
|
102504
|
+
kind: gate.kind,
|
|
102505
|
+
outcome: "escalated"
|
|
102506
|
+
};
|
|
102507
|
+
}
|
|
102508
|
+
yield* dispatchAdvise({
|
|
102509
|
+
type: "thread-pair.gate.advance",
|
|
102510
|
+
commandId: CommandId.make(yield* newId),
|
|
102511
|
+
pairId: pair.id,
|
|
102512
|
+
gateId: gate.id,
|
|
102513
|
+
transition: "objection",
|
|
102514
|
+
advancedAt: now
|
|
102515
|
+
}, input.threadId);
|
|
102516
|
+
if (gate.kind === "command-approval" && gate.requestId !== null) yield* dispatchAdvise({
|
|
102517
|
+
type: "thread.approval.respond",
|
|
102518
|
+
commandId: CommandId.make(yield* newId),
|
|
102519
|
+
threadId: input.threadId,
|
|
102520
|
+
requestId: gate.requestId,
|
|
102521
|
+
decision: "decline",
|
|
102522
|
+
createdAt: now
|
|
102523
|
+
}, input.threadId);
|
|
102524
|
+
yield* sendAdviceTurn({
|
|
102525
|
+
thread,
|
|
102526
|
+
text: `${FUSION_ADVICE_PROMPT_PREFIX} ${input.message}`,
|
|
102527
|
+
createdAt: now
|
|
102528
|
+
});
|
|
102529
|
+
return {
|
|
102530
|
+
threadId: input.threadId,
|
|
102531
|
+
gateId: gate.id,
|
|
102532
|
+
kind: gate.kind,
|
|
102533
|
+
outcome: "objection-sent"
|
|
102534
|
+
};
|
|
102535
|
+
})
|
|
102536
|
+
});
|
|
102537
|
+
//#endregion
|
|
101754
102538
|
//#region src/mcp/toolkits/watch/tools.ts
|
|
101755
102539
|
/**
|
|
101756
102540
|
* Cross-thread by design, which is exactly why it is the most guarded tool in
|
|
@@ -101891,12 +102675,13 @@ const PreviewToolkitRegistrationLive = Layer.mergeAll(PreviewStandardToolkitRegi
|
|
|
101891
102675
|
const TaskToolkitRegistrationLive = McpServer.toolkit(TaskToolkit).pipe(Layer.provide(TaskToolkitHandlersLive));
|
|
101892
102676
|
const ThreadToolkitRegistrationLive = McpServer.toolkit(ThreadToolkit).pipe(Layer.provide(ThreadToolkitHandlersLive));
|
|
101893
102677
|
const WatchToolkitRegistrationLive = McpServer.toolkit(WatchToolkit).pipe(Layer.provide(WatchToolkitHandlersLive));
|
|
102678
|
+
const AdviseToolkitRegistrationLive = McpServer.toolkit(AdviseToolkit).pipe(Layer.provide(AdviseToolkitHandlersLive));
|
|
101894
102679
|
const McpTransportLive = McpServer.layerHttp({
|
|
101895
102680
|
name: "P4Code",
|
|
101896
102681
|
version,
|
|
101897
102682
|
path: "/mcp"
|
|
101898
102683
|
}).pipe(Layer.provide(McpAuthMiddlewareLive));
|
|
101899
|
-
const layer = Layer.mergeAll(PreviewToolkitRegistrationLive, TaskToolkitRegistrationLive, ThreadToolkitRegistrationLive, WatchToolkitRegistrationLive).pipe(Layer.provideMerge(McpTransportLive));
|
|
102684
|
+
const layer = Layer.mergeAll(PreviewToolkitRegistrationLive, TaskToolkitRegistrationLive, ThreadToolkitRegistrationLive, WatchToolkitRegistrationLive, AdviseToolkitRegistrationLive).pipe(Layer.provideMerge(McpTransportLive));
|
|
101900
102685
|
//#endregion
|
|
101901
102686
|
//#region src/orchestration/Services/CheckpointReactor.ts
|
|
101902
102687
|
/**
|
|
@@ -102526,6 +103311,7 @@ function runtimeEventToActivities(event, taskTitle, compressMode) {
|
|
|
102526
103311
|
summary: event.payload.title ?? "Tool",
|
|
102527
103312
|
payload: {
|
|
102528
103313
|
itemType: event.payload.itemType,
|
|
103314
|
+
...event.payload.status ? { status: event.payload.status } : {},
|
|
102529
103315
|
...event.payload.detail ? { detail: truncateDetail(event.payload.detail) } : {},
|
|
102530
103316
|
...event.payload.data !== void 0 ? { data: event.payload.data } : {}
|
|
102531
103317
|
},
|
|
@@ -103559,6 +104345,10 @@ const make$3 = Effect.gen(function* () {
|
|
|
103559
104345
|
watcherThreadId: threadId,
|
|
103560
104346
|
watchedThreadId
|
|
103561
104347
|
}), { discard: true });
|
|
104348
|
+
yield* Effect.forEach(watchThreadIds, (advisedThreadId) => grantActiveMcpAdviseThread({
|
|
104349
|
+
watcherThreadId: threadId,
|
|
104350
|
+
advisedThreadId
|
|
104351
|
+
}), { discard: true });
|
|
103562
104352
|
const resolveActiveSession = (threadId) => providerService.listSessions().pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === threadId)));
|
|
103563
104353
|
const activeSession = yield* resolveActiveSession(threadId);
|
|
103564
104354
|
const activeThreadSession = thread.session !== null && thread.session.status !== "stopped" && activeSession ? thread.session : null;
|
|
@@ -103648,7 +104438,10 @@ const make$3 = Effect.gen(function* () {
|
|
|
103648
104438
|
runtimeMode: desiredRuntimeMode,
|
|
103649
104439
|
compressMode: thread.compressMode,
|
|
103650
104440
|
unpromptedSubagents: thread.unpromptedSubagents
|
|
103651
|
-
}, watchThreadIds.length > 0 ? {
|
|
104441
|
+
}, watchThreadIds.length > 0 ? {
|
|
104442
|
+
watchThreadIds,
|
|
104443
|
+
adviseThreadIds: watchThreadIds
|
|
104444
|
+
} : void 0);
|
|
103652
104445
|
};
|
|
103653
104446
|
const bindSessionToThread = (session) => Effect.gen(function* () {
|
|
103654
104447
|
if (session.providerInstanceId === void 0) return yield* new ProviderAdapterRequestError({
|
|
@@ -104581,9 +105374,28 @@ const make$2 = Effect.gen(function* () {
|
|
|
104581
105374
|
const CheckpointReactorLive = Layer.effect(CheckpointReactor, make$2);
|
|
104582
105375
|
//#endregion
|
|
104583
105376
|
//#region src/orchestration/Layers/FusionWatcherReactor.ts
|
|
105377
|
+
const GATE_TIMEOUT_SWEEP_INTERVAL = "10 seconds";
|
|
104584
105378
|
const reviewCommandId = (pairId, sequence) => CommandId.make(`server:fusion:${pairId}:review:${sequence}`);
|
|
104585
105379
|
const cursorCommandId = (pairId, sequence) => CommandId.make(`server:fusion:${pairId}:cursor:${sequence}`);
|
|
104586
105380
|
const reviewMessageId = (pairId, sequence) => MessageId.make(`fusion-review:${pairId}:${sequence}`);
|
|
105381
|
+
const gateCommandId = (pairId, gateId, suffix) => CommandId.make(`server:fusion:${pairId}:gate:${gateId}:${suffix}`);
|
|
105382
|
+
const gateMessageId = (gateId, round) => MessageId.make(`fusion-gate:${gateId}:wake:${round}`);
|
|
105383
|
+
const gateActivityId = (gateId, threadId, suffix) => EventId.make(`fusion-gate:${gateId}:${suffix}:${threadId}`);
|
|
105384
|
+
/**
|
|
105385
|
+
* What the watcher can do, spelled out in every wake. The instructions repeat
|
|
105386
|
+
* per prompt because the watcher has no separate system prompt: these messages
|
|
105387
|
+
* are the only place its powers and its stop condition are stated. The stop
|
|
105388
|
+
* condition matters as much as the powers - a watcher that polls
|
|
105389
|
+
* thread_watch_events waiting for new activity burns its turn spinning, which
|
|
105390
|
+
* is exactly the "supervisor stuck working" failure this text exists to
|
|
105391
|
+
* prevent.
|
|
105392
|
+
*/
|
|
105393
|
+
const watcherPowers = (implementerThreadId) => `You can steer the builder:
|
|
105394
|
+
|
|
105395
|
+
- thread_advise with threadId ${implementerThreadId} posts advice into the builder thread. Use it when the builder should change course, and whenever the user asks you to tell, instruct or guide the builder.
|
|
105396
|
+
- thread_advise with interrupt: true cancels the builder's running turn before the advice lands. Reserve it for scope drift or work that is causing damage right now.
|
|
105397
|
+
|
|
105398
|
+
Never call thread_watch_events repeatedly to wait for new activity, and never wait for the builder to respond: deliver your review and any advice, then end your turn. The server wakes you at the next turn boundary.`;
|
|
104587
105399
|
const watcherPrompt = (input) => `${FUSION_REVIEW_PROMPT_PREFIX}
|
|
104588
105400
|
Review builder thread ${input.implementerThreadId} after its accepted turn completion.
|
|
104589
105401
|
|
|
@@ -104595,14 +105407,361 @@ Give the user a concise status report after every review:
|
|
|
104595
105407
|
- Verification: summarize checks run and their results, including missing verification.
|
|
104596
105408
|
- Assessment: list concrete objections such as correctness risks, missed requirements, regressions, unsafe changes, or unnecessary scope. Cite evidence. If none exist, say "No objections found."
|
|
104597
105409
|
|
|
105410
|
+
${watcherPowers(input.implementerThreadId)}
|
|
105411
|
+
|
|
104598
105412
|
Mention blockers or unfinished work explicitly. Do not work silently. Do not return only ${FUSION_NO_OBJECTION_TEXT}.`;
|
|
105413
|
+
const gateKindDescription = (gate) => {
|
|
105414
|
+
switch (gate.kind) {
|
|
105415
|
+
case "plan": return "the builder finished a proposed plan and is paused for your review before work starts";
|
|
105416
|
+
case "command-approval": return `the builder raised an approval request (request ${gate.requestId ?? "unknown"}) and its turn is blocked until it is answered`;
|
|
105417
|
+
case "repeated-failure": return `the same command failed 3 times (${gate.commandTitle ?? "unknown command"}) and the builder's turn was interrupted for your review`;
|
|
105418
|
+
}
|
|
105419
|
+
};
|
|
105420
|
+
const gatePrompt = (input) => `${FUSION_GATE_PROMPT_PREFIX}
|
|
105421
|
+
Gate ${input.gate.id} is open on builder thread ${input.implementerThreadId}: ${gateKindDescription(input.gate)}. Round ${Math.min(input.gate.round + 1, input.roundCap)} of ${input.roundCap}.
|
|
105422
|
+
|
|
105423
|
+
Read the delta first: call thread_watch_events with threadId ${input.implementerThreadId} and afterSequence ${input.afterSequence}, paging through sequence ${input.throughSequence}. Inspect the repository when useful.
|
|
105424
|
+
|
|
105425
|
+
Then answer with thread_gate_respond, threadId ${input.implementerThreadId}, gateId ${input.gate.id}:
|
|
105426
|
+
|
|
105427
|
+
- decision "approve" clears the gate and lets the builder proceed.
|
|
105428
|
+
- decision "object" with a message delivers your objection to the builder and spends one exchange round. After ${input.roundCap} objections the gate escalates to the user instead.
|
|
105429
|
+
|
|
105430
|
+
If you do not answer within ${Math.round(input.gateTimeoutMs / 1e3)} seconds the gate fails open and is recorded as passed unwatched. Answer the gate, tell the user your reasoning in a few sentences, then end your turn - do not wait for the builder.`;
|
|
104599
105431
|
const make$1 = Effect.gen(function* () {
|
|
104600
105432
|
const orchestrationEngine = yield* OrchestrationEngineService;
|
|
104601
105433
|
const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
|
|
105434
|
+
/**
|
|
105435
|
+
* Consecutive identical command failures per implementer thread. In-memory
|
|
105436
|
+
* on purpose: the counter's turn dies with the server anyway, and the cost
|
|
105437
|
+
* of losing it is one extra failed attempt after a restart, not a stuck
|
|
105438
|
+
* approval.
|
|
105439
|
+
*/
|
|
105440
|
+
const repeatedFailures = /* @__PURE__ */ new Map();
|
|
105441
|
+
/**
|
|
105442
|
+
* Gate detection and human-override resolution act on the CURRENT persisted
|
|
105443
|
+
* gate state, so replaying them against historical events is wrong twice
|
|
105444
|
+
* over: an old plan event would open a fresh gate, and an old user message
|
|
105445
|
+
* would resolve a gate that opened long after it. They therefore run only
|
|
105446
|
+
* for events sequenced after the head observed at startup. The replay-safe
|
|
105447
|
+
* paths (reviews, gate wakes, activities) keep their deterministic command
|
|
105448
|
+
* ids instead.
|
|
105449
|
+
*/
|
|
105450
|
+
let liveEventsAfterSequence = Number.MAX_SAFE_INTEGER;
|
|
105451
|
+
const readPairs = Effect.gen(function* () {
|
|
105452
|
+
const readModel = yield* projectionSnapshotQuery.getCommandReadModel();
|
|
105453
|
+
return {
|
|
105454
|
+
readModel,
|
|
105455
|
+
activePairs: (readModel.threadPairs ?? []).filter((pair) => pair.detachedAt === null)
|
|
105456
|
+
};
|
|
105457
|
+
});
|
|
105458
|
+
const appendGateActivity = Effect.fn("FusionWatcherReactor.appendGateActivity")(function* (input) {
|
|
105459
|
+
yield* orchestrationEngine.dispatch({
|
|
105460
|
+
type: "thread.activity.append",
|
|
105461
|
+
commandId: CommandId.make(`server:fusion:gate:${input.gateId}:${input.suffix}:${input.threadId}`),
|
|
105462
|
+
threadId: input.threadId,
|
|
105463
|
+
activity: {
|
|
105464
|
+
id: gateActivityId(input.gateId, input.threadId, input.suffix),
|
|
105465
|
+
tone: input.tone,
|
|
105466
|
+
kind: input.kind,
|
|
105467
|
+
summary: input.summary,
|
|
105468
|
+
payload: input.payload,
|
|
105469
|
+
turnId: null,
|
|
105470
|
+
createdAt: input.createdAt
|
|
105471
|
+
},
|
|
105472
|
+
createdAt: input.createdAt
|
|
105473
|
+
});
|
|
105474
|
+
});
|
|
105475
|
+
const appendGateActivityToBoth = Effect.fn("FusionWatcherReactor.appendGateActivityToBoth")(function* (input) {
|
|
105476
|
+
yield* appendGateActivity({
|
|
105477
|
+
...input,
|
|
105478
|
+
threadId: input.pair.implementerThreadId
|
|
105479
|
+
});
|
|
105480
|
+
yield* appendGateActivity({
|
|
105481
|
+
...input,
|
|
105482
|
+
threadId: input.pair.watcherThreadId
|
|
105483
|
+
});
|
|
105484
|
+
});
|
|
105485
|
+
/** Starts one watcher turn for the open gate and advances the pair cursor. */
|
|
105486
|
+
const wakeWatcherForGate = Effect.fn("FusionWatcherReactor.wakeWatcherForGate")(function* (event) {
|
|
105487
|
+
const gate = event.payload.gate;
|
|
105488
|
+
if (gate.state !== "awaiting-watcher") return;
|
|
105489
|
+
const { readModel, activePairs } = yield* readPairs;
|
|
105490
|
+
const pair = activePairs.find((candidate) => candidate.id === event.payload.pairId);
|
|
105491
|
+
if (pair === void 0) return;
|
|
105492
|
+
const watcher = readModel.threads.find((thread) => thread.id === pair.watcherThreadId && thread.deletedAt === null);
|
|
105493
|
+
if (watcher === void 0) return;
|
|
105494
|
+
yield* orchestrationEngine.dispatch({
|
|
105495
|
+
type: "thread.turn.start",
|
|
105496
|
+
commandId: gateCommandId(pair.id, gate.id, `wake:${gate.round}`),
|
|
105497
|
+
threadId: watcher.id,
|
|
105498
|
+
message: {
|
|
105499
|
+
messageId: gateMessageId(gate.id, gate.round),
|
|
105500
|
+
role: "user",
|
|
105501
|
+
text: gatePrompt({
|
|
105502
|
+
implementerThreadId: pair.implementerThreadId,
|
|
105503
|
+
gate,
|
|
105504
|
+
roundCap: pair.roundCap,
|
|
105505
|
+
gateTimeoutMs: pair.gateTimeoutMs,
|
|
105506
|
+
afterSequence: pair.lastReviewedImplementerSequence,
|
|
105507
|
+
throughSequence: event.sequence
|
|
105508
|
+
}),
|
|
105509
|
+
attachments: []
|
|
105510
|
+
},
|
|
105511
|
+
runtimeMode: watcher.runtimeMode,
|
|
105512
|
+
interactionMode: watcher.interactionMode,
|
|
105513
|
+
compressMode: watcher.compressMode,
|
|
105514
|
+
unpromptedSubagents: watcher.unpromptedSubagents,
|
|
105515
|
+
createdAt: event.payload.gate.updatedAt
|
|
105516
|
+
});
|
|
105517
|
+
if (event.sequence > pair.lastReviewedImplementerSequence) yield* orchestrationEngine.dispatch({
|
|
105518
|
+
type: "thread-pair.cursor.advance",
|
|
105519
|
+
commandId: cursorCommandId(pair.id, event.sequence),
|
|
105520
|
+
pairId: pair.id,
|
|
105521
|
+
implementerSequence: event.sequence,
|
|
105522
|
+
advancedAt: event.payload.gate.updatedAt
|
|
105523
|
+
});
|
|
105524
|
+
});
|
|
105525
|
+
const processGateOpened = Effect.fn("FusionWatcherReactor.processGateOpened")(function* (event) {
|
|
105526
|
+
const gate = event.payload.gate;
|
|
105527
|
+
yield* appendGateActivityToBoth({
|
|
105528
|
+
pair: event.payload,
|
|
105529
|
+
gateId: gate.id,
|
|
105530
|
+
suffix: "opened",
|
|
105531
|
+
tone: "approval",
|
|
105532
|
+
kind: "fusion.gate.opened",
|
|
105533
|
+
summary: gate.kind === "plan" ? "Gate opened: plan review" : gate.kind === "command-approval" ? "Gate opened: approval review" : "Gate opened: repeated command failures",
|
|
105534
|
+
payload: {
|
|
105535
|
+
pairId: event.payload.pairId,
|
|
105536
|
+
gateId: gate.id,
|
|
105537
|
+
gateKind: gate.kind,
|
|
105538
|
+
...gate.requestId !== null ? { requestId: gate.requestId } : {},
|
|
105539
|
+
...gate.commandTitle !== null ? { commandTitle: gate.commandTitle } : {}
|
|
105540
|
+
},
|
|
105541
|
+
createdAt: event.payload.openedAt
|
|
105542
|
+
});
|
|
105543
|
+
yield* wakeWatcherForGate(event);
|
|
105544
|
+
});
|
|
105545
|
+
const processGateAdvanced = Effect.fn("FusionWatcherReactor.processGateAdvanced")(function* (event) {
|
|
105546
|
+
const gate = event.payload.gate;
|
|
105547
|
+
if (gate.state === "escalated") {
|
|
105548
|
+
yield* appendGateActivityToBoth({
|
|
105549
|
+
pair: event.payload,
|
|
105550
|
+
gateId: gate.id,
|
|
105551
|
+
suffix: "escalated",
|
|
105552
|
+
tone: "approval",
|
|
105553
|
+
kind: "fusion.gate.escalated",
|
|
105554
|
+
summary: "Gate escalated: the builder and supervisor did not converge",
|
|
105555
|
+
payload: {
|
|
105556
|
+
pairId: event.payload.pairId,
|
|
105557
|
+
gateId: gate.id,
|
|
105558
|
+
gateKind: gate.kind,
|
|
105559
|
+
round: gate.round
|
|
105560
|
+
},
|
|
105561
|
+
createdAt: event.payload.advancedAt
|
|
105562
|
+
});
|
|
105563
|
+
return;
|
|
105564
|
+
}
|
|
105565
|
+
yield* wakeWatcherForGate(event);
|
|
105566
|
+
});
|
|
105567
|
+
const processGateResolved = Effect.fn("FusionWatcherReactor.processGateResolved")(function* (event) {
|
|
105568
|
+
const { outcome, resolvedBy, kind, requestId } = event.payload;
|
|
105569
|
+
const unwatched = outcome === "unwatched";
|
|
105570
|
+
yield* appendGateActivityToBoth({
|
|
105571
|
+
pair: event.payload,
|
|
105572
|
+
gateId: event.payload.gateId,
|
|
105573
|
+
suffix: `resolved:${outcome}`,
|
|
105574
|
+
tone: unwatched ? "error" : "approval",
|
|
105575
|
+
kind: unwatched ? "fusion.gate.unwatched" : "fusion.gate.resolved",
|
|
105576
|
+
summary: unwatched ? FUSION_GATE_UNWATCHED_SUMMARY : outcome === "approved" ? "Gate approved by the supervisor" : "Gate answered by the user",
|
|
105577
|
+
payload: {
|
|
105578
|
+
pairId: event.payload.pairId,
|
|
105579
|
+
gateId: event.payload.gateId,
|
|
105580
|
+
gateKind: kind,
|
|
105581
|
+
outcome,
|
|
105582
|
+
resolvedBy
|
|
105583
|
+
},
|
|
105584
|
+
createdAt: event.payload.resolvedAt
|
|
105585
|
+
});
|
|
105586
|
+
if (outcome !== "unwatched" && outcome !== "human") return;
|
|
105587
|
+
if (kind === "command-approval" && requestId !== null && unwatched) yield* orchestrationEngine.dispatch({
|
|
105588
|
+
type: "thread.approval.respond",
|
|
105589
|
+
commandId: gateCommandId(event.payload.pairId, event.payload.gateId, "timeout-accept"),
|
|
105590
|
+
threadId: event.payload.implementerThreadId,
|
|
105591
|
+
requestId: ApprovalRequestId.make(requestId),
|
|
105592
|
+
decision: "accept",
|
|
105593
|
+
createdAt: event.payload.resolvedAt
|
|
105594
|
+
});
|
|
105595
|
+
if (kind === "repeated-failure") {
|
|
105596
|
+
const { readModel } = yield* readPairs;
|
|
105597
|
+
const implementer = readModel.threads.find((thread) => thread.id === event.payload.implementerThreadId && thread.deletedAt === null);
|
|
105598
|
+
if (implementer === void 0) return;
|
|
105599
|
+
if (outcome === "human" && implementer.latestTurn?.state === "running") return;
|
|
105600
|
+
yield* orchestrationEngine.dispatch({
|
|
105601
|
+
type: "thread.turn.start",
|
|
105602
|
+
commandId: gateCommandId(event.payload.pairId, event.payload.gateId, "unblock-continue"),
|
|
105603
|
+
threadId: implementer.id,
|
|
105604
|
+
message: {
|
|
105605
|
+
messageId: MessageId.make(`fusion-gate:${event.payload.gateId}:unblock-continue`),
|
|
105606
|
+
role: "user",
|
|
105607
|
+
text: unwatched ? `${FUSION_GATE_PROMPT_PREFIX} The supervisor did not answer the repeated-failure gate within the timeout. The gate passed unwatched. Continue the work.` : `${FUSION_GATE_PROMPT_PREFIX} The user skipped the repeated-failure gate. Continue the work.`,
|
|
105608
|
+
attachments: []
|
|
105609
|
+
},
|
|
105610
|
+
runtimeMode: implementer.runtimeMode,
|
|
105611
|
+
interactionMode: implementer.interactionMode,
|
|
105612
|
+
compressMode: implementer.compressMode,
|
|
105613
|
+
unpromptedSubagents: implementer.unpromptedSubagents,
|
|
105614
|
+
createdAt: event.payload.resolvedAt
|
|
105615
|
+
});
|
|
105616
|
+
}
|
|
105617
|
+
});
|
|
105618
|
+
const openGate = Effect.fn("FusionWatcherReactor.openGate")(function* (input) {
|
|
105619
|
+
yield* orchestrationEngine.dispatch({
|
|
105620
|
+
type: "thread-pair.gate.open",
|
|
105621
|
+
commandId: CommandId.make(`server:fusion:${input.pair.id}:gate-open:${input.sequence}`),
|
|
105622
|
+
pairId: input.pair.id,
|
|
105623
|
+
gateId: ThreadPairGateId.make(`${input.pair.id}:g${input.sequence}`),
|
|
105624
|
+
kind: input.kind,
|
|
105625
|
+
...input.requestId !== void 0 ? { requestId: ApprovalRequestId.make(input.requestId) } : {},
|
|
105626
|
+
...input.planId !== void 0 ? { planId: input.planId } : {},
|
|
105627
|
+
...input.commandTitle !== void 0 ? { commandTitle: input.commandTitle } : {},
|
|
105628
|
+
openedAt: input.occurredAt
|
|
105629
|
+
});
|
|
105630
|
+
});
|
|
105631
|
+
const processPlanUpserted = Effect.fn("FusionWatcherReactor.processPlanUpserted")(function* (event) {
|
|
105632
|
+
if (event.sequence <= liveEventsAfterSequence) return;
|
|
105633
|
+
const { activePairs } = yield* readPairs;
|
|
105634
|
+
const pair = activePairs.find((candidate) => candidate.implementerThreadId === event.payload.threadId);
|
|
105635
|
+
if (pair === void 0 || pair.activeGate !== null) return;
|
|
105636
|
+
yield* openGate({
|
|
105637
|
+
pair,
|
|
105638
|
+
sequence: event.sequence,
|
|
105639
|
+
kind: "plan",
|
|
105640
|
+
occurredAt: event.occurredAt,
|
|
105641
|
+
planId: event.payload.proposedPlan.id
|
|
105642
|
+
});
|
|
105643
|
+
});
|
|
105644
|
+
const activityRequestId = (payload) => {
|
|
105645
|
+
if (typeof payload !== "object" || payload === null) return void 0;
|
|
105646
|
+
const requestId = payload["requestId"];
|
|
105647
|
+
return typeof requestId === "string" && requestId.length > 0 ? requestId : void 0;
|
|
105648
|
+
};
|
|
105649
|
+
const processActivityAppended = Effect.fn("FusionWatcherReactor.processActivityAppended")(function* (event) {
|
|
105650
|
+
if (event.sequence <= liveEventsAfterSequence) return;
|
|
105651
|
+
const activity = event.payload.activity;
|
|
105652
|
+
const { activePairs } = yield* readPairs;
|
|
105653
|
+
const pair = activePairs.find((candidate) => candidate.implementerThreadId === event.payload.threadId);
|
|
105654
|
+
if (pair === void 0) return;
|
|
105655
|
+
if (activity.kind === "approval.requested") {
|
|
105656
|
+
if (pair.activeGate !== null) return;
|
|
105657
|
+
const requestId = activityRequestId(activity.payload);
|
|
105658
|
+
if (requestId === void 0) return;
|
|
105659
|
+
yield* openGate({
|
|
105660
|
+
pair,
|
|
105661
|
+
sequence: event.sequence,
|
|
105662
|
+
kind: "command-approval",
|
|
105663
|
+
occurredAt: event.occurredAt,
|
|
105664
|
+
requestId
|
|
105665
|
+
});
|
|
105666
|
+
return;
|
|
105667
|
+
}
|
|
105668
|
+
if (activity.kind === "approval.resolved") {
|
|
105669
|
+
const requestId = activityRequestId(activity.payload);
|
|
105670
|
+
const gate = pair.activeGate;
|
|
105671
|
+
if (gate === null || gate.kind !== "command-approval" || requestId === void 0 || gate.requestId !== requestId) return;
|
|
105672
|
+
yield* orchestrationEngine.dispatch({
|
|
105673
|
+
type: "thread-pair.gate.resolve",
|
|
105674
|
+
commandId: gateCommandId(pair.id, gate.id, `external-resolve:${event.sequence}`),
|
|
105675
|
+
pairId: pair.id,
|
|
105676
|
+
gateId: gate.id,
|
|
105677
|
+
outcome: "human",
|
|
105678
|
+
resolvedBy: "human",
|
|
105679
|
+
resolvedAt: event.occurredAt
|
|
105680
|
+
});
|
|
105681
|
+
return;
|
|
105682
|
+
}
|
|
105683
|
+
if (activity.kind === "tool.completed") {
|
|
105684
|
+
const payload = typeof activity.payload === "object" && activity.payload !== null ? activity.payload : void 0;
|
|
105685
|
+
if (payload?.["itemType"] !== "command_execution") return;
|
|
105686
|
+
const key = activity.summary;
|
|
105687
|
+
if (payload["status"] !== "failed") {
|
|
105688
|
+
repeatedFailures.delete(event.payload.threadId);
|
|
105689
|
+
return;
|
|
105690
|
+
}
|
|
105691
|
+
const previous = repeatedFailures.get(event.payload.threadId);
|
|
105692
|
+
const count = previous !== void 0 && previous.key === key ? previous.count + 1 : 1;
|
|
105693
|
+
repeatedFailures.set(event.payload.threadId, {
|
|
105694
|
+
key,
|
|
105695
|
+
count
|
|
105696
|
+
});
|
|
105697
|
+
if (count < 3 || pair.activeGate !== null) return;
|
|
105698
|
+
repeatedFailures.delete(event.payload.threadId);
|
|
105699
|
+
yield* orchestrationEngine.dispatch({
|
|
105700
|
+
type: "thread.turn.interrupt",
|
|
105701
|
+
commandId: CommandId.make(`server:fusion:${pair.id}:gate-interrupt:${event.sequence}`),
|
|
105702
|
+
threadId: pair.implementerThreadId,
|
|
105703
|
+
createdAt: event.occurredAt
|
|
105704
|
+
});
|
|
105705
|
+
yield* openGate({
|
|
105706
|
+
pair,
|
|
105707
|
+
sequence: event.sequence,
|
|
105708
|
+
kind: "repeated-failure",
|
|
105709
|
+
occurredAt: event.occurredAt,
|
|
105710
|
+
commandTitle: key
|
|
105711
|
+
});
|
|
105712
|
+
}
|
|
105713
|
+
});
|
|
105714
|
+
const processApprovalResponseRequested = Effect.fn("FusionWatcherReactor.processApprovalResponseRequested")(function* (event) {
|
|
105715
|
+
if (event.sequence <= liveEventsAfterSequence) return;
|
|
105716
|
+
const { activePairs } = yield* readPairs;
|
|
105717
|
+
const pair = activePairs.find((candidate) => candidate.implementerThreadId === event.payload.threadId);
|
|
105718
|
+
const gate = pair?.activeGate ?? null;
|
|
105719
|
+
if (pair === void 0 || gate === null || gate.kind !== "command-approval" || gate.requestId !== event.payload.requestId) return;
|
|
105720
|
+
yield* orchestrationEngine.dispatch({
|
|
105721
|
+
type: "thread-pair.gate.resolve",
|
|
105722
|
+
commandId: gateCommandId(pair.id, gate.id, `respond-resolve:${event.sequence}`),
|
|
105723
|
+
pairId: pair.id,
|
|
105724
|
+
gateId: gate.id,
|
|
105725
|
+
outcome: "human",
|
|
105726
|
+
resolvedBy: "human",
|
|
105727
|
+
resolvedAt: event.payload.createdAt
|
|
105728
|
+
});
|
|
105729
|
+
});
|
|
105730
|
+
const isFusionProtocolText = (text) => text.startsWith("[fusion-review]") || text.startsWith("[fusion-gate]") || text.startsWith("[fusion-advice]");
|
|
105731
|
+
const processMessageSent = Effect.fn("FusionWatcherReactor.processMessageSent")(function* (event) {
|
|
105732
|
+
if (event.sequence <= liveEventsAfterSequence) return;
|
|
105733
|
+
if (event.payload.role !== "user") return;
|
|
105734
|
+
if (isFusionProtocolText(event.payload.text)) return;
|
|
105735
|
+
const { activePairs } = yield* readPairs;
|
|
105736
|
+
const pair = activePairs.find((candidate) => candidate.implementerThreadId === event.payload.threadId);
|
|
105737
|
+
const gate = pair?.activeGate ?? null;
|
|
105738
|
+
if (pair === void 0 || gate === null) return;
|
|
105739
|
+
yield* orchestrationEngine.dispatch({
|
|
105740
|
+
type: "thread-pair.gate.resolve",
|
|
105741
|
+
commandId: gateCommandId(pair.id, gate.id, `human-message:${event.sequence}`),
|
|
105742
|
+
pairId: pair.id,
|
|
105743
|
+
gateId: gate.id,
|
|
105744
|
+
outcome: "human",
|
|
105745
|
+
resolvedBy: "human",
|
|
105746
|
+
resolvedAt: event.payload.createdAt
|
|
105747
|
+
});
|
|
105748
|
+
});
|
|
104602
105749
|
const processReview = Effect.fn("FusionWatcherReactor.processReview")(function* (pair, completion) {
|
|
104603
105750
|
const readModel = yield* projectionSnapshotQuery.getCommandReadModel();
|
|
104604
105751
|
const currentPair = (readModel.threadPairs ?? []).find((candidate) => candidate.id === pair.id);
|
|
104605
105752
|
if (currentPair === void 0 || currentPair.detachedAt !== null || completion.sequence <= currentPair.lastReviewedImplementerSequence) return;
|
|
105753
|
+
const gate = currentPair.activeGate;
|
|
105754
|
+
if (gate !== null) {
|
|
105755
|
+
if (gate.state === "awaiting-implementer") yield* orchestrationEngine.dispatch({
|
|
105756
|
+
type: "thread-pair.gate.advance",
|
|
105757
|
+
commandId: gateCommandId(currentPair.id, gate.id, `responded:${completion.sequence}`),
|
|
105758
|
+
pairId: currentPair.id,
|
|
105759
|
+
gateId: gate.id,
|
|
105760
|
+
transition: "implementer-responded",
|
|
105761
|
+
advancedAt: completion.occurredAt
|
|
105762
|
+
});
|
|
105763
|
+
return;
|
|
105764
|
+
}
|
|
104606
105765
|
const watcher = readModel.threads.find((thread) => thread.id === currentPair.watcherThreadId && thread.deletedAt === null);
|
|
104607
105766
|
if (watcher === void 0) return;
|
|
104608
105767
|
yield* orchestrationEngine.dispatch({
|
|
@@ -104645,42 +105804,98 @@ const make$1 = Effect.gen(function* () {
|
|
|
104645
105804
|
discard: true
|
|
104646
105805
|
});
|
|
104647
105806
|
});
|
|
104648
|
-
|
|
104649
|
-
|
|
104650
|
-
|
|
104651
|
-
|
|
104652
|
-
|
|
104653
|
-
|
|
104654
|
-
|
|
104655
|
-
|
|
104656
|
-
|
|
105807
|
+
/**
|
|
105808
|
+
* The fail-open timeout, enforced against the persisted gate rather than
|
|
105809
|
+
* any in-memory state, so a server restart mid-gate resumes the countdown
|
|
105810
|
+
* instead of stranding the run.
|
|
105811
|
+
*/
|
|
105812
|
+
const sweepGateTimeouts = Effect.gen(function* () {
|
|
105813
|
+
const now = yield* Clock.currentTimeMillis;
|
|
105814
|
+
const { activePairs } = yield* readPairs;
|
|
105815
|
+
for (const pair of activePairs) {
|
|
105816
|
+
const gate = pair.activeGate;
|
|
105817
|
+
if (gate === null || gate.state !== "awaiting-watcher") continue;
|
|
105818
|
+
if (gate.awaitingWatcherSince === null) continue;
|
|
105819
|
+
const since = Date.parse(gate.awaitingWatcherSince);
|
|
105820
|
+
if (Number.isNaN(since) || now - since < pair.gateTimeoutMs) continue;
|
|
105821
|
+
yield* orchestrationEngine.dispatch({
|
|
105822
|
+
type: "thread-pair.gate.resolve",
|
|
105823
|
+
commandId: gateCommandId(pair.id, gate.id, `timeout:${gate.round}`),
|
|
105824
|
+
pairId: pair.id,
|
|
105825
|
+
gateId: gate.id,
|
|
105826
|
+
outcome: "unwatched",
|
|
105827
|
+
resolvedBy: "timeout",
|
|
105828
|
+
resolvedAt: new Date(now).toISOString()
|
|
104657
105829
|
});
|
|
104658
|
-
return;
|
|
104659
105830
|
}
|
|
104660
|
-
|
|
104661
|
-
|
|
104662
|
-
|
|
104663
|
-
|
|
104664
|
-
|
|
104665
|
-
|
|
105831
|
+
});
|
|
105832
|
+
const FUSION_REACTOR_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
105833
|
+
"thread.turn-completed",
|
|
105834
|
+
"thread.proposed-plan-upserted",
|
|
105835
|
+
"thread.activity-appended",
|
|
105836
|
+
"thread.approval-response-requested",
|
|
105837
|
+
"thread.message-sent",
|
|
105838
|
+
"thread-pair.created",
|
|
105839
|
+
"thread-pair.detached",
|
|
105840
|
+
"thread-pair.gate-opened",
|
|
105841
|
+
"thread-pair.gate-advanced",
|
|
105842
|
+
"thread-pair.gate-resolved"
|
|
105843
|
+
]);
|
|
105844
|
+
const processEvent = Effect.fn("FusionWatcherReactor.processEvent")(function* (event) {
|
|
105845
|
+
switch (event.type) {
|
|
105846
|
+
case "thread.turn-completed": return yield* processCompletion(event);
|
|
105847
|
+
case "thread.proposed-plan-upserted": return yield* processPlanUpserted(event);
|
|
105848
|
+
case "thread.activity-appended": return yield* processActivityAppended(event);
|
|
105849
|
+
case "thread.approval-response-requested": return yield* processApprovalResponseRequested(event);
|
|
105850
|
+
case "thread.message-sent": return yield* processMessageSent(event);
|
|
105851
|
+
case "thread-pair.gate-opened": return yield* processGateOpened(event);
|
|
105852
|
+
case "thread-pair.gate-advanced": return yield* processGateAdvanced(event);
|
|
105853
|
+
case "thread-pair.gate-resolved": return yield* processGateResolved(event);
|
|
105854
|
+
case "thread-pair.created":
|
|
105855
|
+
yield* grantActiveMcpWatchThread({
|
|
105856
|
+
watcherThreadId: event.payload.watcherThreadId,
|
|
105857
|
+
watchedThreadId: event.payload.implementerThreadId
|
|
105858
|
+
});
|
|
105859
|
+
yield* grantActiveMcpAdviseThread({
|
|
105860
|
+
watcherThreadId: event.payload.watcherThreadId,
|
|
105861
|
+
advisedThreadId: event.payload.implementerThreadId
|
|
105862
|
+
});
|
|
105863
|
+
return;
|
|
105864
|
+
case "thread-pair.detached": {
|
|
105865
|
+
const pair = ((yield* projectionSnapshotQuery.getCommandReadModel()).threadPairs ?? []).find((candidate) => candidate.id === event.payload.pairId);
|
|
105866
|
+
if (pair === void 0) return;
|
|
105867
|
+
yield* revokeActiveMcpWatchThread({
|
|
105868
|
+
watcherThreadId: pair.watcherThreadId,
|
|
105869
|
+
watchedThreadId: pair.implementerThreadId
|
|
105870
|
+
});
|
|
105871
|
+
yield* revokeActiveMcpAdviseThread({
|
|
105872
|
+
watcherThreadId: pair.watcherThreadId,
|
|
105873
|
+
advisedThreadId: pair.implementerThreadId
|
|
105874
|
+
});
|
|
105875
|
+
return;
|
|
105876
|
+
}
|
|
105877
|
+
}
|
|
104666
105878
|
});
|
|
104667
105879
|
const processSafely = (event) => processEvent(event).pipe(Effect.catchCause((cause) => {
|
|
104668
105880
|
if (Cause.hasInterruptsOnly(cause)) return Effect.failCause(cause);
|
|
104669
|
-
return Effect.logWarning("fusion watcher reactor failed to process
|
|
105881
|
+
return Effect.logWarning("fusion watcher reactor failed to process event", {
|
|
104670
105882
|
eventType: event.type,
|
|
104671
105883
|
sequence: event.sequence,
|
|
104672
105884
|
cause: Cause.pretty(cause)
|
|
104673
105885
|
});
|
|
104674
105886
|
}));
|
|
104675
105887
|
const worker = yield* makeDrainableWorker(processSafely);
|
|
104676
|
-
const enqueueEvent = (event) =>
|
|
105888
|
+
const enqueueEvent = (event) => FUSION_REACTOR_EVENT_TYPES.has(event.type) ? worker.enqueue(event) : Effect.void;
|
|
104677
105889
|
return {
|
|
104678
105890
|
start: Effect.fn("FusionWatcherReactor.start")(function* () {
|
|
104679
105891
|
yield* Effect.forkScoped(Stream.runForEach(orchestrationEngine.streamDomainEvents, enqueueEvent));
|
|
105892
|
+
yield* Effect.forkScoped(sweepGateTimeouts.pipe(Effect.catchCause((cause) => Effect.logWarning("fusion gate timeout sweep failed", { cause: Cause.pretty(cause) })), Effect.repeat(Schedule.spaced(GATE_TIMEOUT_SWEEP_INTERVAL))));
|
|
104680
105893
|
const headSequence = yield* orchestrationEngine.latestSequence;
|
|
105894
|
+
liveEventsAfterSequence = headSequence;
|
|
104681
105895
|
yield* Stream.runForEach(orchestrationEngine.readEvents(0, Math.max(1, headSequence)), enqueueEvent).pipe(Effect.catchCause((cause) => Effect.logWarning("fusion watcher reactor failed historical replay", { cause: Cause.pretty(cause) })));
|
|
104682
105896
|
}),
|
|
104683
|
-
drain: worker.drain
|
|
105897
|
+
drain: worker.drain,
|
|
105898
|
+
sweepGates: sweepGateTimeouts.pipe(Effect.catchCause((cause) => Effect.logWarning("fusion gate timeout sweep failed", { cause: Cause.pretty(cause) })))
|
|
104684
105899
|
};
|
|
104685
105900
|
});
|
|
104686
105901
|
const FusionWatcherReactorLive = Layer.effect(FusionWatcherReactor, make$1);
|