@p4code/cli 0.3.21 → 0.3.23
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 +446 -24
- package/dist/client/assets/{DiffPanel-1PDQPrji.js → DiffPanel-2403vJ4U.js} +2 -2
- package/dist/client/assets/{FilePreviewPanel-fG-8XhXp.js → FilePreviewPanel-BGsviGU9.js} +2 -2
- package/dist/client/assets/{PreviewPanel-Bhe06XVH.js → PreviewPanel-Cn_Pl6QG.js} +2 -2
- package/dist/client/assets/{PullRequestCodeTab-BSDaFImm.js → PullRequestCodeTab-VGN0mboo.js} +2 -2
- package/dist/client/assets/{fileCommentAnnotations-uDNxJkyo.js → fileCommentAnnotations-B2OUgNal.js} +2 -2
- package/dist/client/assets/{index-BJLN1W_L.css → index-B4xwPQ4-.css} +1 -1
- package/dist/client/assets/{index-4e98duIu.js → index-DjqIibuF.js} +247 -236
- package/dist/client/assets/previewAssetResource-D_N30GH_.js +48 -0
- package/dist/client/assets/{renderFileChildren-DsXde2Pq.js → renderFileChildren-Bf4w5yFu.js} +2 -2
- package/dist/client/assets/{toggle-group-Dz2sUvd_.js → toggle-group-at0phmH9.js} +2 -2
- package/dist/client/index.html +3 -3
- package/package.json +1 -1
- package/dist/client/assets/previewAssetResource-Omzje4BF.js +0 -48
package/dist/bin.mjs
CHANGED
|
@@ -96,6 +96,7 @@ import * as RcMap from "effect/RcMap";
|
|
|
96
96
|
import { FileFinder } from "@ff-labs/fff-node";
|
|
97
97
|
import * as LayerMap from "effect/LayerMap";
|
|
98
98
|
import * as Logger from "effect/Logger";
|
|
99
|
+
import * as PartitionedSemaphore from "effect/PartitionedSemaphore";
|
|
99
100
|
import * as NodeURL from "node:url";
|
|
100
101
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2";
|
|
101
102
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -238,7 +239,7 @@ const make$91 = () => {
|
|
|
238
239
|
const layer$82 = Layer.sync(NetService, make$91);
|
|
239
240
|
//#endregion
|
|
240
241
|
//#region package.json
|
|
241
|
-
var version = "0.3.
|
|
242
|
+
var version = "0.3.23";
|
|
242
243
|
//#endregion
|
|
243
244
|
//#region src/config.ts
|
|
244
245
|
/**
|
|
@@ -1853,6 +1854,13 @@ const FUSION_NO_OBJECTION_TEXT = "[fusion-no-objection]";
|
|
|
1853
1854
|
const FUSION_ADVICE_PROMPT_PREFIX = "[fusion-advice]";
|
|
1854
1855
|
const FUSION_GATE_PROMPT_PREFIX = "[fusion-gate]";
|
|
1855
1856
|
const FUSION_ACTIVATION_PROMPT_PREFIX = "[fusion-activation]";
|
|
1857
|
+
/**
|
|
1858
|
+
* Message-id namespaces for server-generated fusion wakes. Wake detection is
|
|
1859
|
+
* keyed off these rather than message text, which a user or expanded skill
|
|
1860
|
+
* could echo.
|
|
1861
|
+
*/
|
|
1862
|
+
const FUSION_REVIEW_MESSAGE_ID_PREFIX = "fusion-review:";
|
|
1863
|
+
const FUSION_GATE_MESSAGE_ID_PREFIX = "fusion-gate:";
|
|
1856
1864
|
const FUSION_GATE_UNWATCHED_SUMMARY = "Gate passed unwatched";
|
|
1857
1865
|
const FUSION_GATE_DEFAULT_TIMEOUT_MS = 12e4;
|
|
1858
1866
|
const OrchestrationProposedPlanId = TrimmedNonEmptyString;
|
|
@@ -2452,6 +2460,19 @@ const ThreadSessionStopCommand = Schema$1.Struct({
|
|
|
2452
2460
|
threadId: ThreadId,
|
|
2453
2461
|
createdAt: IsoDateTime
|
|
2454
2462
|
});
|
|
2463
|
+
/**
|
|
2464
|
+
* Force-terminates the provider session's owned OS process tree, separate from
|
|
2465
|
+
* the cooperative `thread.session.stop`. Acceptance surfaces through the
|
|
2466
|
+
* persisted command receipt, whose resultSequence points at the typed
|
|
2467
|
+
* `thread.session-force-stop-requested` event; terminal lifecycle stays on the
|
|
2468
|
+
* authoritative `thread.session-set`.
|
|
2469
|
+
*/
|
|
2470
|
+
const ThreadSessionForceStopCommand = Schema$1.Struct({
|
|
2471
|
+
type: Schema$1.Literal("thread.session.force-stop"),
|
|
2472
|
+
commandId: CommandId,
|
|
2473
|
+
threadId: ThreadId,
|
|
2474
|
+
createdAt: IsoDateTime
|
|
2475
|
+
});
|
|
2455
2476
|
const ThreadPairCreateCommand = Schema$1.Struct({
|
|
2456
2477
|
type: Schema$1.Literal("thread-pair.create"),
|
|
2457
2478
|
commandId: CommandId,
|
|
@@ -2509,6 +2530,7 @@ const DispatchableClientOrchestrationCommand = Schema$1.Union([
|
|
|
2509
2530
|
ThreadUserInputRespondCommand,
|
|
2510
2531
|
ThreadCheckpointRevertCommand,
|
|
2511
2532
|
ThreadSessionStopCommand,
|
|
2533
|
+
ThreadSessionForceStopCommand,
|
|
2512
2534
|
ThreadPairCreateCommand,
|
|
2513
2535
|
ThreadPairDetachCommand,
|
|
2514
2536
|
ThreadPairGateResolveCommand
|
|
@@ -2539,6 +2561,7 @@ const ClientOrchestrationCommand = Schema$1.Union([
|
|
|
2539
2561
|
ThreadUserInputRespondCommand,
|
|
2540
2562
|
ThreadCheckpointRevertCommand,
|
|
2541
2563
|
ThreadSessionStopCommand,
|
|
2564
|
+
ThreadSessionForceStopCommand,
|
|
2542
2565
|
ThreadPairCreateCommand,
|
|
2543
2566
|
ThreadPairDetachCommand,
|
|
2544
2567
|
ThreadPairGateResolveCommand
|
|
@@ -2550,6 +2573,19 @@ const ThreadSessionSetCommand = Schema$1.Struct({
|
|
|
2550
2573
|
session: OrchestrationSession,
|
|
2551
2574
|
createdAt: IsoDateTime
|
|
2552
2575
|
});
|
|
2576
|
+
/**
|
|
2577
|
+
* Server-only: atomic convergence of a confirmed force stop. The decider
|
|
2578
|
+
* expands it into durable cancellations for every open approval/user-input
|
|
2579
|
+
* request plus the authoritative `thread.session-set`, committed in one
|
|
2580
|
+
* transaction so a crash can never expose partially cancelled requests.
|
|
2581
|
+
*/
|
|
2582
|
+
const ThreadSessionForceStopConvergeCommand = Schema$1.Struct({
|
|
2583
|
+
type: Schema$1.Literal("thread.session.force-stop.converge"),
|
|
2584
|
+
commandId: CommandId,
|
|
2585
|
+
threadId: ThreadId,
|
|
2586
|
+
session: OrchestrationSession,
|
|
2587
|
+
createdAt: IsoDateTime
|
|
2588
|
+
});
|
|
2553
2589
|
const ThreadTurnCompleteCommand = Schema$1.Struct({
|
|
2554
2590
|
type: Schema$1.Literal("thread.turn.complete"),
|
|
2555
2591
|
commandId: CommandId,
|
|
@@ -2648,6 +2684,7 @@ const ThreadRevertCompleteCommand = Schema$1.Struct({
|
|
|
2648
2684
|
});
|
|
2649
2685
|
const InternalOrchestrationCommand = Schema$1.Union([
|
|
2650
2686
|
ThreadSessionSetCommand,
|
|
2687
|
+
ThreadSessionForceStopConvergeCommand,
|
|
2651
2688
|
ThreadTurnCompleteCommand,
|
|
2652
2689
|
ThreadPairCursorAdvanceCommand,
|
|
2653
2690
|
ThreadPairGateOpenCommand,
|
|
@@ -2688,6 +2725,7 @@ const OrchestrationEventType = Schema$1.Literals([
|
|
|
2688
2725
|
"thread.checkpoint-revert-requested",
|
|
2689
2726
|
"thread.reverted",
|
|
2690
2727
|
"thread.session-stop-requested",
|
|
2728
|
+
"thread.session-force-stop-requested",
|
|
2691
2729
|
"thread.session-set",
|
|
2692
2730
|
"thread.proposed-plan-upserted",
|
|
2693
2731
|
"thread.turn-diff-completed",
|
|
@@ -2883,6 +2921,10 @@ const ThreadSessionStopRequestedPayload = Schema$1.Struct({
|
|
|
2883
2921
|
threadId: ThreadId,
|
|
2884
2922
|
createdAt: IsoDateTime
|
|
2885
2923
|
});
|
|
2924
|
+
const ThreadSessionForceStopRequestedPayload = Schema$1.Struct({
|
|
2925
|
+
threadId: ThreadId,
|
|
2926
|
+
createdAt: IsoDateTime
|
|
2927
|
+
});
|
|
2886
2928
|
const ThreadSessionSetPayload$1 = Schema$1.Struct({
|
|
2887
2929
|
threadId: ThreadId,
|
|
2888
2930
|
session: OrchestrationSession
|
|
@@ -3124,6 +3166,11 @@ const OrchestrationEvent = Schema$1.Union([
|
|
|
3124
3166
|
type: Schema$1.Literal("thread.session-stop-requested"),
|
|
3125
3167
|
payload: ThreadSessionStopRequestedPayload
|
|
3126
3168
|
}),
|
|
3169
|
+
Schema$1.Struct({
|
|
3170
|
+
...EventBaseFields,
|
|
3171
|
+
type: Schema$1.Literal("thread.session-force-stop-requested"),
|
|
3172
|
+
payload: ThreadSessionForceStopRequestedPayload
|
|
3173
|
+
}),
|
|
3127
3174
|
Schema$1.Struct({
|
|
3128
3175
|
...EventBaseFields,
|
|
3129
3176
|
type: Schema$1.Literal("thread.session-set"),
|
|
@@ -6481,6 +6528,23 @@ const ProviderInterruptTurnInput = Schema$1.Struct({
|
|
|
6481
6528
|
turnId: Schema$1.optional(TurnId)
|
|
6482
6529
|
});
|
|
6483
6530
|
const ProviderStopSessionInput = Schema$1.Struct({ threadId: ThreadId });
|
|
6531
|
+
/**
|
|
6532
|
+
* Result of a force stop. `unsupported` means a live session exists on an
|
|
6533
|
+
* adapter without a registered owned process (the Claude Agent SDK owns its
|
|
6534
|
+
* child privately), so live force termination is refused rather than faked.
|
|
6535
|
+
*/
|
|
6536
|
+
const ProviderForceStopSessionOutcome = Schema$1.Literals([
|
|
6537
|
+
"terminated",
|
|
6538
|
+
"already-exited",
|
|
6539
|
+
"kill-timeout",
|
|
6540
|
+
"no-owned-process",
|
|
6541
|
+
"unsupported"
|
|
6542
|
+
]);
|
|
6543
|
+
Schema$1.Struct({
|
|
6544
|
+
outcome: ProviderForceStopSessionOutcome,
|
|
6545
|
+
provider: Schema$1.NullOr(Schema$1.String),
|
|
6546
|
+
pid: Schema$1.NullOr(Schema$1.Number)
|
|
6547
|
+
});
|
|
6484
6548
|
const ProviderRespondToRequestInput = Schema$1.Struct({
|
|
6485
6549
|
threadId: ThreadId,
|
|
6486
6550
|
requestId: ApprovalRequestId,
|
|
@@ -11619,7 +11683,7 @@ const WsSkillsPublishRpc = Rpc.make(WS_METHODS.skillsPublish, {
|
|
|
11619
11683
|
error: Schema$1.Union([EnvironmentAuthorizationError])
|
|
11620
11684
|
});
|
|
11621
11685
|
const WsSkillsPublishAllRpc = Rpc.make(WS_METHODS.skillsPublishAll, {
|
|
11622
|
-
payload: Schema$1.Struct({}),
|
|
11686
|
+
payload: Schema$1.Struct({ kind: Schema$1.optional(AgentAssetKind) }),
|
|
11623
11687
|
success: Schema$1.Struct({
|
|
11624
11688
|
report: AgentSyncReport,
|
|
11625
11689
|
status: AgentSyncStatus
|
|
@@ -23829,10 +23893,10 @@ const make$75 = Effect.gen(function* () {
|
|
|
23829
23893
|
}]
|
|
23830
23894
|
};
|
|
23831
23895
|
}).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset publish failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
|
|
23832
|
-
const publishAll = Effect.gen(function* () {
|
|
23896
|
+
const publishAll = (kind) => Effect.gen(function* () {
|
|
23833
23897
|
if (!(yield* isConfigured)) return EMPTY_REPORT;
|
|
23834
23898
|
const status = yield* assetStatus;
|
|
23835
|
-
const candidates = status.entries.filter((entry) => entry.state === "unmanaged");
|
|
23899
|
+
const candidates = status.entries.filter((entry) => entry.state === "unmanaged" && (kind === void 0 || entry.kind === kind));
|
|
23836
23900
|
const published = [];
|
|
23837
23901
|
const conflicts = [];
|
|
23838
23902
|
for (const candidate of candidates) {
|
|
@@ -23846,10 +23910,10 @@ const make$75 = Effect.gen(function* () {
|
|
|
23846
23910
|
return {
|
|
23847
23911
|
...EMPTY_REPORT,
|
|
23848
23912
|
ran: true,
|
|
23849
|
-
roots: status.roots,
|
|
23913
|
+
roots: status.roots.filter((root) => kind === void 0 || root.kind === kind),
|
|
23850
23914
|
published,
|
|
23851
23915
|
conflicts,
|
|
23852
|
-
refused: status.refused
|
|
23916
|
+
refused: status.refused.filter((refusal) => kind === void 0 || refusal.kind === kind)
|
|
23853
23917
|
};
|
|
23854
23918
|
}).pipe(Effect.provide(services), Effect.catchCause((cause) => Effect.logWarning("asset publish-all failed", { cause }).pipe(Effect.as(EMPTY_REPORT))));
|
|
23855
23919
|
const unpublish = ({ kind, name }) => Effect.gen(function* () {
|
|
@@ -24609,7 +24673,7 @@ const makeAssetCommands = (words) => {
|
|
|
24609
24673
|
name: nameArgument.pipe(Argument.optional),
|
|
24610
24674
|
all: Flag.boolean("all").pipe(Flag.withDescription("Share everything local this machine does not already share."))
|
|
24611
24675
|
}).pipe(Command.withDescription(`Share local ${words.plural} through the hub, so every machine gets them.`), Command.withHandler((flags) => runWithSync(flags, ({ sync }) => Effect.gen(function* () {
|
|
24612
|
-
if (flags.all) return yield* sync.publishAll.pipe(Effect.flatMap((report) => Console.log(formatReport(report))));
|
|
24676
|
+
if (flags.all) return yield* sync.publishAll().pipe(Effect.flatMap((report) => Console.log(formatReport(report))));
|
|
24613
24677
|
const name = Option.getOrUndefined(flags.name);
|
|
24614
24678
|
if (name === void 0) return yield* Console.error(`Name a ${words.singular}, or pass --all to share every unshared one.`);
|
|
24615
24679
|
return yield* sync.publish({
|
|
@@ -27177,6 +27241,99 @@ const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand")(funct
|
|
|
27177
27241
|
createdAt: command.createdAt
|
|
27178
27242
|
}
|
|
27179
27243
|
};
|
|
27244
|
+
case "thread.session.force-stop":
|
|
27245
|
+
yield* requireThread({
|
|
27246
|
+
readModel,
|
|
27247
|
+
command,
|
|
27248
|
+
threadId: command.threadId
|
|
27249
|
+
});
|
|
27250
|
+
return {
|
|
27251
|
+
...yield* withEventBase({
|
|
27252
|
+
aggregateKind: "thread",
|
|
27253
|
+
aggregateId: command.threadId,
|
|
27254
|
+
occurredAt: command.createdAt,
|
|
27255
|
+
commandId: command.commandId
|
|
27256
|
+
}),
|
|
27257
|
+
type: "thread.session-force-stop-requested",
|
|
27258
|
+
payload: {
|
|
27259
|
+
threadId: command.threadId,
|
|
27260
|
+
createdAt: command.createdAt
|
|
27261
|
+
}
|
|
27262
|
+
};
|
|
27263
|
+
case "thread.session.force-stop.converge": {
|
|
27264
|
+
const thread = yield* requireThread({
|
|
27265
|
+
readModel,
|
|
27266
|
+
command,
|
|
27267
|
+
threadId: command.threadId
|
|
27268
|
+
});
|
|
27269
|
+
const openRequests = /* @__PURE__ */ new Map();
|
|
27270
|
+
for (const activity of thread.activities) {
|
|
27271
|
+
const payload = typeof activity.payload === "object" && activity.payload !== null ? activity.payload : null;
|
|
27272
|
+
const requestId = typeof payload?.requestId === "string" ? payload.requestId : null;
|
|
27273
|
+
if (requestId === null) continue;
|
|
27274
|
+
if (activity.kind === "approval.requested") openRequests.set(requestId, {
|
|
27275
|
+
category: "approval",
|
|
27276
|
+
requestedAt: activity.createdAt
|
|
27277
|
+
});
|
|
27278
|
+
else if (activity.kind === "user-input.requested") openRequests.set(requestId, {
|
|
27279
|
+
category: "user-input",
|
|
27280
|
+
requestedAt: activity.createdAt
|
|
27281
|
+
});
|
|
27282
|
+
else if (activity.kind === "approval.resolved" || activity.kind === "user-input.resolved") openRequests.delete(requestId);
|
|
27283
|
+
}
|
|
27284
|
+
let resolvedAtMillis = Date.parse(command.createdAt);
|
|
27285
|
+
for (const request of openRequests.values()) {
|
|
27286
|
+
const requestedAtMillis = Date.parse(request.requestedAt);
|
|
27287
|
+
if (!Number.isNaN(requestedAtMillis)) resolvedAtMillis = Math.max(resolvedAtMillis, requestedAtMillis + 1);
|
|
27288
|
+
}
|
|
27289
|
+
const resolvedAt = DateTime.formatIso(DateTime.makeUnsafe(resolvedAtMillis));
|
|
27290
|
+
const events = [];
|
|
27291
|
+
for (const [requestId, request] of openRequests) {
|
|
27292
|
+
const base = yield* withEventBase({
|
|
27293
|
+
aggregateKind: "thread",
|
|
27294
|
+
aggregateId: command.threadId,
|
|
27295
|
+
occurredAt: resolvedAt,
|
|
27296
|
+
commandId: command.commandId
|
|
27297
|
+
});
|
|
27298
|
+
events.push({
|
|
27299
|
+
...base,
|
|
27300
|
+
type: "thread.activity-appended",
|
|
27301
|
+
payload: {
|
|
27302
|
+
threadId: command.threadId,
|
|
27303
|
+
activity: {
|
|
27304
|
+
id: base.eventId,
|
|
27305
|
+
tone: "info",
|
|
27306
|
+
kind: request.category === "approval" ? "approval.resolved" : "user-input.resolved",
|
|
27307
|
+
summary: request.category === "approval" ? "Approval request cancelled by force stop" : "User input request cancelled by force stop",
|
|
27308
|
+
payload: {
|
|
27309
|
+
requestId,
|
|
27310
|
+
detail: "Cancelled by session force stop."
|
|
27311
|
+
},
|
|
27312
|
+
turnId: null,
|
|
27313
|
+
createdAt: resolvedAt
|
|
27314
|
+
}
|
|
27315
|
+
}
|
|
27316
|
+
});
|
|
27317
|
+
}
|
|
27318
|
+
events.push({
|
|
27319
|
+
...yield* withEventBase({
|
|
27320
|
+
aggregateKind: "thread",
|
|
27321
|
+
aggregateId: command.threadId,
|
|
27322
|
+
occurredAt: resolvedAt,
|
|
27323
|
+
commandId: command.commandId,
|
|
27324
|
+
metadata: {}
|
|
27325
|
+
}),
|
|
27326
|
+
type: "thread.session-set",
|
|
27327
|
+
payload: {
|
|
27328
|
+
threadId: command.threadId,
|
|
27329
|
+
session: {
|
|
27330
|
+
...command.session,
|
|
27331
|
+
updatedAt: resolvedAt
|
|
27332
|
+
}
|
|
27333
|
+
}
|
|
27334
|
+
});
|
|
27335
|
+
return events;
|
|
27336
|
+
}
|
|
27180
27337
|
case "thread.session.set": {
|
|
27181
27338
|
const thread = yield* requireThread({
|
|
27182
27339
|
readModel,
|
|
@@ -64086,9 +64243,9 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker) => WsRpcGroup.t
|
|
|
64086
64243
|
status: yield* assetSync.status
|
|
64087
64244
|
};
|
|
64088
64245
|
}), { "rpc.aggregate": "skills" }),
|
|
64089
|
-
[WS_METHODS.skillsPublishAll]: (
|
|
64246
|
+
[WS_METHODS.skillsPublishAll]: (input) => observeRpcEffect$1(WS_METHODS.skillsPublishAll, Effect.gen(function* () {
|
|
64090
64247
|
return {
|
|
64091
|
-
report: yield* assetSync.publishAll,
|
|
64248
|
+
report: yield* assetSync.publishAll(input.kind),
|
|
64092
64249
|
status: yield* assetSync.status
|
|
64093
64250
|
};
|
|
64094
64251
|
}), { "rpc.aggregate": "skills" }),
|
|
@@ -65016,6 +65173,102 @@ const ProviderEventLoggersLive = Layer.effect(ProviderEventLoggers, Effect.gen(f
|
|
|
65016
65173
|
};
|
|
65017
65174
|
}));
|
|
65018
65175
|
//#endregion
|
|
65176
|
+
//#region src/provider/ProviderOwnedProcessRegistry.ts
|
|
65177
|
+
/**
|
|
65178
|
+
* ProviderOwnedProcessRegistry - threadId to owned provider process handles.
|
|
65179
|
+
*
|
|
65180
|
+
* Long-lived provider session runtimes (Codex, ACP for Cursor/Grok, Muse,
|
|
65181
|
+
* OpenCode) register the child process they spawned for a thread. Force stop
|
|
65182
|
+
* kills only through a registered handle while that handle still observes its
|
|
65183
|
+
* process as running. The Claude Agent SDK owns its child privately and never
|
|
65184
|
+
* registers here; live force termination for it is unsupported by design.
|
|
65185
|
+
*
|
|
65186
|
+
* Identity model (fail-closed):
|
|
65187
|
+
* - `handle.isRunning` derives from the spawner's exit Deferred, resolved by
|
|
65188
|
+
* Node's kernel-held child handle - it identifies the direct child without
|
|
65189
|
+
* probing numeric PIDs, so a recycled PID can never look "running".
|
|
65190
|
+
* - Kill is attempted only while the handle observes the child running. On
|
|
65191
|
+
* POSIX the child leads its own process group, and a live leader keeps that
|
|
65192
|
+
* pgid owned, so the spawner's group kill cannot hit a recycled group.
|
|
65193
|
+
* - Once the handle observes exit, nothing is ever signalled: cleanup of
|
|
65194
|
+
* surviving detached grandchildren after leader exit is explicitly
|
|
65195
|
+
* unsupported, because no safe identity for the group remains. The residual
|
|
65196
|
+
* check-to-kill window is the same one the spawner's own scope finalizer
|
|
65197
|
+
* has, and is not widened here.
|
|
65198
|
+
*
|
|
65199
|
+
* Concurrency: every mutation of the registry - register, deregister, exit
|
|
65200
|
+
* observation, force stop - runs inside the same per-thread critical section,
|
|
65201
|
+
* so a force stop can never act on a replaced or exited generation.
|
|
65202
|
+
*/
|
|
65203
|
+
const FORCE_STOP_SIGKILL_ESCALATION = Duration.seconds(2);
|
|
65204
|
+
const FORCE_STOP_EXIT_BOUND = Duration.seconds(5);
|
|
65205
|
+
let generationCounter = 0;
|
|
65206
|
+
const entries = /* @__PURE__ */ new Map();
|
|
65207
|
+
const threadLocks = PartitionedSemaphore.makeUnsafe({ permits: 1 });
|
|
65208
|
+
/**
|
|
65209
|
+
* Records the owned child process for a thread and watches its exit so a dead
|
|
65210
|
+
* generation can never be killed. Returns the registration to pass back to
|
|
65211
|
+
* {@link deregisterOwnedProcess} on scope close. Re-registering a thread
|
|
65212
|
+
* replaces the previous generation.
|
|
65213
|
+
*/
|
|
65214
|
+
const registerOwnedProcess = (input) => threadLocks.withPermit(input.threadId)(Effect.gen(function* () {
|
|
65215
|
+
generationCounter += 1;
|
|
65216
|
+
const entry = {
|
|
65217
|
+
threadId: input.threadId,
|
|
65218
|
+
generation: generationCounter,
|
|
65219
|
+
handle: input.handle,
|
|
65220
|
+
exited: false
|
|
65221
|
+
};
|
|
65222
|
+
entries.set(input.threadId, entry);
|
|
65223
|
+
yield* input.handle.exitCode.pipe(Effect.ignore, Effect.andThen(threadLocks.withPermit(input.threadId)(Effect.sync(() => {
|
|
65224
|
+
entry.exited = true;
|
|
65225
|
+
if (entries.get(input.threadId) === entry) entries.delete(input.threadId);
|
|
65226
|
+
}))), Effect.forkDetach);
|
|
65227
|
+
return {
|
|
65228
|
+
threadId: input.threadId,
|
|
65229
|
+
generation: entry.generation
|
|
65230
|
+
};
|
|
65231
|
+
}));
|
|
65232
|
+
/** Removes the registration if this exact generation still owns the thread. */
|
|
65233
|
+
const deregisterOwnedProcess = (registration) => threadLocks.withPermit(registration.threadId)(Effect.sync(() => {
|
|
65234
|
+
if (entries.get(registration.threadId)?.generation === registration.generation) entries.delete(registration.threadId);
|
|
65235
|
+
}));
|
|
65236
|
+
/**
|
|
65237
|
+
* Force-terminates the registered owned process for a thread. The whole
|
|
65238
|
+
* decision-and-kill runs in the per-thread critical section, so the entry it
|
|
65239
|
+
* captured cannot be replaced or reaped concurrently. Bounded and idempotent:
|
|
65240
|
+
* repeats after the process is gone report `no-owned-process` or
|
|
65241
|
+
* `already-exited` without signalling anything, and a `kill-timeout` keeps the
|
|
65242
|
+
* registration for retry instead of pretending convergence.
|
|
65243
|
+
*/
|
|
65244
|
+
const forceStopOwnedProcess = (threadId, options) => threadLocks.withPermit(threadId)(Effect.gen(function* () {
|
|
65245
|
+
const entry = entries.get(threadId);
|
|
65246
|
+
if (entry === void 0) return { outcome: "no-owned-process" };
|
|
65247
|
+
const pid = Number(entry.handle.pid);
|
|
65248
|
+
const running = yield* entry.handle.isRunning.pipe(Effect.orElseSucceed(() => false));
|
|
65249
|
+
if (entry.exited || !running) {
|
|
65250
|
+
entry.exited = true;
|
|
65251
|
+
if (entries.get(threadId) === entry) entries.delete(threadId);
|
|
65252
|
+
return {
|
|
65253
|
+
outcome: "already-exited",
|
|
65254
|
+
pid
|
|
65255
|
+
};
|
|
65256
|
+
}
|
|
65257
|
+
if (!(yield* entry.handle.kill({
|
|
65258
|
+
killSignal: "SIGTERM",
|
|
65259
|
+
forceKillAfter: options?.sigkillAfter ?? FORCE_STOP_SIGKILL_ESCALATION
|
|
65260
|
+
}).pipe(Effect.andThen(entry.handle.exitCode.pipe(Effect.ignore)), Effect.timeout(options?.exitBound ?? FORCE_STOP_EXIT_BOUND), Effect.as(true), Effect.orElseSucceed(() => false)))) return {
|
|
65261
|
+
outcome: "kill-timeout",
|
|
65262
|
+
pid
|
|
65263
|
+
};
|
|
65264
|
+
entry.exited = true;
|
|
65265
|
+
if (entries.get(threadId) === entry) entries.delete(threadId);
|
|
65266
|
+
return {
|
|
65267
|
+
outcome: "terminated",
|
|
65268
|
+
pid
|
|
65269
|
+
};
|
|
65270
|
+
}));
|
|
65271
|
+
//#endregion
|
|
65019
65272
|
//#region src/mcp/McpProviderSession.ts
|
|
65020
65273
|
/**
|
|
65021
65274
|
* The name p4code declares its own toolkit under, in every provider.
|
|
@@ -65826,6 +66079,53 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
|
|
|
65826
66079
|
outcomeAttributes: () => providerMetricAttributes(metricProvider, { operation: "stop" })
|
|
65827
66080
|
}));
|
|
65828
66081
|
});
|
|
66082
|
+
const forceStopSession = Effect.fn("forceStopSession")(function* (rawInput) {
|
|
66083
|
+
const input = yield* decodeInputOrValidationError({
|
|
66084
|
+
operation: "ProviderService.forceStopSession",
|
|
66085
|
+
schema: ProviderStopSessionInput,
|
|
66086
|
+
payload: rawInput
|
|
66087
|
+
});
|
|
66088
|
+
const routed = yield* resolveRoutableSession({
|
|
66089
|
+
threadId: input.threadId,
|
|
66090
|
+
operation: "ProviderService.forceStopSession",
|
|
66091
|
+
allowRecovery: false
|
|
66092
|
+
}).pipe(Effect.catchCause(() => Effect.succeed(null)));
|
|
66093
|
+
const owned = yield* forceStopOwnedProcess(input.threadId);
|
|
66094
|
+
const provider = routed?.adapter.provider ?? null;
|
|
66095
|
+
if (owned.outcome === "no-owned-process" && routed !== null && routed.isActive) return {
|
|
66096
|
+
outcome: "unsupported",
|
|
66097
|
+
provider,
|
|
66098
|
+
pid: null
|
|
66099
|
+
};
|
|
66100
|
+
if (owned.outcome === "kill-timeout") {
|
|
66101
|
+
yield* analytics.record("provider.session.force-stopped", {
|
|
66102
|
+
provider: provider ?? "unknown",
|
|
66103
|
+
outcome: owned.outcome
|
|
66104
|
+
});
|
|
66105
|
+
return {
|
|
66106
|
+
outcome: owned.outcome,
|
|
66107
|
+
provider,
|
|
66108
|
+
pid: owned.pid
|
|
66109
|
+
};
|
|
66110
|
+
}
|
|
66111
|
+
yield* clearMcpSession(input.threadId);
|
|
66112
|
+
if (routed !== null) yield* directory.upsert({
|
|
66113
|
+
threadId: input.threadId,
|
|
66114
|
+
provider: routed.adapter.provider,
|
|
66115
|
+
providerInstanceId: routed.instanceId,
|
|
66116
|
+
status: "stopped",
|
|
66117
|
+
runtimePayload: { activeTurnId: null }
|
|
66118
|
+
});
|
|
66119
|
+
yield* analytics.record("provider.session.force-stopped", {
|
|
66120
|
+
provider: provider ?? "unknown",
|
|
66121
|
+
outcome: owned.outcome
|
|
66122
|
+
});
|
|
66123
|
+
return {
|
|
66124
|
+
outcome: owned.outcome,
|
|
66125
|
+
provider,
|
|
66126
|
+
pid: "pid" in owned ? owned.pid : null
|
|
66127
|
+
};
|
|
66128
|
+
});
|
|
65829
66129
|
const listSessions = Effect.fn("listSessions")(function* () {
|
|
65830
66130
|
const currentAdapters = yield* getAdapterEntries;
|
|
65831
66131
|
const activeSessions = (yield* Effect.forEach(currentAdapters, ([instanceId, adapter]) => adapter.listSessions().pipe(Effect.map((sessions) => sessions.map((session) => ({
|
|
@@ -65928,6 +66228,7 @@ const makeProviderService = Effect.fn("makeProviderService")(function* (options)
|
|
|
65928
66228
|
respondToRequest,
|
|
65929
66229
|
respondToUserInput,
|
|
65930
66230
|
stopSession,
|
|
66231
|
+
forceStopSession,
|
|
65931
66232
|
listSessions,
|
|
65932
66233
|
getCapabilities,
|
|
65933
66234
|
getInstanceInfo,
|
|
@@ -66418,6 +66719,13 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
|
|
|
66418
66719
|
});
|
|
66419
66720
|
const terminateChild = killOpenCodeProcessGroup("SIGTERM").pipe(Effect.andThen(Effect.sleep("1 second")), Effect.andThen(killOpenCodeProcessGroup("SIGKILL")), Effect.ignore);
|
|
66420
66721
|
yield* Scope.addFinalizer(runtimeScope, terminateChild);
|
|
66722
|
+
if (input.threadId !== void 0) {
|
|
66723
|
+
const ownedProcess = yield* registerOwnedProcess({
|
|
66724
|
+
threadId: input.threadId,
|
|
66725
|
+
handle: child
|
|
66726
|
+
});
|
|
66727
|
+
yield* Scope.addFinalizer(runtimeScope, deregisterOwnedProcess(ownedProcess));
|
|
66728
|
+
}
|
|
66421
66729
|
const stdoutRef = yield* Ref.make("");
|
|
66422
66730
|
const stderrRef = yield* Ref.make("");
|
|
66423
66731
|
const readyDeferred = yield* Deferred.make();
|
|
@@ -66478,7 +66786,8 @@ const makeOpenCodeRuntime = Effect.gen(function* () {
|
|
|
66478
66786
|
...input.environment !== void 0 ? { environment: input.environment } : {},
|
|
66479
66787
|
...input.port !== void 0 ? { port: input.port } : {},
|
|
66480
66788
|
...input.hostname !== void 0 ? { hostname: input.hostname } : {},
|
|
66481
|
-
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {}
|
|
66789
|
+
...input.timeoutMs !== void 0 ? { timeoutMs: input.timeoutMs } : {},
|
|
66790
|
+
...input.threadId !== void 0 ? { threadId: input.threadId } : {}
|
|
66482
66791
|
}).pipe(Effect.map((server) => ({
|
|
66483
66792
|
url: server.url,
|
|
66484
66793
|
exitCode: server.exitCode,
|
|
@@ -90764,6 +91073,11 @@ const makeCodexSessionRuntime = (options) => Effect.gen(function* () {
|
|
|
90764
91073
|
command: `${options.binaryPath} app-server`,
|
|
90765
91074
|
cause
|
|
90766
91075
|
})));
|
|
91076
|
+
const ownedProcess = yield* registerOwnedProcess({
|
|
91077
|
+
threadId: options.threadId,
|
|
91078
|
+
handle: child
|
|
91079
|
+
});
|
|
91080
|
+
yield* Scope.addFinalizer(runtimeScope, deregisterOwnedProcess(ownedProcess));
|
|
90767
91081
|
const clientContext = yield* layerChildProcess$1(child).pipe(Layer.build, Effect.provideService(Scope.Scope, runtimeScope));
|
|
90768
91082
|
const client = yield* Effect.service(CodexAppServerClient).pipe(Effect.provide(clientContext));
|
|
90769
91083
|
const serverNotifications = yield* Queue.unbounded();
|
|
@@ -96798,6 +97112,13 @@ const make$6 = (options) => Effect.gen(function* () {
|
|
|
96798
97112
|
command: options.spawn.command,
|
|
96799
97113
|
cause
|
|
96800
97114
|
})));
|
|
97115
|
+
if (options.threadId !== void 0) {
|
|
97116
|
+
const ownedProcess = yield* registerOwnedProcess({
|
|
97117
|
+
threadId: options.threadId,
|
|
97118
|
+
handle: child
|
|
97119
|
+
});
|
|
97120
|
+
yield* Scope.addFinalizer(runtimeScope, deregisterOwnedProcess(ownedProcess));
|
|
97121
|
+
}
|
|
96801
97122
|
const acpContext = yield* Layer.build(layerChildProcess(child, {
|
|
96802
97123
|
...options.protocolLogging?.logIncoming !== void 0 ? { logIncoming: options.protocolLogging.logIncoming } : {},
|
|
96803
97124
|
...options.protocolLogging?.logOutgoing !== void 0 ? { logOutgoing: options.protocolLogging.logOutgoing } : {},
|
|
@@ -98577,6 +98898,7 @@ function makeCursorAdapter(cursorSettings, options) {
|
|
|
98577
98898
|
cursorSettings: effectiveCursorSettings,
|
|
98578
98899
|
...options?.environment ? { environment: options.environment } : {},
|
|
98579
98900
|
childProcessSpawner,
|
|
98901
|
+
threadId: input.threadId,
|
|
98580
98902
|
cwd,
|
|
98581
98903
|
...resumeSessionId ? { resumeSessionId } : {},
|
|
98582
98904
|
clientInfo: {
|
|
@@ -99810,6 +100132,7 @@ function makeGrokAdapter(grokSettings, options) {
|
|
|
99810
100132
|
grokSettings,
|
|
99811
100133
|
...options?.environment ? { environment: options.environment } : {},
|
|
99812
100134
|
childProcessSpawner,
|
|
100135
|
+
threadId: input.threadId,
|
|
99813
100136
|
cwd,
|
|
99814
100137
|
...resumeSessionId ? { resumeSessionId } : {},
|
|
99815
100138
|
clientInfo: {
|
|
@@ -101294,6 +101617,11 @@ const runMuseExec = Effect.fn("runMuseExec")(function* (input) {
|
|
|
101294
101617
|
detail: "Failed to spawn `muse exec`.",
|
|
101295
101618
|
cause
|
|
101296
101619
|
})));
|
|
101620
|
+
const ownedProcess = yield* registerOwnedProcess({
|
|
101621
|
+
threadId: input.threadId,
|
|
101622
|
+
handle: child
|
|
101623
|
+
});
|
|
101624
|
+
yield* Scope.addFinalizer(runScope, deregisterOwnedProcess(ownedProcess));
|
|
101297
101625
|
const stderrRef = yield* Ref.make("");
|
|
101298
101626
|
const interruptedRef = yield* Ref.make(false);
|
|
101299
101627
|
const stdoutFiber = yield* child.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.runForEach(input.onLine), Effect.ignore, Effect.forkIn(runScope));
|
|
@@ -103181,6 +103509,7 @@ function makeOpenCodeAdapter(openCodeSettings, options) {
|
|
|
103181
103509
|
const server = yield* openCodeRuntime.connectToOpenCodeServer({
|
|
103182
103510
|
binaryPath,
|
|
103183
103511
|
serverUrl,
|
|
103512
|
+
threadId: input.threadId,
|
|
103184
103513
|
...options?.environment ? { environment: options.environment } : {}
|
|
103185
103514
|
});
|
|
103186
103515
|
const client = openCodeRuntime.createOpenCodeSdkClient({
|
|
@@ -105204,6 +105533,22 @@ const resolvePendingMcpUserInput = Effect.fn("pendingMcpUserInputs.resolve")(fun
|
|
|
105204
105533
|
const forgetPendingMcpUserInput = (threadId, requestId) => {
|
|
105205
105534
|
if (pending.get(requestId)?.threadId === threadId) pending.delete(requestId);
|
|
105206
105535
|
};
|
|
105536
|
+
/** Ids of the questions a thread has open right now (force-stop snapshot). */
|
|
105537
|
+
const listPendingMcpUserInputRequestIds = (threadId) => [...pending].filter(([, request]) => request.threadId === threadId).map(([requestId]) => requestId);
|
|
105538
|
+
/**
|
|
105539
|
+
* Cancels one pending MCP question (force stop). Interrupts the Deferred
|
|
105540
|
+
* before dropping the entry so the MCP handler blocked on it completes
|
|
105541
|
+
* instead of leaking a suspended fiber. Targeted by request id; the caller
|
|
105542
|
+
* owns the guarantee that the id belongs to the session being killed -
|
|
105543
|
+
* either a pre-kill snapshot, or a re-list taken while the doomed session's
|
|
105544
|
+
* generation is confirmed unchanged and before any stopped state is written.
|
|
105545
|
+
*/
|
|
105546
|
+
const failPendingMcpUserInput = Effect.fn("pendingMcpUserInputs.fail")(function* (threadId, requestId) {
|
|
105547
|
+
const request = pending.get(requestId);
|
|
105548
|
+
if (request?.threadId !== threadId) return;
|
|
105549
|
+
pending.delete(requestId);
|
|
105550
|
+
yield* Deferred.interrupt(request.answers);
|
|
105551
|
+
});
|
|
105207
105552
|
//#endregion
|
|
105208
105553
|
//#region src/mcp/toolkits/threads/handlers.ts
|
|
105209
105554
|
const DEFAULT_MEMORY_APPEND_TARGET = "CLAUDE.md";
|
|
@@ -107524,7 +107869,22 @@ const DEFAULT_RUNTIME_MODE = "full-access";
|
|
|
107524
107869
|
const DEFAULT_THREAD_TITLE = "New thread";
|
|
107525
107870
|
const NON_SYSTEM_PROVIDER_STRUCTURED_USER_QUESTIONS = structuredUserQuestionPrompt("your provider's structured user-input question tool");
|
|
107526
107871
|
const FUSION_PROMOTION_INSTRUCTIONS = `Work independently in this normal thread. Fusion is a silent escalation path, not a startup procedure. Do not inspect Fusion tools/skill, mention Fusion status, or announce that Fusion was not invoked. First analyze the task normally. Only if that analysis reveals a concrete unresolved tradeoff, correctness risk, or design decision materially needing a second opinion, stop before implementation, propose Fusion, and ask the user for explicit approval. The user may approve with ordinary affirmative text such as "approved"; /fusion or $fusion also authorizes Fusion directly without a prior proposal. Do not activate, spawn, or promote until one of those authorizations arrives. UI work, complex logic, task size, unfamiliarity, or duration alone never qualifies.`;
|
|
107527
|
-
const FUSION_BUILDER_INSTRUCTIONS = `You are Fusion Builder in an already-created native server pair. Server owns pairing and coordination. Do not inspect or invoke the Fusion skill, create/pair/rename threads, or announce/setup Fusion. Start the user's task directly. Before editing, create and maintain a
|
|
107872
|
+
const FUSION_BUILDER_INSTRUCTIONS = `You are Fusion Builder in an already-created native server pair. Server owns pairing and coordination. Do not inspect or invoke the Fusion skill, create/pair/rename threads, or announce/setup Fusion. Start the user's task directly. Before editing, create and maintain the phase list in the same plan/todo tool you use for ordinary step tracking, never the MCP task board tools - one entry per phase in order, exactly one in progress at a time, marked completed at each phase end - so phases render in the task banner. That list holds phase entries only for the whole task; keep step-level or per-file todos out of it. Prose alone leaves the banner empty. Split it into the fewest substantial phases the task genuinely needs plus a final integration/whole-task phase; most tasks need one to three work phases. Each phase is a complete reviewable slice of behavior. Never split per file, per function, or per trivial step: over-splitting spends review turns instead of finishing the job. Add a phase only when a real review boundary, risky decision, or independent behavior separates the work. Complete exactly one phase per turn, and finish the whole phase in that turn rather than stopping early. Do not run tests, typecheck, lint, or builds per phase; write the tests the change needs, then run verification once in the final phase over the whole task. Exception: a phase whose own correctness is unclear may run the single narrowest check that resolves it. End every phase turn with phase completed, todo status, changed behavior/files, and remaining phases; do not start the next phase in the same turn. Server then wakes the paired Supervisor, which resumes you through ${FUSION_ADVICE_PROMPT_PREFIX}; a user message may also revise or resume the work. Final phase verifies the entire task against the original request and labels it ready for whole-task review. Supervisor is unreachable during your turn. Never spawn/use another Supervisor thread/subagent or attribute Supervisor decisions without ${FUSION_ADVICE_PROMPT_PREFIX}. Within the current phase, continue when straightforward or evidence is clear. For a concrete unresolved tradeoff, correctness risk, or design decision materially needing judgment, stop safely before the risky choice; final response states the exact question and why review is needed. Evaluate/follow Supervisor advice unless conflicting with user request or verified repo state.`;
|
|
107873
|
+
const FUSION_WATCHER_INSTRUCTIONS = `You are Fusion Supervisor (watcher) in an already-created native server pair. Server owns pairing and coordination and wakes you with ${FUSION_REVIEW_PROMPT_PREFIX} or ${FUSION_GATE_PROMPT_PREFIX} prompts at builder turn boundaries; this message arrived outside such a wake, so your conversational memory of the pair may be gone. The pair metadata below is authoritative: the builder thread exists and is the counterpart thread id. Never report that no builder thread exists. To resume supervision, read builder events with thread_watch_events from lastReviewedImplementerSequence with limit 50, paging forward with the last returned sequence rather than requesting a whole range at once, derive phase from artifacts (git log/status, PR, builder events), steer with thread_advise, and answer an open gate with thread_gate_respond. When a review or gate wake prompt specifies an explicit event range, that range wins over this metadata. Never poll or wait for the builder; deliver review or advice, then end the turn.`;
|
|
107874
|
+
const isFusionWatcherWakeMessageId = (messageId) => messageId.startsWith("fusion-review:") || messageId.startsWith("fusion-gate:");
|
|
107875
|
+
const fusionPairContext = (pair, role) => {
|
|
107876
|
+
const counterpartThreadId = role === "implementer" ? pair.watcherThreadId : pair.implementerThreadId;
|
|
107877
|
+
return [
|
|
107878
|
+
"[fusion-pair]",
|
|
107879
|
+
`role: ${role}`,
|
|
107880
|
+
`pairId: ${pair.id}`,
|
|
107881
|
+
`counterpartThreadId: ${counterpartThreadId}`,
|
|
107882
|
+
`roundCap: ${pair.roundCap}`,
|
|
107883
|
+
`gateTimeoutMs: ${pair.gateTimeoutMs}`,
|
|
107884
|
+
`lastReviewedImplementerSequence: ${pair.lastReviewedImplementerSequence}`,
|
|
107885
|
+
`activeGate: ${pair.activeGate === null ? "none" : JSON.stringify(pair.activeGate)}`
|
|
107886
|
+
].join("\n");
|
|
107887
|
+
};
|
|
107528
107888
|
function providerErrorLabel(value) {
|
|
107529
107889
|
const normalized = value?.trim();
|
|
107530
107890
|
return normalized && normalized.length > 0 ? normalized : "unknown";
|
|
@@ -107609,6 +107969,7 @@ const make$3 = Effect.gen(function* () {
|
|
|
107609
107969
|
const vcsStatusBroadcaster = yield* VcsStatusBroadcaster;
|
|
107610
107970
|
const textGeneration = yield* TextGeneration;
|
|
107611
107971
|
const serverSettingsService = yield* ServerSettingsService;
|
|
107972
|
+
const threadBackgroundLiveness = yield* ThreadBackgroundLivenessService;
|
|
107612
107973
|
const serverConfig = yield* ServerConfig$1;
|
|
107613
107974
|
const serverCommandId = (tag) => crypto.randomUUIDv4.pipe(Effect.map((uuid) => CommandId.make(`server:${tag}:${uuid}`)));
|
|
107614
107975
|
const serverEventId = () => crypto.randomUUIDv4.pipe(Effect.map(EventId.make));
|
|
@@ -108094,7 +108455,7 @@ const make$3 = Effect.gen(function* () {
|
|
|
108094
108455
|
].filter((part) => part !== void 0).join("\n\n");
|
|
108095
108456
|
const activeFusionPair = ((yield* projectionSnapshotQuery.getCommandReadModel()).threadPairs ?? []).find((pair) => pair.detachedAt === null && (pair.implementerThreadId === input.threadId || pair.watcherThreadId === input.threadId));
|
|
108096
108457
|
const isFusionBuilder = activeFusionPair?.implementerThreadId === input.threadId;
|
|
108097
|
-
const fusionInput = expandedInputWithDocuments === void 0 ? void 0 : activeFusionPair === void 0 ? `${FUSION_PROMOTION_INSTRUCTIONS}\n\n${expandedInputWithDocuments}` : isFusionBuilder ? `${FUSION_BUILDER_INSTRUCTIONS}\n\n${expandedInputWithDocuments}` : expandedInputWithDocuments
|
|
108458
|
+
const fusionInput = expandedInputWithDocuments === void 0 ? void 0 : activeFusionPair === void 0 ? `${FUSION_PROMOTION_INSTRUCTIONS}\n\n${expandedInputWithDocuments}` : isFusionBuilder ? `${FUSION_BUILDER_INSTRUCTIONS}\n\n${fusionPairContext(activeFusionPair, "implementer")}\n\n${expandedInputWithDocuments}` : isFusionWatcherWakeMessageId(input.messageId) ? expandedInputWithDocuments : `${FUSION_WATCHER_INSTRUCTIONS}\n\n${fusionPairContext(activeFusionPair, "watcher")}\n\n${expandedInputWithDocuments}`;
|
|
108098
108459
|
const activeSession = yield* providerService.listSessions().pipe(Effect.map((sessions) => sessions.find((session) => session.threadId === input.threadId)));
|
|
108099
108460
|
const providerHasStructuredQuestionSystemPrompt = activeSession?.provider === "claudeAgent" || activeSession?.provider === "codex";
|
|
108100
108461
|
const inputWithStructuredQuestionPolicy = fusionInput !== void 0 && !providerHasStructuredQuestionSystemPrompt ? `${NON_SYSTEM_PROVIDER_STRUCTURED_USER_QUESTIONS}\n\n${fusionInput}` : fusionInput;
|
|
@@ -108260,6 +108621,7 @@ const make$3 = Effect.gen(function* () {
|
|
|
108260
108621
|
})));
|
|
108261
108622
|
const sendTurnRequest = yield* buildSendTurnRequestForThread({
|
|
108262
108623
|
threadId: event.payload.threadId,
|
|
108624
|
+
messageId: message.id,
|
|
108263
108625
|
messageText: message.text,
|
|
108264
108626
|
...message.attachments !== void 0 ? { attachments: message.attachments } : {},
|
|
108265
108627
|
...event.payload.modelSelection !== void 0 ? { modelSelection: event.payload.modelSelection } : {},
|
|
@@ -108368,6 +108730,54 @@ const make$3 = Effect.gen(function* () {
|
|
|
108368
108730
|
createdAt: now
|
|
108369
108731
|
});
|
|
108370
108732
|
});
|
|
108733
|
+
const processSessionForceStopRequested = Effect.fn("processSessionForceStopRequested")(function* (event) {
|
|
108734
|
+
const thread = yield* resolveThread(event.payload.threadId);
|
|
108735
|
+
if (!thread) return;
|
|
108736
|
+
const now = event.payload.createdAt;
|
|
108737
|
+
const pendingInputIdsBeforeKill = listPendingMcpUserInputRequestIds(thread.id);
|
|
108738
|
+
const result = yield* providerService.forceStopSession({ threadId: thread.id }).pipe(Effect.catchCause((cause) => Effect.logWarning("provider session force stop failed", {
|
|
108739
|
+
threadId: thread.id,
|
|
108740
|
+
cause: Cause.pretty(cause)
|
|
108741
|
+
}).pipe(Effect.as(null))));
|
|
108742
|
+
if (result === null || result.outcome === "kill-timeout" || result.outcome === "unsupported") {
|
|
108743
|
+
yield* appendProviderFailureActivity({
|
|
108744
|
+
threadId: thread.id,
|
|
108745
|
+
kind: "provider.session.force-stop.failed",
|
|
108746
|
+
summary: "Force stop failed",
|
|
108747
|
+
detail: result === null ? "Force stop failed before reaching the provider process." : result.outcome === "unsupported" ? `The active ${result.provider ?? "provider"} session does not expose a server-owned process; force stop is not supported while it runs.` : `The provider process (pid ${result.pid}) did not exit within the bound; the session was left running for retry.`,
|
|
108748
|
+
turnId: thread.session?.activeTurnId ?? null,
|
|
108749
|
+
createdAt: now
|
|
108750
|
+
});
|
|
108751
|
+
return;
|
|
108752
|
+
}
|
|
108753
|
+
yield* Effect.forEach(pendingInputIdsBeforeKill, (requestId) => failPendingMcpUserInput(thread.id, requestId), { discard: true });
|
|
108754
|
+
const current = yield* resolveThread(event.payload.threadId);
|
|
108755
|
+
if (!current) return;
|
|
108756
|
+
if (!((current.session?.updatedAt ?? null) === (thread.session?.updatedAt ?? null) && (current.session?.activeTurnId ?? null) === (thread.session?.activeTurnId ?? null))) {
|
|
108757
|
+
yield* Effect.logInfo("force stop superseded by a newer session; skipping convergence", { threadId: thread.id });
|
|
108758
|
+
return;
|
|
108759
|
+
}
|
|
108760
|
+
yield* Effect.forEach(listPendingMcpUserInputRequestIds(thread.id), (requestId) => failPendingMcpUserInput(thread.id, requestId), { discard: true });
|
|
108761
|
+
yield* serverCommandId("provider-session-force-converge").pipe(Effect.flatMap((commandId) => orchestrationEngine.dispatch({
|
|
108762
|
+
type: "thread.session.force-stop.converge",
|
|
108763
|
+
commandId,
|
|
108764
|
+
threadId: thread.id,
|
|
108765
|
+
session: {
|
|
108766
|
+
threadId: thread.id,
|
|
108767
|
+
status: "stopped",
|
|
108768
|
+
providerName: current.session?.providerName ?? null,
|
|
108769
|
+
...current.session?.providerInstanceId !== void 0 ? { providerInstanceId: current.session.providerInstanceId } : {},
|
|
108770
|
+
runtimeMode: current.session?.runtimeMode ?? DEFAULT_RUNTIME_MODE,
|
|
108771
|
+
activeTurnId: null,
|
|
108772
|
+
lastError: current.session?.lastError ?? null,
|
|
108773
|
+
updatedAt: now
|
|
108774
|
+
},
|
|
108775
|
+
createdAt: now
|
|
108776
|
+
})));
|
|
108777
|
+
yield* Effect.all([flushQueuedSettle(thread.id), flushQueuedWorkspaceCleanup(thread.id)], { discard: true });
|
|
108778
|
+
threadSessionRulesetModes.delete(thread.id);
|
|
108779
|
+
threadBackgroundLiveness.clearThreadLiveness(thread.id);
|
|
108780
|
+
});
|
|
108371
108781
|
const processDomainEvent = Effect.fn("processDomainEvent")(function* (event) {
|
|
108372
108782
|
yield* Effect.annotateCurrentSpan({
|
|
108373
108783
|
"orchestration.event_type": event.type,
|
|
@@ -108398,6 +108808,9 @@ const make$3 = Effect.gen(function* () {
|
|
|
108398
108808
|
case "thread.session-stop-requested":
|
|
108399
108809
|
yield* processSessionStopRequested(event);
|
|
108400
108810
|
return;
|
|
108811
|
+
case "thread.session-force-stop-requested":
|
|
108812
|
+
yield* processSessionForceStopRequested(event);
|
|
108813
|
+
return;
|
|
108401
108814
|
}
|
|
108402
108815
|
});
|
|
108403
108816
|
const processDomainEventSafely = (event) => processDomainEvent(event).pipe(Effect.catchCause((cause) => {
|
|
@@ -108408,15 +108821,20 @@ const make$3 = Effect.gen(function* () {
|
|
|
108408
108821
|
});
|
|
108409
108822
|
}));
|
|
108410
108823
|
const worker = yield* makeDrainableWorker(processDomainEventSafely);
|
|
108824
|
+
const forceStopOutstanding = yield* TxRef.make(0);
|
|
108825
|
+
const forceStopContext = yield* Effect.context();
|
|
108826
|
+
const enqueueForceStop = (event) => Effect.uninterruptible(Effect.tx(TxRef.update(forceStopOutstanding, (n) => n + 1)).pipe(Effect.andThen(processDomainEventSafely(event).pipe(Effect.ensuring(Effect.tx(TxRef.update(forceStopOutstanding, (n) => n - 1))), Effect.provideContext(forceStopContext), Effect.forkScoped)), Effect.asVoid));
|
|
108827
|
+
const forceStopDrain = TxRef.get(forceStopOutstanding).pipe(Effect.tap((n) => n > 0 ? Effect.txRetry : Effect.void), Effect.tx);
|
|
108411
108828
|
return {
|
|
108412
108829
|
start: Effect.fn("start")(function* () {
|
|
108413
108830
|
const processEvent = Effect.fn("processEvent")(function* (event) {
|
|
108831
|
+
if (event.type === "thread.session-force-stop-requested") return yield* enqueueForceStop(event);
|
|
108414
108832
|
if (event.type === "thread.runtime-mode-set" || event.type === "thread.turn-start-requested" || event.type === "thread.turn-interrupt-requested" || event.type === "thread.approval-response-requested" || event.type === "thread.user-input-response-requested" || event.type === "thread.session-stop-requested") return yield* worker.enqueue(event);
|
|
108415
108833
|
});
|
|
108416
108834
|
yield* Effect.forkScoped(Stream.runForEach(orchestrationEngine.streamDomainEvents, processEvent));
|
|
108417
108835
|
yield* restorePendingWorkspaceCleanups().pipe(Effect.catchCause((cause) => Effect.logWarning("startup thread workspace cleanup recovery failed", { cause })));
|
|
108418
108836
|
}),
|
|
108419
|
-
drain: worker.drain
|
|
108837
|
+
drain: Effect.all([worker.drain, forceStopDrain], { discard: true }).pipe(Effect.asVoid)
|
|
108420
108838
|
};
|
|
108421
108839
|
});
|
|
108422
108840
|
const ProviderCommandReactorLive = Layer.effect(ProviderCommandReactor, make$3);
|
|
@@ -108937,9 +109355,9 @@ const CheckpointReactorLive = Layer.effect(CheckpointReactor, make$2);
|
|
|
108937
109355
|
const GATE_TIMEOUT_SWEEP_INTERVAL = "10 seconds";
|
|
108938
109356
|
const reviewCommandId = (pairId, sequence) => CommandId.make(`server:fusion:${pairId}:review:${sequence}`);
|
|
108939
109357
|
const cursorCommandId = (pairId, sequence) => CommandId.make(`server:fusion:${pairId}:cursor:${sequence}`);
|
|
108940
|
-
const reviewMessageId = (pairId, sequence) => MessageId.make(
|
|
109358
|
+
const reviewMessageId = (pairId, sequence) => MessageId.make(`${FUSION_REVIEW_MESSAGE_ID_PREFIX}${pairId}:${sequence}`);
|
|
108941
109359
|
const gateCommandId = (pairId, gateId, suffix) => CommandId.make(`server:fusion:${pairId}:gate:${gateId}:${suffix}`);
|
|
108942
|
-
const gateMessageId = (gateId, round) => MessageId.make(
|
|
109360
|
+
const gateMessageId = (gateId, round) => MessageId.make(`${FUSION_GATE_MESSAGE_ID_PREFIX}${gateId}:wake:${round}`);
|
|
108943
109361
|
const gateActivityId = (gateId, threadId, suffix) => EventId.make(`fusion-gate:${gateId}:${suffix}:${threadId}`);
|
|
108944
109362
|
const pairFailureCommandId = (pairId, sequence, suffix) => CommandId.make(`server:fusion:${pairId}:provider-failure:${sequence}:${suffix}`);
|
|
108945
109363
|
const pairFailureActivityId = (pairId, sequence, threadId) => EventId.make(`fusion-provider-failure:${pairId}:${sequence}:${threadId}`);
|
|
@@ -108967,34 +109385,38 @@ Never poll thread_watch_events or wait for builder. Deliver review/advice, end t
|
|
|
108967
109385
|
* definition of done and its recovery procedure must arrive with each wake,
|
|
108968
109386
|
* and the current phase must be derived from artifacts rather than remembered.
|
|
108969
109387
|
*/
|
|
108970
|
-
const deliveryProtocol = (implementerThreadId) => `
|
|
109388
|
+
const deliveryProtocol = (implementerThreadId) => `Delivery loop runs only when the user's task authorizes delivery (commit, push, PR). Never infer authorization from finished code, green checks, or a clean review.
|
|
109389
|
+
|
|
109390
|
+
Authorized - completion requires artifact proof from builder events, git, PR - never memory:
|
|
108971
109391
|
|
|
108972
109392
|
1. Feature/fix branch, explicit-path staging, conventional commit, rebased latest main.
|
|
108973
109393
|
2. Open PR; builder reports commit and PR URL.
|
|
108974
109394
|
3. Global pr-reviewer reviewed branch/PR; confirmed findings fixed; affected checks green; follow-up review has no actionable findings.
|
|
108975
109395
|
|
|
108976
|
-
|
|
109396
|
+
Authorized and code done/no objections but conditions missing: thread_advise ${implementerThreadId} with next delivery step - commit, rebase, PR, global pr-reviewer, fixes, affected checks, repeat until clean. Require final commit and PR URL.
|
|
109397
|
+
|
|
109398
|
+
Not authorized: task is complete once code and final-phase verification are done. Report final state and the changed uncommitted paths to the user in this supervisor thread, say delivery needs their authorization, and end the turn. Nothing blocks the builder here, so do not use waitForUser; the prerequisite rule above applies only when the builder reports it is stuck mid-task awaiting authorization. Never advise the builder to branch, stage, commit, push, or open a PR, and never count missing delivery as unfinished work or an objection.
|
|
108977
109399
|
|
|
108978
109400
|
Never instruct/report merge; user owns merging. After restart/context loss, derive phase from workspace git log/status, PR, recent builder events.`;
|
|
108979
109401
|
const watcherPrompt = (input) => `${FUSION_REVIEW_PROMPT_PREFIX}
|
|
108980
109402
|
Review completed builder turn ${input.implementerThreadId}.
|
|
108981
109403
|
|
|
108982
|
-
Call thread_watch_events, threadId ${input.implementerThreadId}, afterSequence ${input.afterSequence};
|
|
109404
|
+
Call thread_watch_events, threadId ${input.implementerThreadId}, afterSequence ${input.afterSequence}, limit 50; repeat with the last returned event's sequence as afterSequence until you reach ${input.throughSequence}. Never request a whole range in one call: an oversized page exceeds the tool output cap and wastes the turn. Inspect repo when useful.
|
|
108983
109405
|
|
|
108984
109406
|
Always report concise:
|
|
108985
109407
|
|
|
108986
109408
|
- Progress: work done; completeness.
|
|
108987
|
-
- Verification: checks/results
|
|
109409
|
+
- Verification: final phase only - checks/results, missing proof. Intermediate phases: report "deferred to final phase" and never demand per-phase checks.
|
|
108988
109410
|
- Assessment: evidence-cited correctness risks, missed requirements, regressions, unsafe/unneeded scope. None: "No objections found."
|
|
108989
109411
|
|
|
108990
109412
|
Determine review boundary from builder's todo status:
|
|
108991
109413
|
|
|
108992
|
-
- Remaining phases, or final status unclear: intermediate phase review. Always call thread_advise for ${input.implementerThreadId}. With objections, send required corrections and next phase. With none, explicitly approve phase and tell builder to continue next todo phase.
|
|
108993
|
-
- Final whole-task phase ready: call thread_watch_events again
|
|
109414
|
+
- Remaining phases, or final status unclear: intermediate phase review. Always call thread_advise for ${input.implementerThreadId}. With objections, send required corrections and next phase. With none, explicitly approve phase and tell builder to continue next todo phase. Never require tests, typecheck, lint, or builds before the final phase.
|
|
109415
|
+
- Final whole-task phase ready: call thread_watch_events again from afterSequence 0 through ${input.throughSequence}, same limit 50 paging, then inspect full task diff/state. Review original requirements, integration across all phases, verification, and delivery. Advise only for concrete objections or unfinished work; otherwise report no objections and end.
|
|
108994
109416
|
|
|
108995
109417
|
${watcherPowers(input.implementerThreadId)}
|
|
108996
109418
|
|
|
108997
|
-
Builder-blocking external prerequisite only: when the builder explicitly reports it cannot process the request until the user supplies access, authentication, credentials, permission, or another unavailable external prerequisite, call thread_advise with waitForUser true, ask the user in this supervisor thread, and end. That result does not wake the builder. Wait for user action or reply; only then call thread_advise normally with the confirmed result. Never send waiting/pending status to the builder because that starts another review cycle. Do not pause for ordinary review objections, design choices, or human-authority decisions the builder can safely await itself.
|
|
109419
|
+
Builder-blocking external prerequisite only: when the builder explicitly reports it cannot process the request until the user supplies access, authentication, credentials, permission, delivery authorization to commit/push/open a PR, or another unavailable external prerequisite, call thread_advise with waitForUser true, ask the user in this supervisor thread, and end. That result does not wake the builder. Wait for user action or reply; only then call thread_advise normally with the confirmed result. Never send waiting/pending status to the builder because that starts another review cycle. Do not pause for ordinary review objections, design choices, or human-authority decisions the builder can safely await itself.
|
|
108998
109420
|
|
|
108999
109421
|
${deliveryProtocol(input.implementerThreadId)}
|
|
109000
109422
|
|
|
@@ -109009,7 +109431,7 @@ const gateKindDescription = (gate) => {
|
|
|
109009
109431
|
const gatePrompt = (input) => `${FUSION_GATE_PROMPT_PREFIX}
|
|
109010
109432
|
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}.
|
|
109011
109433
|
|
|
109012
|
-
First read delta: thread_watch_events, threadId ${input.implementerThreadId}, afterSequence ${input.afterSequence};
|
|
109434
|
+
First read delta: thread_watch_events, threadId ${input.implementerThreadId}, afterSequence ${input.afterSequence}, limit 50; repeat with the last returned event's sequence as afterSequence until you reach ${input.throughSequence}. Never request a whole range in one call: an oversized page exceeds the tool output cap and wastes the turn. Inspect repo when useful.
|
|
109013
109435
|
|
|
109014
109436
|
Then thread_gate_respond, threadId ${input.implementerThreadId}, gateId ${input.gate.id}:
|
|
109015
109437
|
|
|
@@ -109232,7 +109654,7 @@ const make$1 = Effect.gen(function* () {
|
|
|
109232
109654
|
commandId: gateCommandId(event.payload.pairId, event.payload.gateId, "unblock-continue"),
|
|
109233
109655
|
threadId: implementer.id,
|
|
109234
109656
|
message: {
|
|
109235
|
-
messageId: MessageId.make(
|
|
109657
|
+
messageId: MessageId.make(`${FUSION_GATE_MESSAGE_ID_PREFIX}${event.payload.gateId}:unblock-continue`),
|
|
109236
109658
|
role: "user",
|
|
109237
109659
|
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.`,
|
|
109238
109660
|
attachments: []
|