@p4code/cli 0.6.15 → 0.6.17
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 +280 -34
- package/dist/client/assets/DiffPanel-vLwzeivL.js +98 -0
- package/dist/client/assets/{FilePreviewPanel-Xd7rNhEc.js → FilePreviewPanel-_FKtLfoQ.js} +4 -4
- package/dist/client/assets/PreviewPanel-DFsRFkuE.js +2 -0
- package/dist/client/assets/PullRequestCodeTab-tcvVxcBi.js +251 -0
- package/dist/client/assets/{fileCommentAnnotations-3__-nxMy.js → fileCommentAnnotations-D-4zF4gD.js} +2 -2
- package/dist/client/assets/{index-UKht9nt4.js → index-DMIHPPHL.js} +243 -233
- package/dist/client/assets/previewAssetResource-CscIFHNv.js +48 -0
- package/dist/client/assets/{renderFileChildren-4kqXMwk5.js → renderFileChildren-DhC88Tru.js} +2 -2
- package/dist/client/assets/{toggle-group-BgpxQt7v.js → toggle-group-DrOJrfFB.js} +2 -2
- package/dist/client/index.html +2 -2
- package/dist/resource-monitor/darwin-arm64/p4-resource-monitor +0 -0
- package/dist/resource-monitor/darwin-x64/p4-resource-monitor +0 -0
- package/dist/resource-monitor/linux-x64/p4-resource-monitor +0 -0
- package/dist/resource-monitor/win32-x64/p4-resource-monitor.exe +0 -0
- package/package.json +1 -1
- package/dist/client/assets/DiffPanel-B2uEx-Ip.js +0 -98
- package/dist/client/assets/PreviewPanel-UqzJdhZ2.js +0 -2
- package/dist/client/assets/PullRequestCodeTab-DCdVX6w6.js +0 -251
- package/dist/client/assets/previewAssetResource-OVkEskN2.js +0 -48
package/dist/bin.mjs
CHANGED
|
@@ -268,7 +268,7 @@ const make$124 = () => {
|
|
|
268
268
|
const layer$124 = Layer.sync(NetService, make$124);
|
|
269
269
|
//#endregion
|
|
270
270
|
//#region package.json
|
|
271
|
-
var version$1 = "0.6.
|
|
271
|
+
var version$1 = "0.6.17";
|
|
272
272
|
//#endregion
|
|
273
273
|
//#region src/config.ts
|
|
274
274
|
/**
|
|
@@ -3284,6 +3284,8 @@ const FUSION_GATE_PROMPT_PREFIX = "[fusion-gate]";
|
|
|
3284
3284
|
const FUSION_ACTIVATION_PROMPT_PREFIX = "[fusion-activation]";
|
|
3285
3285
|
/** Server message that returns a Builder to its unfinished phase on Jev's advice. */
|
|
3286
3286
|
const FUSION_JEV_PROMPT_PREFIX = "[fusion-jev]";
|
|
3287
|
+
/** Server message that resumes a Fusion child once after a transient provider failure. */
|
|
3288
|
+
const FUSION_RETRY_PROMPT_PREFIX = "[fusion-retry]";
|
|
3287
3289
|
/**
|
|
3288
3290
|
* Message-id namespaces for server-generated fusion wakes. Wake detection is
|
|
3289
3291
|
* keyed off these rather than message text, which a user or expanded skill
|
|
@@ -5296,7 +5298,7 @@ var OrchestrationSearchThreadsError = class extends Schema$1.TaggedErrorClass()(
|
|
|
5296
5298
|
const ThreadWatchEventsInput = Schema$1.Struct({
|
|
5297
5299
|
threadId: ThreadId.annotate({ description: "The thread to read events from. Any thread on this server, not only this session's own." }),
|
|
5298
5300
|
afterSequence: Schema$1.optional(NonNegativeInt.annotate({ description: "Exclusive sequence cursor. Events with a higher sequence are returned. Omit to read from the beginning." })),
|
|
5299
|
-
limit: Schema$1.optional(NonNegativeInt.
|
|
5301
|
+
limit: Schema$1.optional(NonNegativeInt.annotate({ description: `Maximum events to return (default 50, max 200; larger values are clamped).` }))
|
|
5300
5302
|
});
|
|
5301
5303
|
const ThreadWatchEventsResult = Schema$1.Struct({
|
|
5302
5304
|
threadId: ThreadId,
|
|
@@ -9888,7 +9890,13 @@ const DeviceSession = Schema$1.Struct({
|
|
|
9888
9890
|
platform: DevicePlatform,
|
|
9889
9891
|
openedAt: Schema$1.String,
|
|
9890
9892
|
/** Opaque lifetime identity; absent only on servers predating guarded close. */
|
|
9891
|
-
leaseId: Schema$1.optional(DeviceId)
|
|
9893
|
+
leaseId: Schema$1.optional(DeviceId),
|
|
9894
|
+
/**
|
|
9895
|
+
* Server-resolved folder of the agent's thread. Apple-backed control opens the
|
|
9896
|
+
* Xcode project found here, because Xcode asks the user to approve an agent
|
|
9897
|
+
* only once it opens a project.
|
|
9898
|
+
*/
|
|
9899
|
+
workspaceRoot: Schema$1.optional(Schema$1.String)
|
|
9892
9900
|
});
|
|
9893
9901
|
const DeviceServiceState = Schema$1.Struct({
|
|
9894
9902
|
hosts: Schema$1.Array(DeviceHostSummary),
|
|
@@ -11334,6 +11342,12 @@ const ServerSettings = Schema$1.Struct({
|
|
|
11334
11342
|
*/
|
|
11335
11343
|
enableUnpromptedSubagents: Schema$1.Boolean.pipe(Schema$1.withDecodingDefault(Effect.succeed(true))),
|
|
11336
11344
|
/**
|
|
11345
|
+
* Compression mode new threads start with. The composer writes the user's
|
|
11346
|
+
* last pick here, so the next new thread follows it; each thread keeps its
|
|
11347
|
+
* own mode once created.
|
|
11348
|
+
*/
|
|
11349
|
+
defaultCompressMode: CompressMode.pipe(Schema$1.withDecodingDefault(Effect.succeed(DEFAULT_COMPRESS_MODE))),
|
|
11350
|
+
/**
|
|
11337
11351
|
* Beta. Whether Fusion role rules travel once through the provider's
|
|
11338
11352
|
* session channel (Claude's system prompt, Codex's developer instructions)
|
|
11339
11353
|
* with a one-line reference on each message, or the legacy way: the full
|
|
@@ -11637,6 +11651,7 @@ const ServerSettingsPatch = Schema$1.Struct({
|
|
|
11637
11651
|
enableVerificationBeforeCompletion: Schema$1.optionalKey(Schema$1.Boolean),
|
|
11638
11652
|
enableRootCauseBeforeFix: Schema$1.optionalKey(Schema$1.Boolean),
|
|
11639
11653
|
enableUnpromptedSubagents: Schema$1.optionalKey(Schema$1.Boolean),
|
|
11654
|
+
defaultCompressMode: Schema$1.optionalKey(CompressMode),
|
|
11640
11655
|
enableOptimizedFusionPromptDelivery: Schema$1.optionalKey(Schema$1.Boolean),
|
|
11641
11656
|
enableFusionObservation: Schema$1.optionalKey(Schema$1.Boolean),
|
|
11642
11657
|
storageCleanup: Schema$1.optionalKey(Schema$1.Struct({
|
|
@@ -13342,7 +13357,9 @@ const ThreadContextUpdateInput = Schema$1.Struct({
|
|
|
13342
13357
|
});
|
|
13343
13358
|
const ThreadContextUpdateResult = Schema$1.Struct({
|
|
13344
13359
|
pairId: ThreadPairId,
|
|
13345
|
-
updatedAt: IsoDateTime
|
|
13360
|
+
updatedAt: IsoDateTime,
|
|
13361
|
+
/** Explains input the server dropped instead of rejecting the whole update. */
|
|
13362
|
+
notice: Schema$1.optional(Schema$1.String)
|
|
13346
13363
|
});
|
|
13347
13364
|
const RoleContextItems = Schema$1.Struct({
|
|
13348
13365
|
supervisor: SharedContextItems,
|
|
@@ -21548,6 +21565,7 @@ const makeRuntimeSqliteLayer = Effect.fn("makeRuntimeSqliteLayer")(function* (co
|
|
|
21548
21565
|
const applyPragmas = Effect.gen(function* () {
|
|
21549
21566
|
const sql = yield* SqlClient.SqlClient;
|
|
21550
21567
|
yield* sql`PRAGMA journal_mode = WAL;`;
|
|
21568
|
+
yield* sql`PRAGMA synchronous = NORMAL;`;
|
|
21551
21569
|
yield* sql`PRAGMA foreign_keys = ON;`;
|
|
21552
21570
|
});
|
|
21553
21571
|
const setupFile = (dbPath) => Layer.effectDiscard(applyPragmas.pipe(Effect.andThen(runGuardedMigrations({ dbPath })), Effect.andThen(runHistoricalActivityCompaction())));
|
|
@@ -45404,8 +45422,9 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
45404
45422
|
threadId,
|
|
45405
45423
|
...bounds
|
|
45406
45424
|
})).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listActivities:query", "ProjectionSnapshotQuery.getThreadDetailById:listActivities:decodeRows")), Effect.map((rows) => rows.map(mapThreadActivityRow)));
|
|
45407
|
-
const
|
|
45408
|
-
|
|
45425
|
+
const threadRow = yield* getActiveThreadRowById({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getThread:query", "ProjectionSnapshotQuery.getThreadDetailById:getThread:decodeRow")));
|
|
45426
|
+
if (Option.isNone(threadRow)) return Option.none();
|
|
45427
|
+
const [messageRows, proposedPlanRows, activities, checkpointRows, turnRows, latestTurnRow, sessionRow, scheduledTaskRows] = yield* Effect.all([
|
|
45409
45428
|
(bounds === void 0 ? listThreadMessageRowsByThread({ threadId }) : listThreadMessageRowsByThreadWindow({
|
|
45410
45429
|
threadId,
|
|
45411
45430
|
...bounds
|
|
@@ -45418,7 +45437,6 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
|
|
|
45418
45437
|
getThreadSessionRowByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:getSession:query", "ProjectionSnapshotQuery.getThreadDetailById:getSession:decodeRow"))),
|
|
45419
45438
|
listThreadScheduledTaskRowsByThread({ threadId }).pipe(Effect.mapError(toPersistenceSqlOrDecodeError$1("ProjectionSnapshotQuery.getThreadDetailById:listScheduledTasks:query", "ProjectionSnapshotQuery.getThreadDetailById:listScheduledTasks:decodeRows")))
|
|
45420
45439
|
]);
|
|
45421
|
-
if (Option.isNone(threadRow)) return Option.none();
|
|
45422
45440
|
const thread = {
|
|
45423
45441
|
id: threadRow.value.threadId,
|
|
45424
45442
|
projectId: threadRow.value.projectId,
|
|
@@ -49540,13 +49558,21 @@ var XcodeMcpClient = class XcodeMcpClient {
|
|
|
49540
49558
|
throw error;
|
|
49541
49559
|
}
|
|
49542
49560
|
}
|
|
49543
|
-
|
|
49561
|
+
/**
|
|
49562
|
+
* `abandonAfterMs` gives up on a slow reply without closing the bridge; the
|
|
49563
|
+
* default timeout closes it, since a stuck bridge cannot be trusted.
|
|
49564
|
+
*/
|
|
49565
|
+
request(method, params, signal, abandonAfterMs) {
|
|
49544
49566
|
signal.throwIfAborted();
|
|
49545
49567
|
if (this.closed) return Promise.reject(/* @__PURE__ */ new Error("Device MCP session is closed. Select the device again."));
|
|
49546
49568
|
const id = ++this.sequence;
|
|
49547
49569
|
return new Promise((resolve, reject) => {
|
|
49548
49570
|
const abort = () => this.close(/* @__PURE__ */ new Error("Device MCP request cancelled. Select the device again."));
|
|
49549
|
-
const timer = setTimeout(() => this.close(/* @__PURE__ */ new Error("Device MCP timed out. Select the device again.")), REQUEST_TIMEOUT_MS$5)
|
|
49571
|
+
const timer = abandonAfterMs === void 0 ? setTimeout(() => this.close(/* @__PURE__ */ new Error("Device MCP timed out. Select the device again.")), REQUEST_TIMEOUT_MS$5) : setTimeout(() => {
|
|
49572
|
+
this.pending.delete(id);
|
|
49573
|
+
finish();
|
|
49574
|
+
reject(/* @__PURE__ */ new Error(`Device MCP ${method} did not answer in time.`));
|
|
49575
|
+
}, abandonAfterMs);
|
|
49550
49576
|
const finish = () => {
|
|
49551
49577
|
clearTimeout(timer);
|
|
49552
49578
|
signal.removeEventListener("abort", abort);
|
|
@@ -49570,7 +49596,7 @@ var XcodeMcpClient = class XcodeMcpClient {
|
|
|
49570
49596
|
}) + "\n");
|
|
49571
49597
|
});
|
|
49572
49598
|
}
|
|
49573
|
-
async call(name, args, signal) {
|
|
49599
|
+
async call(name, args, signal, options) {
|
|
49574
49600
|
if (!this.tools.has(name)) throw new Error(`Selected device MCP service does not expose ${name}. Check adapter capabilities and Xcode setup.`);
|
|
49575
49601
|
const redact = (error) => {
|
|
49576
49602
|
let message = error instanceof Error ? error.message : String(error);
|
|
@@ -49581,7 +49607,7 @@ var XcodeMcpClient = class XcodeMcpClient {
|
|
|
49581
49607
|
const result = objectValue(await this.request("tools/call", {
|
|
49582
49608
|
name,
|
|
49583
49609
|
arguments: args
|
|
49584
|
-
}, signal));
|
|
49610
|
+
}, signal, options?.abandonAfterMs));
|
|
49585
49611
|
const structured = objectValue(result.structuredContent);
|
|
49586
49612
|
if (result.isError === true || structured.didError === true) {
|
|
49587
49613
|
const detail = (Array.isArray(result.content) ? result.content : []).map((part) => objectValue(part).text).filter((part) => typeof part === "string").join("\n");
|
|
@@ -49783,6 +49809,15 @@ const REQUIRED_DEVICE_TOOLS = [
|
|
|
49783
49809
|
"DeviceInteractionSynthesize",
|
|
49784
49810
|
"DeviceInteractionEndSession"
|
|
49785
49811
|
];
|
|
49812
|
+
const XCODE_PROJECT_SEARCH_DEPTH = 3;
|
|
49813
|
+
const DISCOVERED_PROJECT_OPEN_WAIT_MS = 15e3;
|
|
49814
|
+
const XCODE_PROJECT_SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([
|
|
49815
|
+
"node_modules",
|
|
49816
|
+
"Pods",
|
|
49817
|
+
"DerivedData",
|
|
49818
|
+
"build",
|
|
49819
|
+
"Carthage"
|
|
49820
|
+
]);
|
|
49786
49821
|
const FRAME_PATTERN = /\{\{(-?[\d.]+),\s*(-?[\d.]+)\},\s*\{([\d.]+),\s*([\d.]+)\}\}/;
|
|
49787
49822
|
const HIT_PATTERN = /hitPoint:\s*\{(-?[\d.]+),\s*(-?[\d.]+)\}/;
|
|
49788
49823
|
const Capture = Schema$1.Struct({
|
|
@@ -49850,6 +49885,33 @@ function appleHierarchySnapshot(text, sequence) {
|
|
|
49850
49885
|
elements
|
|
49851
49886
|
};
|
|
49852
49887
|
}
|
|
49888
|
+
/**
|
|
49889
|
+
* Shallowest `.xcworkspace` under a thread's folder, else the shallowest
|
|
49890
|
+
* `.xcodeproj`. Bundles, dot folders and dependency trees are not searched.
|
|
49891
|
+
*/
|
|
49892
|
+
const findXcodeProject = (fs, root) => Effect.gen(function* () {
|
|
49893
|
+
let level = [root];
|
|
49894
|
+
for (let depth = 0; depth <= XCODE_PROJECT_SEARCH_DEPTH && level.length > 0; depth++) {
|
|
49895
|
+
const next = [];
|
|
49896
|
+
let project;
|
|
49897
|
+
for (const directory of level) {
|
|
49898
|
+
const names = yield* fs.readDirectory(directory).pipe(Effect.orElseSucceed(() => []));
|
|
49899
|
+
for (const name of names.toSorted()) {
|
|
49900
|
+
const path = `${directory}/${name}`;
|
|
49901
|
+
if (name.endsWith(".xcworkspace")) return path;
|
|
49902
|
+
if (name.endsWith(".xcodeproj")) {
|
|
49903
|
+
project ??= path;
|
|
49904
|
+
continue;
|
|
49905
|
+
}
|
|
49906
|
+
if (name.startsWith(".") || XCODE_PROJECT_SKIPPED_DIRECTORIES.has(name)) continue;
|
|
49907
|
+
const info = yield* fs.stat(path).pipe(Effect.option);
|
|
49908
|
+
if (Option.isSome(info) && info.value.type === "Directory") next.push(path);
|
|
49909
|
+
}
|
|
49910
|
+
}
|
|
49911
|
+
if (project) return project;
|
|
49912
|
+
level = next;
|
|
49913
|
+
}
|
|
49914
|
+
});
|
|
49853
49915
|
function createAppleDeviceAdapter(input) {
|
|
49854
49916
|
const sessions = /* @__PURE__ */ new Map();
|
|
49855
49917
|
const fail = (operation) => (cause) => new DeviceOperationError({
|
|
@@ -49872,7 +49934,9 @@ function createAppleDeviceAdapter(input) {
|
|
|
49872
49934
|
}
|
|
49873
49935
|
}).catch(() => {});
|
|
49874
49936
|
};
|
|
49875
|
-
const
|
|
49937
|
+
const workspaceFor = (session) => input.workspacePath !== void 0 || !session.workspaceRoot || session.leaseId !== void 0 && sessions.has(session.leaseId) ? Effect.succeed(input.workspacePath) : findXcodeProject(input.fs, session.workspaceRoot);
|
|
49938
|
+
const get = (session) => Effect.flatMap(workspaceFor(session), (workspace) => open(session, workspace));
|
|
49939
|
+
const open = (session, workspace) => Effect.tryPromise({
|
|
49876
49940
|
try: async (signal) => {
|
|
49877
49941
|
if (session.hostId !== "local" || session.platform !== "ios" || !session.leaseId) throw new Error("Apple MCP requires an exact local iOS device lease.");
|
|
49878
49942
|
let pending = sessions.get(session.leaseId);
|
|
@@ -49887,6 +49951,7 @@ function createAppleDeviceAdapter(input) {
|
|
|
49887
49951
|
const missing = REQUIRED_DEVICE_TOOLS.filter((tool) => !client.tools.has(tool));
|
|
49888
49952
|
if (missing.length > 0) throw new Error(`Selected Xcode lacks required device tools: ${missing.join(", ")}. No device session was opened.`);
|
|
49889
49953
|
if (input.workspacePath) await client.call("XcodeOpenWorkspace", { path: input.workspacePath }, signal);
|
|
49954
|
+
else if (workspace) await client.call("XcodeOpenWorkspace", { path: workspace }, signal, { abandonAfterMs: DISCOVERED_PROJECT_OPEN_WAIT_MS }).catch(() => void 0);
|
|
49890
49955
|
const opened = Schema$1.decodeUnknownSync(Session)(appleMcpData(await client.call("DeviceInteractionStartSession", {
|
|
49891
49956
|
deviceIdentifier: session.deviceId,
|
|
49892
49957
|
sessionIdentifier: `P4 Device ${session.threadId.slice(0, 8)}`
|
|
@@ -49958,7 +50023,7 @@ function createAppleDeviceAdapter(input) {
|
|
|
49958
50023
|
const permission = objectValue(status.permission);
|
|
49959
50024
|
if ((Array.isArray(permission.pendingAgentApprovals) ? permission.pendingAgentApprovals : []).some((item) => objectValue(objectValue(objectValue(objectValue(item).subject).agent)._0).name === "p4code-device-adapter")) return "Apple Xcode is waiting for approval of p4code-device-adapter and its project folder. Approve the pending request in Xcode, then select the device again. P4 cannot grant this approval automatically.";
|
|
49960
50025
|
if (status.running === false) return "Apple MCP service is not running. Open the selected Xcode project or configure its headless service, then select the device again.";
|
|
49961
|
-
return "Apple MCP did not respond
|
|
50026
|
+
return "Apple MCP refused or did not respond. Xcode asks to approve p4code-device-adapter only when P4 opens an Xcode project: use a thread whose folder contains an .xcworkspace or .xcodeproj (or set P4CODE_APPLE_MCP_WORKSPACE), approve the prompt in Xcode, then select the device again.";
|
|
49962
50027
|
}).pipe(Effect.orElseSucceed(() => "Apple MCP did not respond. Check Xcode agent/folder permissions and service status.")),
|
|
49963
50028
|
discover: Effect.gen(function* () {
|
|
49964
50029
|
const result = yield* input.run("xcrun", [
|
|
@@ -51691,7 +51756,8 @@ const makeWithHosts = Effect.fn("DeviceService.makeWithHosts")(function* (input)
|
|
|
51691
51756
|
deviceId: found.device.id,
|
|
51692
51757
|
platform: found.device.platform,
|
|
51693
51758
|
openedAt,
|
|
51694
|
-
leaseId: NodeCrypto.randomUUID()
|
|
51759
|
+
leaseId: NodeCrypto.randomUUID(),
|
|
51760
|
+
...value.workspaceRoot ? { workspaceRoot: value.workspaceRoot } : {}
|
|
51695
51761
|
};
|
|
51696
51762
|
yield* closeCommands(sessionsToClose(state.sessions, session));
|
|
51697
51763
|
yield* update((current) => ({
|
|
@@ -57985,6 +58051,18 @@ const withDeviceInvocation = (execute, capability = "devices") => Effect.gen(fun
|
|
|
57985
58051
|
const work = execute(service, invocation.threadId).pipe(Effect.tap(() => lifetime.revoked ? Effect.fail(denied()) : Effect.void), Effect.mapError((error) => isDeviceToolUnavailableError(error) || isDeviceActionUnavailableError(error) ? error : isDeviceOperationError(error) && error.reason === "agent_access_disabled" ? new DeviceToolUnavailableError({ reason: error.message }) : denied()));
|
|
57986
58052
|
return yield* Effect.raceFirst(work, Deferred.await(lifetime.revocation).pipe(Effect.andThen(Effect.fail(denied()))));
|
|
57987
58053
|
});
|
|
58054
|
+
/**
|
|
58055
|
+
* The thread's worktree, else its project root, for opening a device session.
|
|
58056
|
+
* Unreadable projections yield nothing: the session still opens without it.
|
|
58057
|
+
*/
|
|
58058
|
+
const threadWorkspaceRoot = (threadId) => Effect.gen(function* () {
|
|
58059
|
+
const query = yield* ProjectionSnapshotQuery;
|
|
58060
|
+
const thread = yield* query.getThreadShellById(threadId);
|
|
58061
|
+
if (Option.isNone(thread)) return void 0;
|
|
58062
|
+
if (thread.value.worktreePath) return thread.value.worktreePath;
|
|
58063
|
+
const project = yield* query.getProjectShellById(thread.value.projectId);
|
|
58064
|
+
return Option.isSome(project) ? project.value.workspaceRoot : void 0;
|
|
58065
|
+
}).pipe(Effect.orElseSucceed(() => void 0));
|
|
57988
58066
|
const withDeviceSession = (target, execute) => withDeviceInvocation((service, threadId) => service.withAgentCommand({
|
|
57989
58067
|
hostId: target.hostId,
|
|
57990
58068
|
deviceId: target.deviceId,
|
|
@@ -58017,6 +58095,7 @@ const makeComputerUseDevices = Effect.fn("ComputerUseDevices.make")(function* ()
|
|
|
58017
58095
|
const screenshots = yield* DeviceScreenshot;
|
|
58018
58096
|
const commands = yield* DeviceCommandProxy;
|
|
58019
58097
|
const http = yield* HttpClient.HttpClient;
|
|
58098
|
+
const snapshots = yield* ProjectionSnapshotQuery;
|
|
58020
58099
|
const stale = () => new DeviceOperationError({
|
|
58021
58100
|
operation: "command",
|
|
58022
58101
|
reason: "request_failed",
|
|
@@ -58037,11 +58116,13 @@ const makeComputerUseDevices = Effect.fn("ComputerUseDevices.make")(function* ()
|
|
|
58037
58116
|
const device = state.devices.find((device) => device.hostId === input.hostId && device.id === input.deviceId && device.platform === input.platform);
|
|
58038
58117
|
if (!device) return yield* stale();
|
|
58039
58118
|
if (device.physical) return yield* new DeviceToolUnavailableError({ reason: "Physical-device UI control is not supported by this adapter. Pairing, trust, Developer Mode and signing must be configured separately; use supported device-management workflows." });
|
|
58119
|
+
const workspaceRoot = yield* threadWorkspaceRoot(threadId).pipe(Effect.provideService(ProjectionSnapshotQuery, snapshots));
|
|
58040
58120
|
const session = yield* service.agentOpen({
|
|
58041
58121
|
threadId,
|
|
58042
58122
|
hostId: input.hostId,
|
|
58043
58123
|
deviceId: input.deviceId,
|
|
58044
|
-
platform: input.platform
|
|
58124
|
+
platform: input.platform,
|
|
58125
|
+
...workspaceRoot ? { workspaceRoot } : {}
|
|
58045
58126
|
});
|
|
58046
58127
|
if (!session.leaseId) return yield* stale();
|
|
58047
58128
|
return {
|
|
@@ -61790,6 +61871,38 @@ const prepareSharedCheckout = (input, git) => Effect.gen(function* () {
|
|
|
61790
61871
|
return { branch };
|
|
61791
61872
|
}).pipe(Effect.withSpan("prepareSharedCheckout"));
|
|
61792
61873
|
//#endregion
|
|
61874
|
+
//#region src/orchestration/ShellStreamingThrottle.ts
|
|
61875
|
+
/** Minimum gap between shell refetches caused by one streaming message. */
|
|
61876
|
+
const SHELL_STREAMING_REFRESH_INTERVAL_MS = 1e3;
|
|
61877
|
+
/**
|
|
61878
|
+
* Per-subscription filter for the shell stream. A streaming assistant message
|
|
61879
|
+
* emits a delta roughly every 50 ms and each one only moves the thread's
|
|
61880
|
+
* `updatedAt`, yet every delta would otherwise refetch the thread shell and
|
|
61881
|
+
* push it to the client. The first delta of a message always passes (it can
|
|
61882
|
+
* change `latestTurn`), later deltas pass at most once per interval, and the
|
|
61883
|
+
* final non-streaming message always passes, so the shell still settles on the
|
|
61884
|
+
* current state.
|
|
61885
|
+
*
|
|
61886
|
+
* Returns `(event, nowMs) => keep`. Create one per subscription: the state is
|
|
61887
|
+
* which deltas this subscriber has already seen.
|
|
61888
|
+
*/
|
|
61889
|
+
function makeShellStreamingThrottle(intervalMs = SHELL_STREAMING_REFRESH_INTERVAL_MS) {
|
|
61890
|
+
const lastPassedAtByMessageId = /* @__PURE__ */ new Map();
|
|
61891
|
+
return (event, nowMs) => {
|
|
61892
|
+
if (event.type !== "thread.message-sent") return true;
|
|
61893
|
+
const messageId = event.payload.messageId;
|
|
61894
|
+
if (!event.payload.streaming) {
|
|
61895
|
+
lastPassedAtByMessageId.delete(messageId);
|
|
61896
|
+
return true;
|
|
61897
|
+
}
|
|
61898
|
+
const lastPassedAt = lastPassedAtByMessageId.get(messageId);
|
|
61899
|
+
if (lastPassedAt !== void 0 && nowMs - lastPassedAt < intervalMs) return false;
|
|
61900
|
+
if (lastPassedAtByMessageId.size >= 1e3) lastPassedAtByMessageId.clear();
|
|
61901
|
+
lastPassedAtByMessageId.set(messageId, nowMs);
|
|
61902
|
+
return true;
|
|
61903
|
+
};
|
|
61904
|
+
}
|
|
61905
|
+
//#endregion
|
|
61793
61906
|
//#region src/provider/ProviderInstanceEnvironment.ts
|
|
61794
61907
|
function mergeProviderInstanceEnvironment(environment, baseEnv = process.env) {
|
|
61795
61908
|
if (!environment || environment.length === 0) return baseEnv;
|
|
@@ -92194,6 +92307,11 @@ const makeCodexSubagentModelReporter = (options) => {
|
|
|
92194
92307
|
*/
|
|
92195
92308
|
/** Codex's own name for the token variable of p4code's built-in server. */
|
|
92196
92309
|
const CODEX_P4CODE_BEARER_TOKEN_ENV_VAR = "P4_MCP_BEARER_TOKEN";
|
|
92310
|
+
/**
|
|
92311
|
+
* Codex `mcp_servers.<name>.tool_timeout_sec` for p4code's built-in server, a
|
|
92312
|
+
* TOML float. An hour, because `ask_user_question` blocks until a person answers.
|
|
92313
|
+
*/
|
|
92314
|
+
const CODEX_P4CODE_TOOL_TIMEOUT_SEC = "3600.0";
|
|
92197
92315
|
/** Its own variable, so the Computer Use credential never rides on p4-code's. */
|
|
92198
92316
|
const CODEX_P4_COMPUTER_USE_BEARER_TOKEN_ENV_VAR = "P4_COMPUTER_USE_BEARER_TOKEN";
|
|
92199
92317
|
/** Prefix for the per-server variables the registered servers get. */
|
|
@@ -94329,7 +94447,9 @@ const makeCodexAdapter = Effect.fn("makeCodexAdapter")(function* (codexConfig, o
|
|
|
94329
94447
|
"-c",
|
|
94330
94448
|
`mcp_servers.${P4CODE_MCP_SERVER_NAME}.url=${mcpSession.endpoint}`,
|
|
94331
94449
|
"-c",
|
|
94332
|
-
`mcp_servers.${P4CODE_MCP_SERVER_NAME}.bearer_token_env_var="${CODEX_P4CODE_BEARER_TOKEN_ENV_VAR}"
|
|
94450
|
+
`mcp_servers.${P4CODE_MCP_SERVER_NAME}.bearer_token_env_var="${CODEX_P4CODE_BEARER_TOKEN_ENV_VAR}"`,
|
|
94451
|
+
"-c",
|
|
94452
|
+
`mcp_servers.${P4CODE_MCP_SERVER_NAME}.tool_timeout_sec=${CODEX_P4CODE_TOOL_TIMEOUT_SEC}`
|
|
94333
94453
|
] : [],
|
|
94334
94454
|
...mcpSession?.computerUse ? [
|
|
94335
94455
|
"-c",
|
|
@@ -132505,7 +132625,10 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker, computerUseBrok
|
|
|
132505
132625
|
output.push(...yield* coalesceShellEvents(pendingEvents));
|
|
132506
132626
|
return output;
|
|
132507
132627
|
});
|
|
132508
|
-
const coalesceShellLiveStream = (stream) =>
|
|
132628
|
+
const coalesceShellLiveStream = (stream) => {
|
|
132629
|
+
const keepStreamingDelta = makeShellStreamingThrottle();
|
|
132630
|
+
return stream.pipe(Stream.filterEffect((input) => input.kind === "event" ? Clock.currentTimeMillis.pipe(Effect.map((nowMs) => keepStreamingDelta(input.event, nowMs))) : Effect.succeed(true)), Stream.groupedWithin(SHELL_COALESCE_MAX_CHUNK, SHELL_COALESCE_WINDOW), Stream.mapEffect(coalesceShellLiveInputs), Stream.flatMap((items) => Stream.fromIterable(items)));
|
|
132631
|
+
};
|
|
132509
132632
|
const dispatchBootstrapTurnStart = (command) => Effect.gen(function* () {
|
|
132510
132633
|
const bootstrap = command.bootstrap;
|
|
132511
132634
|
const { bootstrap: _bootstrap, ...finalTurnStartCommand } = command;
|
|
@@ -133178,6 +133301,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker, computerUseBrok
|
|
|
133178
133301
|
reason: plan.reason
|
|
133179
133302
|
});
|
|
133180
133303
|
const modelSelection = plan.model;
|
|
133304
|
+
const compressMode = yield* serverSettings.getSettings.pipe(Effect.map((settings) => settings.defaultCompressMode), Effect.orElseSucceed(() => DEFAULT_COMPRESS_MODE));
|
|
133181
133305
|
const threadId = ThreadId.make(yield* crypto.randomUUIDv4.pipe(Effect.orDie));
|
|
133182
133306
|
const createdAt = yield* nowIso$2;
|
|
133183
133307
|
const threadTitle = buildTaskThreadTitle(task.title);
|
|
@@ -133202,7 +133326,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker, computerUseBrok
|
|
|
133202
133326
|
titleSeed: threadTitle,
|
|
133203
133327
|
runtimeMode: DEFAULT_RUNTIME_MODE$2,
|
|
133204
133328
|
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
|
|
133205
|
-
compressMode
|
|
133329
|
+
compressMode,
|
|
133206
133330
|
unpromptedSubagents: true,
|
|
133207
133331
|
bootstrap: { createThread: {
|
|
133208
133332
|
projectId: plan.projectId,
|
|
@@ -133210,7 +133334,7 @@ const makeWsRpcLayer = (currentSession, previewAutomationBroker, computerUseBrok
|
|
|
133210
133334
|
modelSelection,
|
|
133211
133335
|
runtimeMode: DEFAULT_RUNTIME_MODE$2,
|
|
133212
133336
|
interactionMode: DEFAULT_PROVIDER_INTERACTION_MODE,
|
|
133213
|
-
compressMode
|
|
133337
|
+
compressMode,
|
|
133214
133338
|
unpromptedSubagents: true,
|
|
133215
133339
|
branch: null,
|
|
133216
133340
|
worktreePath: null,
|
|
@@ -135398,7 +135522,11 @@ const DeviceOpenTool = Tool.make("device_open", {
|
|
|
135398
135522
|
parameters: DeviceToolOpenInput,
|
|
135399
135523
|
success: DeviceToolOpenResult,
|
|
135400
135524
|
failure: DeviceToolError,
|
|
135401
|
-
dependencies: [
|
|
135525
|
+
dependencies: [
|
|
135526
|
+
McpInvocationContext,
|
|
135527
|
+
DeviceService,
|
|
135528
|
+
ProjectionSnapshotQuery
|
|
135529
|
+
]
|
|
135402
135530
|
}).annotate(Tool.Title, "Open device").annotate(Tool.Readonly, false);
|
|
135403
135531
|
const DeviceCloseTool = Tool.make("device_close", {
|
|
135404
135532
|
description: "Close this thread's device sessions on the named host. Omitted device closes all sessions on that host only. Optional shutdown powers off the selected devices and closes their other viewing sessions.",
|
|
@@ -135510,11 +135638,13 @@ const DeviceToolkitHandlersLive = DeviceToolkit.toLayer({
|
|
|
135510
135638
|
try: () => selectDevice(state, threadId, value),
|
|
135511
135639
|
catch: () => new DeviceToolUnavailableError({ reason: "Choose an explicit platform/device from device_list on the requested host." })
|
|
135512
135640
|
});
|
|
135641
|
+
const workspaceRoot = yield* threadWorkspaceRoot(threadId);
|
|
135513
135642
|
const session = yield* service.agentOpen({
|
|
135514
135643
|
threadId,
|
|
135515
135644
|
hostId: value.hostId,
|
|
135516
135645
|
deviceId: selected.id,
|
|
135517
|
-
platform: selected.platform
|
|
135646
|
+
platform: selected.platform,
|
|
135647
|
+
...workspaceRoot ? { workspaceRoot } : {}
|
|
135518
135648
|
});
|
|
135519
135649
|
const current = yield* service.state;
|
|
135520
135650
|
const device = current.devices.find((device) => device.hostId === session.hostId && device.id === session.deviceId && device.platform === session.platform);
|
|
@@ -136915,7 +137045,9 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
|
|
|
136915
137045
|
const { invocation, pair, reject } = yield* requireSharedPair("thread_context_update");
|
|
136916
137046
|
const isBuilder = invocation.threadId === pair.implementerThreadId;
|
|
136917
137047
|
if (isBuilder && (input.requirements !== void 0 || input.decisions !== void 0)) return yield* reject("Supervisor owns requirements and decisions. Propose revisions through thread_ask.");
|
|
136918
|
-
|
|
137048
|
+
const { summary, ...supervisorContext } = input;
|
|
137049
|
+
const ignoresSummary = !isBuilder && summary !== void 0;
|
|
137050
|
+
if (ignoresSummary && input.requirements === void 0 && input.decisions === void 0 && input.blockers.length === 0 && input.evidence.length === 0) return yield* reject("Only Builder may replace its phase summary.");
|
|
136919
137051
|
const crypto = yield* Crypto.Crypto;
|
|
136920
137052
|
const createdAt = DateTime.formatIso(yield* DateTime.now);
|
|
136921
137053
|
yield* dispatchControl({
|
|
@@ -136931,14 +137063,15 @@ const ThreadToolkitHandlersLive = ThreadToolkit.toLayer({
|
|
|
136931
137063
|
summary: "Shared working context updated",
|
|
136932
137064
|
turnId: null,
|
|
136933
137065
|
payload: {
|
|
136934
|
-
...input,
|
|
137066
|
+
...isBuilder ? input : supervisorContext,
|
|
136935
137067
|
pairId: pair.id
|
|
136936
137068
|
}
|
|
136937
137069
|
}
|
|
136938
137070
|
}, invocation.threadId);
|
|
136939
137071
|
return {
|
|
136940
137072
|
pairId: pair.id,
|
|
136941
|
-
updatedAt: createdAt
|
|
137073
|
+
updatedAt: createdAt,
|
|
137074
|
+
...ignoresSummary ? { notice: "summary was ignored because only Builder owns its phase summary; the other fields were saved." } : {}
|
|
136942
137075
|
};
|
|
136943
137076
|
}),
|
|
136944
137077
|
thread_plan_update: (input) => Effect.gen(function* () {
|
|
@@ -138970,7 +139103,7 @@ const WatchToolkitHandlersLive = WatchToolkit.toLayer({
|
|
|
138970
139103
|
const page = yield* (yield* ThreadEventStreamService).read({
|
|
138971
139104
|
threadId: input.threadId,
|
|
138972
139105
|
...input.afterSequence !== void 0 ? { afterSequence: input.afterSequence } : {},
|
|
138973
|
-
limit: input.limit ?? 50
|
|
139106
|
+
limit: Math.min(input.limit ?? 50, 200)
|
|
138974
139107
|
}).pipe(Effect.mapError((cause) => new ThreadWatchFailedError({
|
|
138975
139108
|
threadId: input.threadId,
|
|
138976
139109
|
detail: cause.message
|
|
@@ -144933,7 +145066,8 @@ function citedSequences(summary) {
|
|
|
144933
145066
|
* sequence numbers, the summary is required to cite them, and a dropped line,
|
|
144934
145067
|
* unrenderable transcript field, or dropped ledger row makes coverage
|
|
144935
145068
|
* incomplete. A shortened transcript field or clipped ledger argument stays
|
|
144936
|
-
* visible and its full text stays in the raw event, so neither does.
|
|
145069
|
+
* visible and its full text stays in the raw event, so neither does. A summary
|
|
145070
|
+
* line citing a sequence the transcript lacks is removed, never kept.
|
|
144937
145071
|
*
|
|
144938
145072
|
* @module orchestration/handoff/fusionEvidenceHandoff
|
|
144939
145073
|
*/
|
|
@@ -145087,6 +145221,34 @@ function summaryRejection(input) {
|
|
|
145087
145221
|
if (cited.length === 0) return "summary-uncited";
|
|
145088
145222
|
return cited.every((sequence) => input.includedSequences.has(sequence)) ? null : "summary-citation-invalid";
|
|
145089
145223
|
}
|
|
145224
|
+
/**
|
|
145225
|
+
* The summary with every line citing a sequence absent from the transcript,
|
|
145226
|
+
* and the uncited lines continuing it, removed; or null when nothing was
|
|
145227
|
+
* removed or nothing cited survives.
|
|
145228
|
+
*
|
|
145229
|
+
* A line is the unit of claim: one invented citation condemns the line it is
|
|
145230
|
+
* on, never the lines beside it that cite real evidence. The removal is
|
|
145231
|
+
* disclosed in the text so the supervisor knows the summary was cut.
|
|
145232
|
+
*/
|
|
145233
|
+
function salvageSummary(input) {
|
|
145234
|
+
const lines = input.summary.split("\n");
|
|
145235
|
+
const kept = [];
|
|
145236
|
+
let droppingClaim = false;
|
|
145237
|
+
for (const line of lines) {
|
|
145238
|
+
const cited = citedSequences(line);
|
|
145239
|
+
if (cited.length > 0) droppingClaim = !cited.every((sequence) => input.includedSequences.has(sequence));
|
|
145240
|
+
else if (/^\s*#{1,6}\s/u.test(line)) droppingClaim = false;
|
|
145241
|
+
if (!droppingClaim) kept.push(line);
|
|
145242
|
+
}
|
|
145243
|
+
const dropped = lines.length - kept.length;
|
|
145244
|
+
if (dropped === 0) return null;
|
|
145245
|
+
const text = kept.join("\n").trim();
|
|
145246
|
+
if (summaryRejection({
|
|
145247
|
+
summary: text,
|
|
145248
|
+
includedSequences: input.includedSequences
|
|
145249
|
+
}) !== null) return null;
|
|
145250
|
+
return `${text}\n\n[Server removed ${dropped} summary ${dropped === 1 ? "line" : "lines"} resting on sequences absent from this transcript.]`;
|
|
145251
|
+
}
|
|
145090
145252
|
const buildFusionEvidenceHandoff = Effect.fn("fusionEvidenceHandoff.build")(function* (input) {
|
|
145091
145253
|
const engine = yield* OrchestrationEngineService;
|
|
145092
145254
|
const textGeneration = yield* TextGeneration;
|
|
@@ -145154,6 +145316,19 @@ const buildFusionEvidenceHandoff = Effect.fn("fusionEvidenceHandoff.build")(func
|
|
|
145154
145316
|
produced: Option.none(),
|
|
145155
145317
|
reason
|
|
145156
145318
|
});
|
|
145319
|
+
const salvaged = (result) => {
|
|
145320
|
+
const summary = salvageSummary({
|
|
145321
|
+
summary: result.summary,
|
|
145322
|
+
includedSequences: evidence.includedSequences
|
|
145323
|
+
});
|
|
145324
|
+
return summary === null ? null : {
|
|
145325
|
+
produced: Option.some({
|
|
145326
|
+
...result,
|
|
145327
|
+
summary
|
|
145328
|
+
}),
|
|
145329
|
+
reason: null
|
|
145330
|
+
};
|
|
145331
|
+
};
|
|
145157
145332
|
const attempt = yield* Effect.gen(function* () {
|
|
145158
145333
|
const first = yield* runSummary(handoffSummaryInstructions({ includedSequences: evidence.includedSequences }));
|
|
145159
145334
|
if (Option.isNone(first)) return noSummary("summary-unavailable");
|
|
@@ -145167,6 +145342,8 @@ const buildFusionEvidenceHandoff = Effect.fn("fusionEvidenceHandoff.build")(func
|
|
|
145167
145342
|
produced: first,
|
|
145168
145343
|
reason: null
|
|
145169
145344
|
};
|
|
145345
|
+
const firstSalvage = salvaged(first.value);
|
|
145346
|
+
if (firstSalvage !== null) return firstSalvage;
|
|
145170
145347
|
const repaired = yield* runSummary(handoffSummaryRepairInstructions({
|
|
145171
145348
|
rejection,
|
|
145172
145349
|
summary: first.value.summary,
|
|
@@ -145182,7 +145359,7 @@ const buildFusionEvidenceHandoff = Effect.fn("fusionEvidenceHandoff.build")(func
|
|
|
145182
145359
|
return repairedRejection === null ? {
|
|
145183
145360
|
produced: repaired,
|
|
145184
145361
|
reason: null
|
|
145185
|
-
} : noSummary(repairedRejection);
|
|
145362
|
+
} : salvaged(repaired.value) ?? noSummary(repairedRejection);
|
|
145186
145363
|
}).pipe(Effect.timeoutOption(HANDOFF_TIMEOUT), Effect.map(Option.getOrElse(() => noSummary("summary-unavailable"))));
|
|
145187
145364
|
const produced = attempt.produced;
|
|
145188
145365
|
if (Option.isNone(produced)) return yield* fallback(attempt.reason ?? "summary-unavailable");
|
|
@@ -145273,6 +145450,26 @@ const gateMessageId = (gateId, round) => MessageId.make(`${FUSION_GATE_MESSAGE_I
|
|
|
145273
145450
|
const gateActivityId = (gateId, threadId, suffix) => EventId.make(`fusion-gate:${gateId}:${suffix}:${threadId}`);
|
|
145274
145451
|
const pairFailureCommandId = (pairId, sequence, suffix) => CommandId.make(`server:fusion:${pairId}:provider-failure:${sequence}:${suffix}`);
|
|
145275
145452
|
const pairFailureActivityId = (pairId, sequence, threadId) => EventId.make(`fusion-provider-failure:${pairId}:${sequence}:${threadId}`);
|
|
145453
|
+
const PROVIDER_RETRY_MESSAGE_ID_PREFIX = "fusion-provider-retry:";
|
|
145454
|
+
const providerRetryCommandId = (pairId, sequence) => CommandId.make(`server:fusion:${pairId}:provider-retry:${sequence}`);
|
|
145455
|
+
/** Carries the origin and the failed completion's sequence ahead of the free-form pair id. */
|
|
145456
|
+
const providerRetryMessageId = (origin, sequence, pairId) => MessageId.make(`${PROVIDER_RETRY_MESSAGE_ID_PREFIX}${origin}:${sequence}:${pairId}`);
|
|
145457
|
+
const PROVIDER_RETRY_MESSAGE_ID = /^fusion-provider-retry:(review|other):(\d+):/u;
|
|
145458
|
+
/** The failed completion a review retry resumes, or null for any other message. */
|
|
145459
|
+
const retriedReviewSequence = (messageId) => {
|
|
145460
|
+
const match = messageId === null ? null : PROVIDER_RETRY_MESSAGE_ID.exec(messageId);
|
|
145461
|
+
return match?.[1] === "review" ? Number(match[2]) : null;
|
|
145462
|
+
};
|
|
145463
|
+
const TRANSIENT_PROVIDER_FAILURE = /runtime stream failed|stream disconnected|at capacity|overloaded|server failed|server error|internal error|\b(?:500|502|503|504|529)\b/i;
|
|
145464
|
+
const PERSISTENT_PROVIDER_FAILURE = /usage limit|rate limit|quota|billing|auth|log ?in|credential|unauthori[sz]ed|forbidden|\b(?:401|403)\b|does not exist|not found/i;
|
|
145465
|
+
/**
|
|
145466
|
+
* Whether a failed turn's recorded error is worth one automatic retry: a
|
|
145467
|
+
* dropped stream, a provider at capacity, or a server-side outage. Usage
|
|
145468
|
+
* limits, auth and missing paths never are, because retrying cannot fix them.
|
|
145469
|
+
*/
|
|
145470
|
+
function isTransientProviderFailure(lastError) {
|
|
145471
|
+
return typeof lastError === "string" && TRANSIENT_PROVIDER_FAILURE.test(lastError) && !PERSISTENT_PROVIDER_FAILURE.test(lastError);
|
|
145472
|
+
}
|
|
145276
145473
|
const idlePauseCommandId = (pairId, sequence, threadId) => CommandId.make(`server:fusion:${pairId}:idle-pause:${sequence}:${threadId}`);
|
|
145277
145474
|
const idlePauseActivityId = (pairId, sequence, threadId) => EventId.make(`fusion-idle-pause:${pairId}:${sequence}:${threadId}`);
|
|
145278
145475
|
/**
|
|
@@ -145551,10 +145748,11 @@ const make$3 = Effect.gen(function* () {
|
|
|
145551
145748
|
});
|
|
145552
145749
|
});
|
|
145553
145750
|
/**
|
|
145554
|
-
* A provider-level failure is not a review boundary.
|
|
145555
|
-
* it is still active, record one visible pair-level
|
|
145556
|
-
* idle until a human starts the next turn.
|
|
145557
|
-
* no-op, while ignoring interrupted
|
|
145751
|
+
* A provider-level failure that was not retried is not a review boundary.
|
|
145752
|
+
* Stop the other child if it is still active, record one visible pair-level
|
|
145753
|
+
* reason, and leave both idle until a human starts the next turn.
|
|
145754
|
+
* Deterministic ids make replay a no-op, while ignoring interrupted
|
|
145755
|
+
* completions prevents a reciprocal loop.
|
|
145558
145756
|
*/
|
|
145559
145757
|
const pausePairAfterProviderFailure = Effect.fn("FusionWatcherReactor.pausePairAfterProviderFailure")(function* (input) {
|
|
145560
145758
|
const { readModel } = yield* readPairs;
|
|
@@ -145591,6 +145789,46 @@ const make$3 = Effect.gen(function* () {
|
|
|
145591
145789
|
discard: true
|
|
145592
145790
|
});
|
|
145593
145791
|
});
|
|
145792
|
+
/**
|
|
145793
|
+
* Resumes a child whose turn failed on a transient provider error, once,
|
|
145794
|
+
* instead of pausing the pair and interrupting its healthy peer. True when
|
|
145795
|
+
* the retry went out (now or on an earlier pass); false sends the failure to
|
|
145796
|
+
* the pause. A retry turn that fails again, a turn whose start message is
|
|
145797
|
+
* unknown, a thread someone already started again, and a failure replayed
|
|
145798
|
+
* at boot are never retried.
|
|
145799
|
+
*/
|
|
145800
|
+
const retryTransientProviderFailure = Effect.fn("FusionWatcherReactor.retryTransientProviderFailure")(function* (input) {
|
|
145801
|
+
const { pair, completion, readModel } = input;
|
|
145802
|
+
const threadId = completion.payload.threadId;
|
|
145803
|
+
const retryIds = new Set(["review", "other"].map((origin) => providerRetryMessageId(origin, completion.sequence, pair.id)));
|
|
145804
|
+
const laterStarts = yield* orchestrationEngine.readEvents(completion.sequence, readModel.snapshotSequence - completion.sequence, { eventTypes: ["thread.turn-start-requested"] }).pipe(Stream.takeWhile((event) => event.sequence <= readModel.snapshotSequence), Stream.filter((event) => event.type === "thread.turn-start-requested" && event.payload.threadId === threadId), Stream.runCollect);
|
|
145805
|
+
if (laterStarts.some((event) => event.type === "thread.turn-start-requested" && retryIds.has(event.payload.messageId))) return true;
|
|
145806
|
+
if (input.replayed) return false;
|
|
145807
|
+
const thread = readModel.threads.find((candidate) => candidate.id === threadId && candidate.deletedAt === null);
|
|
145808
|
+
if (laterStarts.length > 0 || thread === void 0 || thread.latestTurn?.state === "running" || !isTransientProviderFailure(thread.session?.lastError) || completion.payload.turnId === null) return false;
|
|
145809
|
+
const started = yield* projectionSnapshotQuery.getTurnStart(threadId, completion.payload.turnId).pipe(Effect.map(Option.getOrNull), Effect.orElseSucceed(() => null));
|
|
145810
|
+
if (started === null || started.messageId === null || started.messageId.startsWith(PROVIDER_RETRY_MESSAGE_ID_PREFIX)) return false;
|
|
145811
|
+
return yield* orchestrationEngine.dispatch({
|
|
145812
|
+
type: "thread.turn.start",
|
|
145813
|
+
commandId: providerRetryCommandId(pair.id, completion.sequence),
|
|
145814
|
+
threadId,
|
|
145815
|
+
message: {
|
|
145816
|
+
messageId: providerRetryMessageId(started.messageId.startsWith("fusion-review:") ? "review" : "other", completion.sequence, pair.id),
|
|
145817
|
+
role: "user",
|
|
145818
|
+
text: `${FUSION_RETRY_PROMPT_PREFIX} Your previous turn stopped on a transient provider error. Continue the same work from where it stopped; no new work or authority is added. If it was already finished, end with the report you would have given.`,
|
|
145819
|
+
attachments: []
|
|
145820
|
+
},
|
|
145821
|
+
runtimeMode: thread.runtimeMode,
|
|
145822
|
+
interactionMode: thread.interactionMode,
|
|
145823
|
+
compressMode: thread.compressMode,
|
|
145824
|
+
unpromptedSubagents: thread.unpromptedSubagents,
|
|
145825
|
+
createdAt: completion.occurredAt
|
|
145826
|
+
}).pipe(Effect.as(true), Effect.catchCause((cause) => Effect.logWarning("provider failure retry was not sent; pausing the pair instead", {
|
|
145827
|
+
pairId: pair.id,
|
|
145828
|
+
sequence: completion.sequence,
|
|
145829
|
+
cause
|
|
145830
|
+
}).pipe(Effect.as(false))));
|
|
145831
|
+
});
|
|
145594
145832
|
const wakeWasRequested = Effect.fn("FusionWatcherReactor.wakeWasRequested")(function* (afterSequence, throughSequence, messageId) {
|
|
145595
145833
|
return (yield* orchestrationEngine.readEvents(afterSequence, throughSequence - afterSequence, { eventTypes: ["thread.turn-start-requested"] }).pipe(Stream.takeWhile((event) => event.sequence <= throughSequence), Stream.filter((event) => event.type === "thread.turn-start-requested" && event.payload.messageId === messageId), Stream.take(1), Stream.runCollect)).length > 0;
|
|
145596
145834
|
});
|
|
@@ -145925,7 +146163,7 @@ const make$3 = Effect.gen(function* () {
|
|
|
145925
146163
|
resolvedAt: event.payload.createdAt
|
|
145926
146164
|
});
|
|
145927
146165
|
});
|
|
145928
|
-
const isFusionProtocolText = (text) => text.startsWith("[fusion-review]") || text.startsWith("[fusion-gate]") || text.startsWith("[fusion-activation]") || text.startsWith("[fusion-jev]") || text.startsWith("[fusion-advice]");
|
|
146166
|
+
const isFusionProtocolText = (text) => text.startsWith("[fusion-review]") || text.startsWith("[fusion-gate]") || text.startsWith("[fusion-activation]") || text.startsWith("[fusion-jev]") || text.startsWith("[fusion-retry]") || text.startsWith("[fusion-advice]");
|
|
145929
146167
|
const processMessageSent = Effect.fn("FusionWatcherReactor.processMessageSent")(function* (event) {
|
|
145930
146168
|
if (event.sequence <= liveEventsAfterSequence) return;
|
|
145931
146169
|
if (event.payload.role !== "user") return;
|
|
@@ -146998,17 +147236,19 @@ Set outcome to summarized, fallback, or failed from the review that just ended.
|
|
|
146998
147236
|
if (Option.isNone(turn)) return;
|
|
146999
147237
|
const started = turn.value;
|
|
147000
147238
|
if (started.threadId !== pair.watcherThreadId) return;
|
|
147239
|
+
const retriedSequence = retriedReviewSequence(started.messageId);
|
|
147240
|
+
const retriedTurnId = retriedSequence === null ? null : yield* orchestrationEngine.readEvents(retriedSequence - 1, 1).pipe(Stream.runHead, Effect.map(Option.flatMap((event) => event.type === "thread.turn-completed" && event.payload.threadId === pair.watcherThreadId ? Option.fromNullishOr(event.payload.turnId) : Option.none())), Effect.map(Option.getOrNull), Effect.orElseSucceed(() => null));
|
|
147001
147241
|
const readAt = Date.parse(latest.readAt);
|
|
147002
147242
|
if (latest.turnId === null) {
|
|
147003
147243
|
if (input.replayed) return;
|
|
147004
147244
|
if (started.startedAt !== null && readAt < Date.parse(started.startedAt)) return;
|
|
147005
147245
|
if (readAt > Date.parse(completion.occurredAt)) return;
|
|
147006
|
-
} else if (latest.turnId !== completion.payload.turnId) return;
|
|
147246
|
+
} else if (latest.turnId !== completion.payload.turnId && (retriedTurnId === null || latest.turnId !== retriedTurnId)) return;
|
|
147007
147247
|
const wakeAt = (prefix) => projectionSnapshotQuery.getLatestThreadMessageWithIdPrefix(pair.watcherThreadId, prefix).pipe(Effect.map(Option.flatMap((wake) => {
|
|
147008
147248
|
const sentAt = Date.parse(wake.createdAt);
|
|
147009
147249
|
return Number.isNaN(sentAt) || sentAt > readAt ? Option.none() : Option.some(sentAt);
|
|
147010
147250
|
})), Effect.orElseSucceed(() => Option.none()));
|
|
147011
|
-
if (!(started.messageId === null ? yield* Effect.map(Effect.all([wakeAt("fusion-review:"), wakeAt("fusion-gate:")]), ([review, gate]) => Option.isSome(review) && (Option.isNone(gate) || gate.value < review.value)) : started.messageId.startsWith("fusion-review:"))) return;
|
|
147251
|
+
if (!(started.messageId === null ? yield* Effect.map(Effect.all([wakeAt("fusion-review:"), wakeAt("fusion-gate:")]), ([review, gate]) => Option.isSome(review) && (Option.isNone(gate) || gate.value < review.value)) : started.messageId.startsWith("fusion-review:") || retriedSequence !== null)) return;
|
|
147012
147252
|
if (latest.hasReport) {
|
|
147013
147253
|
if (input.watcher !== void 0) yield* dispatchEvidenceReportRepair(pair, input.watcher, completion.occurredAt, {
|
|
147014
147254
|
watchedThreadId: latest.watchedThreadId,
|
|
@@ -147024,6 +147264,12 @@ Set outcome to summarized, fallback, or failed from the review that just ended.
|
|
|
147024
147264
|
const activePairs = (readModel.threadPairs ?? []).filter((pair) => pair.detachedAt === null);
|
|
147025
147265
|
const failedPair = activePairs.find((pair) => pair.implementerThreadId === event.payload.threadId || pair.watcherThreadId === event.payload.threadId);
|
|
147026
147266
|
if (event.payload.state === "failed" && failedPair !== void 0) {
|
|
147267
|
+
if (yield* retryTransientProviderFailure({
|
|
147268
|
+
pair: failedPair,
|
|
147269
|
+
completion: event,
|
|
147270
|
+
readModel,
|
|
147271
|
+
replayed: event.sequence <= liveEventsAfterSequence
|
|
147272
|
+
})) return;
|
|
147027
147273
|
yield* pausePairAfterProviderFailure({
|
|
147028
147274
|
pair: failedPair,
|
|
147029
147275
|
failedThreadId: event.payload.threadId,
|