codeden 0.6.2 → 0.6.3
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/cli.js +1 -1
- package/dist/drizzle/20261112140714_turn_fork_point/migration.sql +1 -0
- package/dist/drizzle/20261112140714_turn_fork_point/snapshot.json +3524 -0
- package/dist/{main-hdQfX9V6.js → main-Bx58CdV4.js} +1375 -559
- package/dist/main-Bx58CdV4.js.map +1 -0
- package/dist/{main-DZBmzxSH.js → main-CMt4z1Z9.js} +2 -2
- package/dist/{main-DZBmzxSH.js.map → main-CMt4z1Z9.js.map} +1 -1
- package/dist/{mediaType-nJs-4z5U.js → mediaType-BCz-hbZl.js} +2 -2
- package/dist/{mediaType-nJs-4z5U.js.map → mediaType-BCz-hbZl.js.map} +1 -1
- package/dist/{runner-DYf8dV43.js → runner-CiaiUWOE.js} +636 -71
- package/dist/runner-CiaiUWOE.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/{server-test-COAiKxTV.js → server-test-b0hHPqXb.js} +3 -3
- package/dist/{server-test-COAiKxTV.js.map → server-test-b0hHPqXb.js.map} +1 -1
- package/dist/{tree-CF7Q8tXK.js → tree-31WAOm5k.js} +184 -23
- package/dist/tree-31WAOm5k.js.map +1 -0
- package/dist/web/assets/{core-Rhf-hNq6.js → core-FqX7NQzW.js} +1 -1
- package/dist/web/assets/engine-oniguruma-C5UP18m1.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-DcKwRTm5.js → highlighted-body-JLMTFACH-BMKFJRL-.js} +1 -1
- package/dist/web/assets/{highlights-DCtMC0YR.js → highlights-9yuGk1pg.js} +1 -1
- package/dist/web/assets/{index-BLggvVMT.js → index-C3QkKvUn.js} +233 -233
- package/dist/web/assets/index-CmllrYFx.css +1 -0
- package/dist/web/assets/langs-DfCgj44a.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-zTA5f8lD.js +1 -0
- package/dist/web/assets/{shiki-B3-ws3OK.js → shiki-Qa-y3xHo.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-hKFrMxml.js → stream-tokenizer-VL43-ily.js} +1 -1
- package/dist/web/assets/{themes-Cvhh4Ovr.js → themes-Di4DO5zr.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/main-hdQfX9V6.js.map +0 -1
- package/dist/runner-DYf8dV43.js.map +0 -1
- package/dist/tree-CF7Q8tXK.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-BbpT993F.js +0 -1
- package/dist/web/assets/index-JjF9o00P.css +0 -1
- package/dist/web/assets/langs-B2ZGl3f_.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-C2_MUy_a.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DegdyBIC.js";
|
|
2
|
-
import {
|
|
2
|
+
import { Ar as ReasoningEffort, Bn as BROWSER_RELAY_SECRET_HEADER, Hn as RuntimeResponse, Vn as ProviderCapabilities, Wn as ProjectConfigFile, a as childEnv, ar as winningCommands, d as RunnerLock, er as AgentEvent, i as listDescendants, ir as oneLineDescription, m as ServerToRunnerMessage, n as killTree$1, nr as mergeItemDeltas, o as CODEDEN_VERSION, or as itemDeltaText, sr as UserInput, t as isAlive$1, u as RunnerEventMessage, zn as SETUP_TAIL_LIMIT } from "./tree-31WAOm5k.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { accessSync, appendFileSync, chmodSync, closeSync, constants, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
@@ -3612,6 +3612,8 @@ var import_websocket = /* @__PURE__ */ __toESM(require_websocket(), 1);
|
|
|
3612
3612
|
var import_websocket_server = /* @__PURE__ */ __toESM(require_websocket_server(), 1);
|
|
3613
3613
|
var wrapper_default = import_websocket.default;
|
|
3614
3614
|
// ../../packages/providers/src/adapter.ts
|
|
3615
|
+
/** `ProviderSessionError` code: the conversation doesn't hold the fork point it was asked for. */
|
|
3616
|
+
const FORK_POINT_MISSING = "fork_point_missing";
|
|
3615
3617
|
/**
|
|
3616
3618
|
* Session options ready to open (`SessionOptions.forkFrom`): with `forkFrom` and no
|
|
3617
3619
|
* `resumeToken`, the conversation is copied first (`forkConversation`) and the session resumes
|
|
@@ -4513,6 +4515,33 @@ function parseClaudeAuthStatus(stdout) {
|
|
|
4513
4515
|
return unknown;
|
|
4514
4516
|
}
|
|
4515
4517
|
}
|
|
4518
|
+
// ../../packages/providers/src/claude/fork.ts
|
|
4519
|
+
const reasonOf = (error) => error instanceof Error ? error.message : String(error);
|
|
4520
|
+
/**
|
|
4521
|
+
* Copy a Claude conversation with the SDK's `forkSession()`: the transcript is written to a new
|
|
4522
|
+
* session file at once (fresh message ids, the original untouched) and the new session id is
|
|
4523
|
+
* returned for resuming. With `upTo` (a turn's fork point: its last transcript message), the
|
|
4524
|
+
* transcript is read first (`getSessionMessages`) to confirm it still holds that message, and
|
|
4525
|
+
* the copy ends with it. A conversation that no longer holds it (replaced or deleted) throws
|
|
4526
|
+
* `FORK_POINT_MISSING` before anything is copied, so it never becomes a whole copy instead.
|
|
4527
|
+
*/
|
|
4528
|
+
async function forkClaudeConversation(sdk, resumeToken, upTo) {
|
|
4529
|
+
if (upTo) await confirmForkPoint(sdk, resumeToken, upTo);
|
|
4530
|
+
try {
|
|
4531
|
+
return (await sdk.forkSession(resumeToken, upTo ? { upToMessageId: upTo } : void 0)).sessionId;
|
|
4532
|
+
} catch (error) {
|
|
4533
|
+
throw new Error(`Could not fork the Claude conversation ${resumeToken}: ${reasonOf(error)}`);
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
async function confirmForkPoint(sdk, resumeToken, upTo) {
|
|
4537
|
+
let messages;
|
|
4538
|
+
try {
|
|
4539
|
+
messages = await sdk.getSessionMessages(resumeToken, { includeSystemMessages: true });
|
|
4540
|
+
} catch (error) {
|
|
4541
|
+
throw new Error(`Could not read the Claude conversation ${resumeToken}: ${reasonOf(error)}`);
|
|
4542
|
+
}
|
|
4543
|
+
if (!messages.some((m) => m.uuid === upTo)) throw new ProviderSessionError(FORK_POINT_MISSING, "The conversation no longer holds the end of that turn (it was replaced or deleted), so it can't be forked there");
|
|
4544
|
+
}
|
|
4516
4545
|
// ../../packages/providers/src/util/model.ts
|
|
4517
4546
|
/** The id the model pickers use for "the provider's own default model". */
|
|
4518
4547
|
const DEFAULT_MODEL_ID = "default";
|
|
@@ -8434,6 +8463,12 @@ var ClaudeEventMapper = class {
|
|
|
8434
8463
|
retryNoticeId;
|
|
8435
8464
|
/** What the current turn said about a failure, for classifying it (`errors.ts`). */
|
|
8436
8465
|
signals = {};
|
|
8466
|
+
/**
|
|
8467
|
+
* The `uuid` of the current root turn's last main-chain transcript message (an `assistant`
|
|
8468
|
+
* or `user` message without `parent_tool_use_id`): the turn's fork point, where
|
|
8469
|
+
* `forkSession({ upToMessageId })` ends a copy that keeps this turn and no later one.
|
|
8470
|
+
*/
|
|
8471
|
+
lastChainUuid;
|
|
8437
8472
|
/** The `/` command list last reported, so an unchanged list isn't sent again. */
|
|
8438
8473
|
commandsKey;
|
|
8439
8474
|
constructor(opts) {
|
|
@@ -8491,6 +8526,7 @@ var ClaudeEventMapper = class {
|
|
|
8491
8526
|
this.rootTurnId = turnId;
|
|
8492
8527
|
this.turnBaseline = this.queryTotal;
|
|
8493
8528
|
this.signals = {};
|
|
8529
|
+
this.lastChainUuid = void 0;
|
|
8494
8530
|
this.emit({
|
|
8495
8531
|
type: "turn.started",
|
|
8496
8532
|
threadId: this.rootThreadId,
|
|
@@ -8535,9 +8571,11 @@ var ClaudeEventMapper = class {
|
|
|
8535
8571
|
return;
|
|
8536
8572
|
case "assistant":
|
|
8537
8573
|
this.onAssistant(msg);
|
|
8574
|
+
this.noteChainMessage(msg);
|
|
8538
8575
|
return;
|
|
8539
8576
|
case "user":
|
|
8540
8577
|
this.onUser(msg);
|
|
8578
|
+
this.noteChainMessage(msg);
|
|
8541
8579
|
return;
|
|
8542
8580
|
case "system":
|
|
8543
8581
|
this.onSystem(msg);
|
|
@@ -8560,6 +8598,16 @@ var ClaudeEventMapper = class {
|
|
|
8560
8598
|
default: return;
|
|
8561
8599
|
}
|
|
8562
8600
|
}
|
|
8601
|
+
/**
|
|
8602
|
+
* Keep a root turn's latest main-chain message as its fork point. Subagents' messages don't
|
|
8603
|
+
* count, nor do meta or synthetic ones, which the transcript read that confirms a fork point
|
|
8604
|
+
* (`getSessionMessages`) may leave out.
|
|
8605
|
+
*/
|
|
8606
|
+
noteChainMessage(msg) {
|
|
8607
|
+
if (msg.parent_tool_use_id || !this.rootTurnId || msg.isMeta || msg.isSynthetic) return;
|
|
8608
|
+
const uuid = str(msg, "uuid");
|
|
8609
|
+
if (uuid) this.lastChainUuid = uuid;
|
|
8610
|
+
}
|
|
8563
8611
|
onAssistant(msg) {
|
|
8564
8612
|
const message = msg.message ?? {};
|
|
8565
8613
|
const ctx = this.ctxFor(msg.parent_tool_use_id);
|
|
@@ -8703,19 +8751,24 @@ var ClaudeEventMapper = class {
|
|
|
8703
8751
|
this.tools.finishTurn(ctx, outcome.type === "completed");
|
|
8704
8752
|
this.rootTurnId = void 0;
|
|
8705
8753
|
this.interruptRequested = false;
|
|
8754
|
+
const end = {
|
|
8755
|
+
...ctx,
|
|
8756
|
+
...this.lastChainUuid ? { forkPoint: this.lastChainUuid } : {}
|
|
8757
|
+
};
|
|
8758
|
+
this.lastChainUuid = void 0;
|
|
8706
8759
|
if (outcome.type === "completed") this.emit({
|
|
8707
8760
|
type: "turn.completed",
|
|
8708
|
-
...
|
|
8761
|
+
...end,
|
|
8709
8762
|
stopReason: outcome.stopReason
|
|
8710
8763
|
});
|
|
8711
8764
|
else if (outcome.type === "failed") this.emit({
|
|
8712
8765
|
type: "turn.failed",
|
|
8713
|
-
...
|
|
8766
|
+
...end,
|
|
8714
8767
|
error: outcome.error
|
|
8715
8768
|
});
|
|
8716
8769
|
else this.emit({
|
|
8717
8770
|
type: "turn.interrupted",
|
|
8718
|
-
...
|
|
8771
|
+
...end,
|
|
8719
8772
|
reason: outcome.reason
|
|
8720
8773
|
});
|
|
8721
8774
|
}
|
|
@@ -9405,18 +9458,12 @@ var ClaudeAdapter = class {
|
|
|
9405
9458
|
});
|
|
9406
9459
|
}
|
|
9407
9460
|
/**
|
|
9408
|
-
* Copy a conversation
|
|
9409
|
-
*
|
|
9410
|
-
*
|
|
9461
|
+
* Copy a conversation (see `forkClaudeConversation`): at once, with fresh message ids, the
|
|
9462
|
+
* original untouched; with `upTo`, only through the turn that ended with that message. The
|
|
9463
|
+
* copy is found from any worktree of the project.
|
|
9411
9464
|
*/
|
|
9412
|
-
async forkConversation(resumeToken) {
|
|
9413
|
-
|
|
9414
|
-
try {
|
|
9415
|
-
return (await forkSession(resumeToken)).sessionId;
|
|
9416
|
-
} catch (error) {
|
|
9417
|
-
const reason = error instanceof Error ? error.message : String(error);
|
|
9418
|
-
throw new Error(`Could not fork the Claude conversation ${resumeToken}: ${reason}`);
|
|
9419
|
-
}
|
|
9465
|
+
async forkConversation(resumeToken, opts) {
|
|
9466
|
+
return forkClaudeConversation(await (this.options.loadSdk ?? loadClaudeSdk)(), resumeToken, opts?.upTo);
|
|
9420
9467
|
}
|
|
9421
9468
|
utilityModel() {
|
|
9422
9469
|
return new ClaudeUtilityModel(() => this.executable());
|
|
@@ -9733,11 +9780,27 @@ async function startAppServer(exe, handlers, opts = {}) {
|
|
|
9733
9780
|
}
|
|
9734
9781
|
}
|
|
9735
9782
|
// ../../packages/providers/src/codex/fork.ts
|
|
9783
|
+
/** Turns listed per `thread/turns/list` page, and the most pages read to find a turn. */
|
|
9784
|
+
const TURNS_PAGE = 100;
|
|
9785
|
+
const MAX_PAGES$1 = 50;
|
|
9786
|
+
/**
|
|
9787
|
+
* How the app-server answers for a thread or turn it doesn't have. Codex 0.160 says
|
|
9788
|
+
* `no rollout found for thread id <id>` for a deleted thread and `turn not found: <id>` for a
|
|
9789
|
+
* missing `lastTurnId`. Other refusals (a turn still in progress, a malformed id) don't match.
|
|
9790
|
+
*/
|
|
9791
|
+
const MISSING = /\bno rollout found\b|\b(thread|turn|rollout) not found\b/i;
|
|
9736
9792
|
/**
|
|
9737
9793
|
* Copy a Codex thread with `thread/fork` through a short-lived `codex app-server` and return
|
|
9738
9794
|
* the new thread's id. The original thread is untouched; the session that resumes the copy
|
|
9739
9795
|
* sets its own cwd, policy and instructions (`thread/resume` overrides). The process is closed
|
|
9740
9796
|
* before returning, so it no longer holds the new thread when that session resumes it.
|
|
9797
|
+
*
|
|
9798
|
+
* With `upTo` (a turn id), the copy ends with that turn (`lastTurnId`). A thread that no
|
|
9799
|
+
* longer holds the turn, or no longer exists, throws `FORK_POINT_MISSING`: when the turn list
|
|
9800
|
+
* (`thread/turns/list`) shows it before anything is copied, or else when `thread/fork` answers
|
|
9801
|
+
* so. A short-lived app-server usually can't list a thread it hasn't loaded (Codex 0.160 says
|
|
9802
|
+
* `thread not loaded`), so `thread/fork`'s answer is what decides then. Any other failure, and
|
|
9803
|
+
* every failure of a whole copy, stays a plain error.
|
|
9741
9804
|
*/
|
|
9742
9805
|
async function forkCodexThread(exe, threadId, opts) {
|
|
9743
9806
|
const { rpc } = await startAppServer(exe, {
|
|
@@ -9746,17 +9809,47 @@ async function forkCodexThread(exe, threadId, opts) {
|
|
|
9746
9809
|
onExit: () => {}
|
|
9747
9810
|
}, { cwd: opts.cwd });
|
|
9748
9811
|
try {
|
|
9749
|
-
|
|
9812
|
+
if (opts.upTo && await holdsTurn(rpc, threadId, opts.upTo) === false) throw new ProviderSessionError(FORK_POINT_MISSING, "The thread no longer holds that turn (it was replaced or deleted), so it can't be forked there");
|
|
9813
|
+
const params = {
|
|
9750
9814
|
threadId,
|
|
9751
9815
|
cwd: opts.cwd,
|
|
9752
|
-
excludeTurns: true
|
|
9753
|
-
|
|
9816
|
+
excludeTurns: true,
|
|
9817
|
+
...opts.upTo ? { lastTurnId: opts.upTo } : {}
|
|
9818
|
+
};
|
|
9819
|
+
return (await rpc.request("thread/fork", params)).thread.id;
|
|
9754
9820
|
} catch (error) {
|
|
9755
|
-
|
|
9821
|
+
if (error instanceof ProviderSessionError) throw error;
|
|
9822
|
+
const message = error.message;
|
|
9823
|
+
if (opts.upTo && error instanceof CodexRpcError && MISSING.test(message)) throw new ProviderSessionError(FORK_POINT_MISSING, `The thread no longer holds that turn (${message}), so it can't be forked there`);
|
|
9824
|
+
throw new Error(`Could not fork Codex thread ${threadId}: ${message}`);
|
|
9756
9825
|
} finally {
|
|
9757
9826
|
await rpc.close();
|
|
9758
9827
|
}
|
|
9759
9828
|
}
|
|
9829
|
+
/**
|
|
9830
|
+
* Whether the thread still holds `turnId`, from its turn list. Undefined when the list can't be
|
|
9831
|
+
* read (a thread this app-server hasn't loaded, or an app-server without the method):
|
|
9832
|
+
* `thread/fork` then decides.
|
|
9833
|
+
*/
|
|
9834
|
+
async function holdsTurn(rpc, threadId, turnId) {
|
|
9835
|
+
let cursor = null;
|
|
9836
|
+
try {
|
|
9837
|
+
for (let page = 0; page < MAX_PAGES$1; page++) {
|
|
9838
|
+
const res = await rpc.request("thread/turns/list", {
|
|
9839
|
+
threadId,
|
|
9840
|
+
limit: TURNS_PAGE,
|
|
9841
|
+
itemsView: "notLoaded",
|
|
9842
|
+
...cursor ? { cursor } : {}
|
|
9843
|
+
});
|
|
9844
|
+
if (res.data.some((t) => t.id === turnId)) return true;
|
|
9845
|
+
cursor = res.nextCursor;
|
|
9846
|
+
if (!cursor) return false;
|
|
9847
|
+
}
|
|
9848
|
+
return;
|
|
9849
|
+
} catch {
|
|
9850
|
+
return;
|
|
9851
|
+
}
|
|
9852
|
+
}
|
|
9760
9853
|
/** `model/list` pages are followed up to this many (a guard against a looping cursor). */
|
|
9761
9854
|
const MAX_PAGES = 20;
|
|
9762
9855
|
/**
|
|
@@ -9946,7 +10039,7 @@ function permissionsRisk(permissions, ctx) {
|
|
|
9946
10039
|
})
|
|
9947
10040
|
]);
|
|
9948
10041
|
}
|
|
9949
|
-
// ../../packages/providers/src/codex/
|
|
10042
|
+
// ../../packages/providers/src/codex/approval-context.ts
|
|
9950
10043
|
const decisionOf = (r) => r.kind === "approval" ? r.decision : "cancel";
|
|
9951
10044
|
/**
|
|
9952
10045
|
* Approve `item` by policy when the permission level allows `risk`: record it as decided by
|
|
@@ -9965,6 +10058,296 @@ function approvedByPolicy(ac, p, item, risk) {
|
|
|
9965
10058
|
});
|
|
9966
10059
|
return true;
|
|
9967
10060
|
}
|
|
10061
|
+
// ../../packages/providers/src/codex/elicitation-form.ts
|
|
10062
|
+
/** The option that leaves an optional field empty. */
|
|
10063
|
+
const LEAVE_EMPTY = "Leave empty";
|
|
10064
|
+
/** A form without fields is a confirmation: one question, accept or decline. */
|
|
10065
|
+
const CONFIRM_ID = "confirm";
|
|
10066
|
+
const ACCEPT = "Accept";
|
|
10067
|
+
const DECLINE = "Decline";
|
|
10068
|
+
function formFields(schema) {
|
|
10069
|
+
const required = new Set(schema.required ?? []);
|
|
10070
|
+
return Object.entries(schema.properties).flatMap(([name, prop], i) => prop ? [field(name, i, prop, required.has(name))] : []);
|
|
10071
|
+
}
|
|
10072
|
+
function field(name, index, s, required) {
|
|
10073
|
+
const description = s.description?.trim();
|
|
10074
|
+
const base = {
|
|
10075
|
+
name,
|
|
10076
|
+
id: name.length > 0 && name.length <= 200 ? name : `field-${index}`,
|
|
10077
|
+
label: s.title?.trim() || name,
|
|
10078
|
+
...description ? { description } : {},
|
|
10079
|
+
required,
|
|
10080
|
+
choices: []
|
|
10081
|
+
};
|
|
10082
|
+
if (s.type === "boolean") return {
|
|
10083
|
+
...base,
|
|
10084
|
+
type: "boolean"
|
|
10085
|
+
};
|
|
10086
|
+
if (s.type === "number" || s.type === "integer") return {
|
|
10087
|
+
...base,
|
|
10088
|
+
type: s.type
|
|
10089
|
+
};
|
|
10090
|
+
if (s.type === "array") {
|
|
10091
|
+
const items = s.items;
|
|
10092
|
+
const choices = "anyOf" in items ? items.anyOf.map((o) => ({
|
|
10093
|
+
value: o.const,
|
|
10094
|
+
label: o.title
|
|
10095
|
+
})) : items.enum.map((value) => ({
|
|
10096
|
+
value,
|
|
10097
|
+
label: value
|
|
10098
|
+
}));
|
|
10099
|
+
return {
|
|
10100
|
+
...base,
|
|
10101
|
+
type: "choices",
|
|
10102
|
+
choices: unique(choices)
|
|
10103
|
+
};
|
|
10104
|
+
}
|
|
10105
|
+
if ("oneOf" in s) return {
|
|
10106
|
+
...base,
|
|
10107
|
+
type: "choice",
|
|
10108
|
+
choices: unique(s.oneOf.map((o) => ({
|
|
10109
|
+
value: o.const,
|
|
10110
|
+
label: o.title
|
|
10111
|
+
})))
|
|
10112
|
+
};
|
|
10113
|
+
if ("enum" in s) {
|
|
10114
|
+
const names = "enumNames" in s ? s.enumNames : void 0;
|
|
10115
|
+
const choices = s.enum.map((value, i) => ({
|
|
10116
|
+
value,
|
|
10117
|
+
label: names?.[i] ?? value
|
|
10118
|
+
}));
|
|
10119
|
+
return {
|
|
10120
|
+
...base,
|
|
10121
|
+
type: "choice",
|
|
10122
|
+
choices: unique(choices)
|
|
10123
|
+
};
|
|
10124
|
+
}
|
|
10125
|
+
return {
|
|
10126
|
+
...base,
|
|
10127
|
+
type: "text"
|
|
10128
|
+
};
|
|
10129
|
+
}
|
|
10130
|
+
/**
|
|
10131
|
+
* Labels are how answers come back, so each must name exactly one value: a repeated label (or
|
|
10132
|
+
* one that reads as {@link LEAVE_EMPTY}) gets its value added, then a number, until it is free.
|
|
10133
|
+
*/
|
|
10134
|
+
function unique(choices) {
|
|
10135
|
+
const seen = /* @__PURE__ */ new Set([LEAVE_EMPTY]);
|
|
10136
|
+
return choices.map((c) => {
|
|
10137
|
+
const label = c.label.trim() || c.value;
|
|
10138
|
+
let free = seen.has(label) ? `${label} (${c.value})` : label;
|
|
10139
|
+
for (let n = 2; seen.has(free); n++) free = `${label} (${c.value}) ${n}`;
|
|
10140
|
+
seen.add(free);
|
|
10141
|
+
return {
|
|
10142
|
+
value: c.value,
|
|
10143
|
+
label: free
|
|
10144
|
+
};
|
|
10145
|
+
});
|
|
10146
|
+
}
|
|
10147
|
+
const HINTS = {
|
|
10148
|
+
integer: " (a whole number)",
|
|
10149
|
+
number: " (a number)"
|
|
10150
|
+
};
|
|
10151
|
+
/** The form as questions; the first one carries the server's message. */
|
|
10152
|
+
function formQuestions(server, message, fields) {
|
|
10153
|
+
const lead = `${server} asks: ${message.trim()}`;
|
|
10154
|
+
if (fields.length === 0) return [{
|
|
10155
|
+
id: CONFIRM_ID,
|
|
10156
|
+
header: server,
|
|
10157
|
+
question: lead,
|
|
10158
|
+
options: [{ label: ACCEPT }, { label: DECLINE }],
|
|
10159
|
+
multiSelect: false,
|
|
10160
|
+
allowFreeText: false
|
|
10161
|
+
}];
|
|
10162
|
+
return fields.map((f, i) => {
|
|
10163
|
+
const ask = `${f.description ?? f.label}${HINTS[f.type] ?? ""}`;
|
|
10164
|
+
const plain = fields.length === 1 && ask === f.label;
|
|
10165
|
+
const question = i > 0 ? ask : plain ? lead : `${lead} — ${ask}`;
|
|
10166
|
+
const options = f.type === "boolean" ? [{ label: "Yes" }, { label: "No" }] : f.choices.map((c) => ({ label: c.label }));
|
|
10167
|
+
return {
|
|
10168
|
+
id: f.id,
|
|
10169
|
+
header: f.label.length > 40 ? `${f.label.slice(0, 39)}…` : f.label,
|
|
10170
|
+
question,
|
|
10171
|
+
options: f.required ? options : [...options, { label: LEAVE_EMPTY }],
|
|
10172
|
+
multiSelect: f.type === "choices",
|
|
10173
|
+
allowFreeText: f.type === "text" || f.type === "number" || f.type === "integer"
|
|
10174
|
+
};
|
|
10175
|
+
});
|
|
10176
|
+
}
|
|
10177
|
+
/**
|
|
10178
|
+
* The form's content from the answers (keyed by question id), or undefined when a required
|
|
10179
|
+
* field has no answer or an answer doesn't fit its field.
|
|
10180
|
+
*/
|
|
10181
|
+
function formContent(fields, answers) {
|
|
10182
|
+
const content = {};
|
|
10183
|
+
for (const f of fields) {
|
|
10184
|
+
const given = (answers[f.id] ?? []).map((a) => a.trim()).filter(Boolean);
|
|
10185
|
+
const values = f.required ? given : given.filter((a) => a !== LEAVE_EMPTY);
|
|
10186
|
+
if (values.length === 0) {
|
|
10187
|
+
if (f.required) return void 0;
|
|
10188
|
+
continue;
|
|
10189
|
+
}
|
|
10190
|
+
const value = parse$1(f, values);
|
|
10191
|
+
if (value === void 0) return void 0;
|
|
10192
|
+
content[f.name] = value;
|
|
10193
|
+
}
|
|
10194
|
+
return content;
|
|
10195
|
+
}
|
|
10196
|
+
function parse$1(f, values) {
|
|
10197
|
+
const [one] = values;
|
|
10198
|
+
switch (f.type) {
|
|
10199
|
+
case "text": return values.join("\n");
|
|
10200
|
+
case "number":
|
|
10201
|
+
case "integer": {
|
|
10202
|
+
const n = values.length === 1 ? Number(one) : NaN;
|
|
10203
|
+
return Number.isFinite(n) && (f.type === "number" || Number.isInteger(n)) ? n : void 0;
|
|
10204
|
+
}
|
|
10205
|
+
case "boolean": {
|
|
10206
|
+
const v = values.length === 1 ? one?.toLowerCase() : void 0;
|
|
10207
|
+
return v === "yes" || v === "true" ? true : v === "no" || v === "false" ? false : void 0;
|
|
10208
|
+
}
|
|
10209
|
+
case "choice": return values.length === 1 && one !== void 0 ? choice(f, one) : void 0;
|
|
10210
|
+
case "choices": {
|
|
10211
|
+
const picked = values.map((v) => choice(f, v));
|
|
10212
|
+
return picked.every((v) => v !== void 0) ? picked : void 0;
|
|
10213
|
+
}
|
|
10214
|
+
}
|
|
10215
|
+
}
|
|
10216
|
+
/**
|
|
10217
|
+
* The value of the option the user picked. The card sends labels, so labels are matched first,
|
|
10218
|
+
* across every option; a raw value (a PM may answer with one) only when no label matches.
|
|
10219
|
+
*/
|
|
10220
|
+
const choice = (f, answer) => (f.choices.find((c) => c.label === answer) ?? f.choices.find((c) => c.value === answer))?.value;
|
|
10221
|
+
// ../../packages/providers/src/codex/elicitation.ts
|
|
10222
|
+
const accept = (content, meta = null) => ({
|
|
10223
|
+
action: "accept",
|
|
10224
|
+
content,
|
|
10225
|
+
_meta: meta
|
|
10226
|
+
});
|
|
10227
|
+
const DECLINED = {
|
|
10228
|
+
action: "decline",
|
|
10229
|
+
content: null,
|
|
10230
|
+
_meta: null
|
|
10231
|
+
};
|
|
10232
|
+
const CANCELLED = {
|
|
10233
|
+
action: "cancel",
|
|
10234
|
+
content: null,
|
|
10235
|
+
_meta: null
|
|
10236
|
+
};
|
|
10237
|
+
const READ_ONLY = "This session has read-only permissions: it can't run MCP tools that may change things.";
|
|
10238
|
+
async function handleElicitation(p, ac) {
|
|
10239
|
+
if (!p.turnId) return DECLINED;
|
|
10240
|
+
const where = {
|
|
10241
|
+
threadId: p.threadId,
|
|
10242
|
+
turnId: p.turnId
|
|
10243
|
+
};
|
|
10244
|
+
if (p.mode === "url") return openLink(p, where, ac);
|
|
10245
|
+
const meta = asMeta(p._meta);
|
|
10246
|
+
if (meta.codex_approval_kind === "mcp_tool_call") return toolApproval(p, meta, where, ac);
|
|
10247
|
+
if (p.mode === "form") return fillForm(p, where, ac);
|
|
10248
|
+
return DECLINED;
|
|
10249
|
+
}
|
|
10250
|
+
/**
|
|
10251
|
+
* Codex asks before it runs one of the user's MCP tools that isn't marked read-only (its
|
|
10252
|
+
* `tool_call_mcp_elicitation` feature, on by default).
|
|
10253
|
+
*/
|
|
10254
|
+
async function toolApproval(p, meta, where, ac) {
|
|
10255
|
+
const server = p.serverName;
|
|
10256
|
+
const tool = text$1(meta.tool_name) ?? text$1(meta.tool_title) ?? /run tool "([^"]+)"/.exec(p.message)?.[1];
|
|
10257
|
+
const persist = meta.persist;
|
|
10258
|
+
const offersSession = persist === "session" || Array.isArray(persist) && persist.includes("session");
|
|
10259
|
+
const requestId = newRequestId();
|
|
10260
|
+
const item = {
|
|
10261
|
+
id: `approval-${requestId}`,
|
|
10262
|
+
kind: "approval_request",
|
|
10263
|
+
status: "in_progress",
|
|
10264
|
+
requestId,
|
|
10265
|
+
action: "tool",
|
|
10266
|
+
title: tool ? `Use ${server}: ${shorten(tool, 80)}` : `Use a tool of ${server}`,
|
|
10267
|
+
...p.message.trim() ? { reason: p.message.trim() } : {},
|
|
10268
|
+
...tool ? { toolName: `mcp__${server}__${tool}` } : {},
|
|
10269
|
+
...meta.tool_params !== void 0 ? { toolInput: toJson(meta.tool_params) } : {},
|
|
10270
|
+
options: offersSession ? [
|
|
10271
|
+
"allow_once",
|
|
10272
|
+
"allow_session",
|
|
10273
|
+
"deny"
|
|
10274
|
+
] : ["allow_once", "deny"]
|
|
10275
|
+
};
|
|
10276
|
+
const ctx = ac.ctx(where.threadId, where.turnId);
|
|
10277
|
+
if ((ac.autonomy?.() ?? "supervised") === "read_only") {
|
|
10278
|
+
ac.broker.record(ctx, {
|
|
10279
|
+
...item,
|
|
10280
|
+
status: "declined",
|
|
10281
|
+
reason: READ_ONLY,
|
|
10282
|
+
decision: "deny",
|
|
10283
|
+
decidedBy: "policy"
|
|
10284
|
+
});
|
|
10285
|
+
return DECLINED;
|
|
10286
|
+
}
|
|
10287
|
+
if (!(meta.codex_sensitive_action === true || meta.codex_requires_user_input === true) && approvedByPolicy(ac, where, item, { risky: false })) return accept({});
|
|
10288
|
+
const decision = decisionOf(await ac.broker.request(ctx, item));
|
|
10289
|
+
if (decision === "allow_session" && offersSession) return accept({}, { persist: "session" });
|
|
10290
|
+
if (decision === "allow_once" || decision === "allow_session") return accept({});
|
|
10291
|
+
return decision === "deny" ? DECLINED : CANCELLED;
|
|
10292
|
+
}
|
|
10293
|
+
/** A server's form: a question card built from its schema, the answers sent as its content. */
|
|
10294
|
+
async function fillForm(p, where, ac) {
|
|
10295
|
+
const fields = formFields(p.requestedSchema);
|
|
10296
|
+
const requestId = newRequestId();
|
|
10297
|
+
const response = await ac.broker.request(ac.ctx(where.threadId, where.turnId), {
|
|
10298
|
+
id: `question-${requestId}`,
|
|
10299
|
+
kind: "user_question",
|
|
10300
|
+
status: "in_progress",
|
|
10301
|
+
requestId,
|
|
10302
|
+
mcpServer: p.serverName,
|
|
10303
|
+
questions: formQuestions(p.serverName, p.message, fields)
|
|
10304
|
+
});
|
|
10305
|
+
if (response.kind !== "answer") return CANCELLED;
|
|
10306
|
+
if (fields.length === 0) return response.answers["confirm"]?.includes("Accept") ? accept({}) : DECLINED;
|
|
10307
|
+
const content = formContent(fields, response.answers);
|
|
10308
|
+
return content ? accept(content) : DECLINED;
|
|
10309
|
+
}
|
|
10310
|
+
/**
|
|
10311
|
+
* A server asks the user to open a link (to sign in, say). The card shows the link and the user
|
|
10312
|
+
* opens it: never decided by policy, and nothing opens it on the user's behalf.
|
|
10313
|
+
*/
|
|
10314
|
+
async function openLink(p, where, ac) {
|
|
10315
|
+
const url = webUrl(p.url);
|
|
10316
|
+
if (!url) return DECLINED;
|
|
10317
|
+
const requestId = newRequestId();
|
|
10318
|
+
const response = await ac.broker.request(ac.ctx(where.threadId, where.turnId), {
|
|
10319
|
+
id: `approval-${requestId}`,
|
|
10320
|
+
kind: "approval_request",
|
|
10321
|
+
status: "in_progress",
|
|
10322
|
+
requestId,
|
|
10323
|
+
action: "network",
|
|
10324
|
+
title: `${p.serverName} asks you to open ${url.host}`,
|
|
10325
|
+
...p.message.trim() ? { reason: p.message.trim() } : {},
|
|
10326
|
+
url: url.href,
|
|
10327
|
+
options: ["allow_once", "deny"]
|
|
10328
|
+
});
|
|
10329
|
+
const decision = decisionOf(response);
|
|
10330
|
+
if (decision === "allow_once" || decision === "allow_session") return accept(null);
|
|
10331
|
+
return decision === "deny" ? DECLINED : CANCELLED;
|
|
10332
|
+
}
|
|
10333
|
+
/** Elicitations carry no item id, and app-server request ids restart with each process. */
|
|
10334
|
+
const newRequestId = () => `mcp-${randomUUID()}`;
|
|
10335
|
+
function asMeta(value) {
|
|
10336
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
10337
|
+
}
|
|
10338
|
+
function text$1(value) {
|
|
10339
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
10340
|
+
}
|
|
10341
|
+
/** Web links only: a card never links to a script or a local file. */
|
|
10342
|
+
function webUrl(raw) {
|
|
10343
|
+
try {
|
|
10344
|
+
const url = new URL(raw);
|
|
10345
|
+
return url.protocol === "https:" || url.protocol === "http:" ? url : void 0;
|
|
10346
|
+
} catch {
|
|
10347
|
+
return;
|
|
10348
|
+
}
|
|
10349
|
+
}
|
|
10350
|
+
// ../../packages/providers/src/codex/approvals.ts
|
|
9968
10351
|
/** The item with the classifier's verdict, for the user to decide. */
|
|
9969
10352
|
function withRisk(item, risk) {
|
|
9970
10353
|
if (!risk.risky) return item;
|
|
@@ -10099,11 +10482,7 @@ async function handleServerRequest(req, ac) {
|
|
|
10099
10482
|
if (response.kind === "answer") for (const [id, values] of Object.entries(response.answers)) answers[id] = { answers: values };
|
|
10100
10483
|
return { answers };
|
|
10101
10484
|
}
|
|
10102
|
-
case "mcpServer/elicitation/request": return
|
|
10103
|
-
action: "decline",
|
|
10104
|
-
content: null,
|
|
10105
|
-
_meta: null
|
|
10106
|
-
};
|
|
10485
|
+
case "mcpServer/elicitation/request": return handleElicitation(req.params, ac);
|
|
10107
10486
|
case "execCommandApproval":
|
|
10108
10487
|
case "applyPatchApproval": return { decision: { denied: { rejection: "Unsupported approval request" } } };
|
|
10109
10488
|
default: throw new CodexRpcError(-32601, `CodeDen does not handle ${req.method}`);
|
|
@@ -11115,10 +11494,16 @@ var CodexThreadMapper = class {
|
|
|
11115
11494
|
});
|
|
11116
11495
|
}
|
|
11117
11496
|
}
|
|
11497
|
+
/**
|
|
11498
|
+
* Codex ended a turn. On the session's own thread the turn's id is its fork point:
|
|
11499
|
+
* `thread/fork { lastTurnId }` keeps it and drops later turns. Turns CodeDen closes itself
|
|
11500
|
+
* (the process died, the session closed) report none, as Codex may not have finished them.
|
|
11501
|
+
*/
|
|
11118
11502
|
completeTurn(turn) {
|
|
11119
11503
|
this.beginTurn(turn.id);
|
|
11120
|
-
|
|
11121
|
-
|
|
11504
|
+
const forkPoint = this.o.root ? turn.id : void 0;
|
|
11505
|
+
if (turn.status === "completed") this.endTurn({ type: "completed" }, turn.id, forkPoint);
|
|
11506
|
+
else if (turn.status === "interrupted") this.endTurn({ type: "interrupted" }, turn.id, forkPoint);
|
|
11122
11507
|
else {
|
|
11123
11508
|
const info = turn.error?.codexErrorInfo;
|
|
11124
11509
|
const kind = classifyCodexError(info);
|
|
@@ -11130,13 +11515,17 @@ var CodexThreadMapper = class {
|
|
|
11130
11515
|
code: codexErrorCode(info),
|
|
11131
11516
|
kind,
|
|
11132
11517
|
retryAt
|
|
11133
|
-
}, turn.id);
|
|
11518
|
+
}, turn.id, forkPoint);
|
|
11134
11519
|
}
|
|
11135
11520
|
}
|
|
11136
11521
|
/** Close the running turn (also used when the process dies or the session closes). */
|
|
11137
|
-
endTurn(outcome, turnId = this.current) {
|
|
11522
|
+
endTurn(outcome, turnId = this.current, forkPoint) {
|
|
11138
11523
|
if (!turnId || turnId !== this.current) return;
|
|
11139
11524
|
const ctx = this.ctx(turnId);
|
|
11525
|
+
const end = {
|
|
11526
|
+
...ctx,
|
|
11527
|
+
...forkPoint ? { forkPoint } : {}
|
|
11528
|
+
};
|
|
11140
11529
|
const ok = outcome.type === "completed";
|
|
11141
11530
|
for (const item of this.open.values()) this.o.emit({
|
|
11142
11531
|
type: "item.completed",
|
|
@@ -11163,17 +11552,17 @@ var CodexThreadMapper = class {
|
|
|
11163
11552
|
this.current = void 0;
|
|
11164
11553
|
if (outcome.type === "completed") this.o.emit({
|
|
11165
11554
|
type: "turn.completed",
|
|
11166
|
-
...
|
|
11555
|
+
...end,
|
|
11167
11556
|
stopReason: "end_turn"
|
|
11168
11557
|
});
|
|
11169
11558
|
else if (outcome.type === "interrupted") this.o.emit({
|
|
11170
11559
|
type: "turn.interrupted",
|
|
11171
|
-
...
|
|
11560
|
+
...end,
|
|
11172
11561
|
reason: "user"
|
|
11173
11562
|
});
|
|
11174
11563
|
else this.o.emit({
|
|
11175
11564
|
type: "turn.failed",
|
|
11176
|
-
...
|
|
11565
|
+
...end,
|
|
11177
11566
|
error: {
|
|
11178
11567
|
code: outcome.code ?? "turn_failed",
|
|
11179
11568
|
message: outcome.message,
|
|
@@ -12054,7 +12443,10 @@ var CodexAdapter = class {
|
|
|
12054
12443
|
}
|
|
12055
12444
|
return session;
|
|
12056
12445
|
}
|
|
12057
|
-
/**
|
|
12446
|
+
/**
|
|
12447
|
+
* Copy a thread with `thread/fork` (see `forkCodexThread`); returns the new thread's id. With
|
|
12448
|
+
* `upTo` (a turn id), the copy ends with that turn.
|
|
12449
|
+
*/
|
|
12058
12450
|
async forkConversation(resumeToken, opts) {
|
|
12059
12451
|
const executable = this.executable();
|
|
12060
12452
|
if (!executable) throw new Error("Codex is not installed (no `codex` on PATH)");
|
|
@@ -12316,6 +12708,7 @@ var FakeSession = class {
|
|
|
12316
12708
|
}
|
|
12317
12709
|
async runTurn(turn, input) {
|
|
12318
12710
|
const { threadId, turnId } = turn;
|
|
12711
|
+
const forkPoint = turnId;
|
|
12319
12712
|
const prompt = input.text;
|
|
12320
12713
|
try {
|
|
12321
12714
|
this.emit({
|
|
@@ -12342,6 +12735,7 @@ var FakeSession = class {
|
|
|
12342
12735
|
type: "turn.failed",
|
|
12343
12736
|
threadId,
|
|
12344
12737
|
turnId,
|
|
12738
|
+
forkPoint,
|
|
12345
12739
|
error: {
|
|
12346
12740
|
code: "fake_failure",
|
|
12347
12741
|
message: "Scripted failure",
|
|
@@ -12357,6 +12751,7 @@ var FakeSession = class {
|
|
|
12357
12751
|
type: "turn.failed",
|
|
12358
12752
|
threadId,
|
|
12359
12753
|
turnId,
|
|
12754
|
+
forkPoint,
|
|
12360
12755
|
error: usage ? {
|
|
12361
12756
|
code: "usage_limit",
|
|
12362
12757
|
message: "Scripted usage limit",
|
|
@@ -12378,6 +12773,7 @@ var FakeSession = class {
|
|
|
12378
12773
|
type: "turn.completed",
|
|
12379
12774
|
threadId,
|
|
12380
12775
|
turnId,
|
|
12776
|
+
forkPoint,
|
|
12381
12777
|
stopReason: "end_turn"
|
|
12382
12778
|
});
|
|
12383
12779
|
return;
|
|
@@ -12426,6 +12822,7 @@ var FakeSession = class {
|
|
|
12426
12822
|
type: "turn.completed",
|
|
12427
12823
|
threadId,
|
|
12428
12824
|
turnId,
|
|
12825
|
+
forkPoint,
|
|
12429
12826
|
stopReason: "end_turn"
|
|
12430
12827
|
});
|
|
12431
12828
|
} catch (error) {
|
|
@@ -12434,6 +12831,7 @@ var FakeSession = class {
|
|
|
12434
12831
|
type: "turn.interrupted",
|
|
12435
12832
|
threadId,
|
|
12436
12833
|
turnId,
|
|
12834
|
+
forkPoint,
|
|
12437
12835
|
reason: "user"
|
|
12438
12836
|
});
|
|
12439
12837
|
return;
|
|
@@ -12443,6 +12841,7 @@ var FakeSession = class {
|
|
|
12443
12841
|
type: "turn.failed",
|
|
12444
12842
|
threadId,
|
|
12445
12843
|
turnId,
|
|
12844
|
+
forkPoint,
|
|
12446
12845
|
error: { message }
|
|
12447
12846
|
});
|
|
12448
12847
|
}
|
|
@@ -12843,10 +13242,13 @@ var FakeProvider = class {
|
|
|
12843
13242
|
async openSession(opts) {
|
|
12844
13243
|
return new FakeSession(await withFork(this, opts), this.options);
|
|
12845
13244
|
}
|
|
12846
|
-
/**
|
|
12847
|
-
|
|
13245
|
+
/**
|
|
13246
|
+
* A fork gets a conversation of its own, named after the one it copies and, with `upTo`,
|
|
13247
|
+
* the turn it ends with (`<token>:fork-<n>@<turn>`).
|
|
13248
|
+
*/
|
|
13249
|
+
async forkConversation(resumeToken, opts) {
|
|
12848
13250
|
this.forks += 1;
|
|
12849
|
-
return `${resumeToken}:fork-${this.forks}`;
|
|
13251
|
+
return `${resumeToken}:fork-${this.forks}${opts?.upTo ? `@${opts.upTo}` : ""}`;
|
|
12850
13252
|
}
|
|
12851
13253
|
forks = 0;
|
|
12852
13254
|
};
|
|
@@ -21858,9 +22260,12 @@ async function executeCommand(core, command) {
|
|
|
21858
22260
|
const adapter = registry.get(command.providerId);
|
|
21859
22261
|
if (!adapter?.capabilities().fork || !adapter.forkConversation) throw new CommandError("unsupported", `Provider ${command.providerId} cannot fork`);
|
|
21860
22262
|
try {
|
|
21861
|
-
return { resumeToken: await adapter.forkConversation(command.resumeToken, {
|
|
22263
|
+
return { resumeToken: await adapter.forkConversation(command.resumeToken, {
|
|
22264
|
+
cwd: command.cwd,
|
|
22265
|
+
...command.upTo ? { upTo: command.upTo } : {}
|
|
22266
|
+
}) };
|
|
21862
22267
|
} catch (error) {
|
|
21863
|
-
throw new CommandError("fork_failed", error.message);
|
|
22268
|
+
throw new CommandError(error.code === FORK_POINT_MISSING ? FORK_POINT_MISSING : "fork_failed", error.message);
|
|
21864
22269
|
}
|
|
21865
22270
|
}
|
|
21866
22271
|
case "workspace.prepare":
|
|
@@ -21943,7 +22348,7 @@ async function executeCommand(core, command) {
|
|
|
21943
22348
|
case "checkpoint.restoreFiles": return restoreFiles(command.cwd, command.sessionId, command.index, command.paths);
|
|
21944
22349
|
case "ports.list": return { ports: await listPorts(command.probe) };
|
|
21945
22350
|
case "mcp.test": {
|
|
21946
|
-
const { testMcpServer } = await import("./server-test-
|
|
22351
|
+
const { testMcpServer } = await import("./server-test-b0hHPqXb.js");
|
|
21947
22352
|
return testMcpServer({
|
|
21948
22353
|
spec: command.spec,
|
|
21949
22354
|
cwd: command.cwd,
|
|
@@ -22004,21 +22409,44 @@ function removeOwnLock(lockFile) {
|
|
|
22004
22409
|
* monotonically increasing seq and kept until the server acks them, so nothing is lost
|
|
22005
22410
|
* while the server is away. `state.json` holds the stable runner id and seq counters.
|
|
22006
22411
|
*
|
|
22007
|
-
*
|
|
22412
|
+
* An ack only moves the acked position in memory. While acks flow, the journal persists at
|
|
22413
|
+
* most every `persistMs`: it writes `state.json`, then, when enough acked events have piled
|
|
22414
|
+
* up and they outweigh what is left, rewrites the file without them (compaction), so each
|
|
22415
|
+
* ack costs no more than its append did. State always goes first, so `lastSeq` outlives the
|
|
22416
|
+
* events a compaction drops. `close` persists and compacts at once. After a crash the journal
|
|
22417
|
+
* may still hold events the server already acked; the server's `resume` acks them again and
|
|
22418
|
+
* they are not replayed.
|
|
22008
22419
|
*/
|
|
22009
22420
|
var Journal = class {
|
|
22010
22421
|
file;
|
|
22011
22422
|
stateFile;
|
|
22423
|
+
compactEntries;
|
|
22424
|
+
compactBytes;
|
|
22425
|
+
persistMs;
|
|
22012
22426
|
state;
|
|
22427
|
+
/** Every event in `journal.jsonl`, oldest first; those before `head` are acked. */
|
|
22013
22428
|
entries = [];
|
|
22014
|
-
|
|
22429
|
+
head = 0;
|
|
22430
|
+
fileBytes = 0;
|
|
22431
|
+
ackedBytes = 0;
|
|
22432
|
+
persistTimer;
|
|
22433
|
+
constructor(dir, opts = {}) {
|
|
22015
22434
|
mkdirSync(dir, { recursive: true });
|
|
22016
22435
|
this.file = join(dir, "journal.jsonl");
|
|
22017
22436
|
this.stateFile = join(dir, "state.json");
|
|
22437
|
+
this.compactEntries = opts.compactEntries ?? 1e3;
|
|
22438
|
+
this.compactBytes = opts.compactBytes ?? 1048576;
|
|
22439
|
+
this.persistMs = opts.persistMs ?? 250;
|
|
22018
22440
|
this.state = this.loadState();
|
|
22019
|
-
|
|
22020
|
-
|
|
22021
|
-
this.
|
|
22441
|
+
const { entries, torn } = this.loadEntries();
|
|
22442
|
+
this.entries = entries;
|
|
22443
|
+
for (const e of entries) this.fileBytes += e.bytes;
|
|
22444
|
+
this.state.lastSeq = Math.max(this.state.lastSeq, entries.at(-1)?.msg.seq ?? 0);
|
|
22445
|
+
this.advanceHead(this.state.ackedSeq);
|
|
22446
|
+
if (torn) {
|
|
22447
|
+
this.writeState(this.state);
|
|
22448
|
+
this.compact();
|
|
22449
|
+
}
|
|
22022
22450
|
}
|
|
22023
22451
|
get runnerId() {
|
|
22024
22452
|
return this.state.runnerId;
|
|
@@ -22027,7 +22455,7 @@ var Journal = class {
|
|
|
22027
22455
|
return this.state.lastSeq;
|
|
22028
22456
|
}
|
|
22029
22457
|
get unacked() {
|
|
22030
|
-
return this.entries.length;
|
|
22458
|
+
return this.entries.length - this.head;
|
|
22031
22459
|
}
|
|
22032
22460
|
/** Assign the next seq, persist the event, and return the wire message. */
|
|
22033
22461
|
append(event) {
|
|
@@ -22038,20 +22466,84 @@ var Journal = class {
|
|
|
22038
22466
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
22039
22467
|
event
|
|
22040
22468
|
};
|
|
22041
|
-
|
|
22042
|
-
this.
|
|
22469
|
+
const line = `${JSON.stringify(msg)}\n`;
|
|
22470
|
+
appendFileSync(this.file, line);
|
|
22471
|
+
const bytes = Buffer.byteLength(line);
|
|
22472
|
+
this.entries.push({
|
|
22473
|
+
msg,
|
|
22474
|
+
bytes
|
|
22475
|
+
});
|
|
22476
|
+
this.fileBytes += bytes;
|
|
22043
22477
|
return msg;
|
|
22044
22478
|
}
|
|
22045
|
-
/**
|
|
22479
|
+
/** Acknowledge everything with `seq <= seq` (cumulative ack). */
|
|
22046
22480
|
ack(seq) {
|
|
22047
|
-
|
|
22048
|
-
|
|
22049
|
-
this.
|
|
22050
|
-
this.
|
|
22481
|
+
const upTo = Math.min(seq, this.state.lastSeq);
|
|
22482
|
+
if (upTo <= this.state.ackedSeq) return;
|
|
22483
|
+
this.state.ackedSeq = upTo;
|
|
22484
|
+
this.advanceHead(upTo);
|
|
22485
|
+
this.schedulePersist();
|
|
22051
22486
|
}
|
|
22052
22487
|
/** Unacked events after `seq`, oldest first (for replay on resume). */
|
|
22053
22488
|
after(seq) {
|
|
22054
|
-
|
|
22489
|
+
const out = [];
|
|
22490
|
+
for (let i = this.head; i < this.entries.length; i++) {
|
|
22491
|
+
const msg = this.entries[i]?.msg;
|
|
22492
|
+
if (msg && msg.seq > seq) out.push(msg);
|
|
22493
|
+
}
|
|
22494
|
+
return out;
|
|
22495
|
+
}
|
|
22496
|
+
/** Persist the acked position and drop every acked event from the file (runner shutdown). */
|
|
22497
|
+
close() {
|
|
22498
|
+
this.persist(true);
|
|
22499
|
+
}
|
|
22500
|
+
advanceHead(ackedSeq) {
|
|
22501
|
+
for (;;) {
|
|
22502
|
+
const entry = this.entries[this.head];
|
|
22503
|
+
if (!entry || entry.msg.seq > ackedSeq) return;
|
|
22504
|
+
this.ackedBytes += entry.bytes;
|
|
22505
|
+
this.head += 1;
|
|
22506
|
+
}
|
|
22507
|
+
}
|
|
22508
|
+
/** Enough acked events piled up, and rewriting what is left costs no more than they do. */
|
|
22509
|
+
compactDue() {
|
|
22510
|
+
return (this.head >= this.compactEntries || this.ackedBytes >= this.compactBytes) && this.ackedBytes >= this.fileBytes - this.ackedBytes;
|
|
22511
|
+
}
|
|
22512
|
+
schedulePersist() {
|
|
22513
|
+
if (this.persistTimer) return;
|
|
22514
|
+
this.persistTimer = setTimeout(() => {
|
|
22515
|
+
this.persistTimer = void 0;
|
|
22516
|
+
try {
|
|
22517
|
+
this.persist(false);
|
|
22518
|
+
} catch {}
|
|
22519
|
+
}, this.persistMs);
|
|
22520
|
+
this.persistTimer.unref();
|
|
22521
|
+
}
|
|
22522
|
+
/**
|
|
22523
|
+
* Write the acked position, then compact when it is due (`all`: whenever anything is acked).
|
|
22524
|
+
* State first, so `lastSeq` outlives the events the compaction drops.
|
|
22525
|
+
*/
|
|
22526
|
+
persist(all) {
|
|
22527
|
+
if (this.persistTimer) clearTimeout(this.persistTimer);
|
|
22528
|
+
this.persistTimer = void 0;
|
|
22529
|
+
this.writeState(this.state);
|
|
22530
|
+
if (all ? this.head > 0 : this.compactDue()) this.compact();
|
|
22531
|
+
}
|
|
22532
|
+
/** Rewrite the file without its acked events. Callers write the state first. */
|
|
22533
|
+
compact() {
|
|
22534
|
+
const live = this.entries.slice(this.head);
|
|
22535
|
+
const tmp = `${this.file}.tmp`;
|
|
22536
|
+
writeFileSync(tmp, live.map((e) => `${JSON.stringify(e.msg)}\n`).join(""));
|
|
22537
|
+
renameSync(tmp, this.file);
|
|
22538
|
+
this.entries = live;
|
|
22539
|
+
this.head = 0;
|
|
22540
|
+
this.fileBytes -= this.ackedBytes;
|
|
22541
|
+
this.ackedBytes = 0;
|
|
22542
|
+
}
|
|
22543
|
+
writeState(state) {
|
|
22544
|
+
const tmp = `${this.stateFile}.tmp`;
|
|
22545
|
+
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
|
|
22546
|
+
renameSync(tmp, this.stateFile);
|
|
22055
22547
|
}
|
|
22056
22548
|
loadState() {
|
|
22057
22549
|
try {
|
|
@@ -22071,25 +22563,26 @@ var Journal = class {
|
|
|
22071
22563
|
return fresh;
|
|
22072
22564
|
}
|
|
22073
22565
|
loadEntries() {
|
|
22074
|
-
if (!existsSync(this.file)) return
|
|
22075
|
-
|
|
22076
|
-
|
|
22566
|
+
if (!existsSync(this.file)) return {
|
|
22567
|
+
entries: [],
|
|
22568
|
+
torn: false
|
|
22569
|
+
};
|
|
22570
|
+
const raw = readFileSync(this.file, "utf8");
|
|
22571
|
+
const entries = [];
|
|
22572
|
+
let torn = raw.length > 0 && !raw.endsWith("\n");
|
|
22573
|
+
for (const line of raw.split("\n")) {
|
|
22077
22574
|
if (!line.trim()) continue;
|
|
22078
22575
|
const parsed = RunnerEventMessage.safeParse(safeJson(line));
|
|
22079
|
-
if (parsed.success)
|
|
22576
|
+
if (parsed.success) entries.push({
|
|
22577
|
+
msg: parsed.data,
|
|
22578
|
+
bytes: Buffer.byteLength(line) + 1
|
|
22579
|
+
});
|
|
22580
|
+
else torn = true;
|
|
22080
22581
|
}
|
|
22081
|
-
return
|
|
22082
|
-
|
|
22083
|
-
|
|
22084
|
-
|
|
22085
|
-
writeFileSync(tmp, this.entries.map((e) => `${JSON.stringify(e)}\n`).join(""));
|
|
22086
|
-
renameSync(tmp, this.file);
|
|
22087
|
-
this.writeState(this.state);
|
|
22088
|
-
}
|
|
22089
|
-
writeState(state) {
|
|
22090
|
-
const tmp = `${this.stateFile}.tmp`;
|
|
22091
|
-
writeFileSync(tmp, `${JSON.stringify(state, null, 2)}\n`);
|
|
22092
|
-
renameSync(tmp, this.stateFile);
|
|
22582
|
+
return {
|
|
22583
|
+
entries: entries.sort((a, b) => a.msg.seq - b.msg.seq),
|
|
22584
|
+
torn
|
|
22585
|
+
};
|
|
22093
22586
|
}
|
|
22094
22587
|
};
|
|
22095
22588
|
function safeJson(line) {
|
|
@@ -24047,6 +24540,68 @@ function connectionHandlers(core, register, unregister, onAttach) {
|
|
|
24047
24540
|
}
|
|
24048
24541
|
};
|
|
24049
24542
|
}
|
|
24543
|
+
/**
|
|
24544
|
+
* Batches streamed `item.delta` events on their way to the journal. Every event the runner
|
|
24545
|
+
* journals is also a send, a server transaction and an ack, and providers stream an answer a
|
|
24546
|
+
* few characters at a time. Per session, consecutive deltas that extend the same item the
|
|
24547
|
+
* same way (`mergeItemDeltas`) are merged and passed on as one: after `windowMs`, once the
|
|
24548
|
+
* batch holds `maxChars`, or right before any other event of that session, whoever emits it
|
|
24549
|
+
* (the session's provider, a checkpoint command, workspace setup). An event that belongs to
|
|
24550
|
+
* no session (a terminal exiting, a drain) passes every batch on first. So no event overtakes
|
|
24551
|
+
* a delta that came before it, and only deltas ever wait.
|
|
24552
|
+
*/
|
|
24553
|
+
var DeltaBatcher = class {
|
|
24554
|
+
out;
|
|
24555
|
+
opts;
|
|
24556
|
+
batches = /* @__PURE__ */ new Map();
|
|
24557
|
+
constructor(out, opts = {}) {
|
|
24558
|
+
this.out = out;
|
|
24559
|
+
this.opts = opts;
|
|
24560
|
+
}
|
|
24561
|
+
push(event) {
|
|
24562
|
+
if (event.kind === "agent" && event.event.type === "item.delta") {
|
|
24563
|
+
this.add(event.sessionId, event.event);
|
|
24564
|
+
return;
|
|
24565
|
+
}
|
|
24566
|
+
this.flush("sessionId" in event ? event.sessionId : void 0);
|
|
24567
|
+
this.out(event);
|
|
24568
|
+
}
|
|
24569
|
+
/** Pass on the pending batch of one session, or of every session. */
|
|
24570
|
+
flush(sessionId) {
|
|
24571
|
+
if (sessionId === void 0) {
|
|
24572
|
+
for (const id of [...this.batches.keys()]) this.flush(id);
|
|
24573
|
+
return;
|
|
24574
|
+
}
|
|
24575
|
+
const batch = this.batches.get(sessionId);
|
|
24576
|
+
if (!batch) return;
|
|
24577
|
+
clearTimeout(batch.timer);
|
|
24578
|
+
this.batches.delete(sessionId);
|
|
24579
|
+
this.out({
|
|
24580
|
+
kind: "agent",
|
|
24581
|
+
sessionId,
|
|
24582
|
+
event: batch.delta
|
|
24583
|
+
});
|
|
24584
|
+
}
|
|
24585
|
+
add(sessionId, delta) {
|
|
24586
|
+
const chars = itemDeltaText(delta.delta).length;
|
|
24587
|
+
let batch = this.batches.get(sessionId);
|
|
24588
|
+
const merged = batch && mergeItemDeltas(batch.delta, delta);
|
|
24589
|
+
if (batch && merged) {
|
|
24590
|
+
batch.delta = merged;
|
|
24591
|
+
batch.chars += chars;
|
|
24592
|
+
} else {
|
|
24593
|
+
this.flush(sessionId);
|
|
24594
|
+
const windowMs = this.opts.windowMs ?? 50;
|
|
24595
|
+
batch = {
|
|
24596
|
+
delta,
|
|
24597
|
+
chars,
|
|
24598
|
+
timer: setTimeout(() => this.flush(sessionId), windowMs)
|
|
24599
|
+
};
|
|
24600
|
+
this.batches.set(sessionId, batch);
|
|
24601
|
+
}
|
|
24602
|
+
if (batch.chars >= (this.opts.maxChars ?? 16384)) this.flush(sessionId);
|
|
24603
|
+
}
|
|
24604
|
+
};
|
|
24050
24605
|
// ../runner/src/session-host.ts
|
|
24051
24606
|
/**
|
|
24052
24607
|
* Owns live provider sessions on this runner and pumps their normalized events into the
|
|
@@ -24346,11 +24901,15 @@ async function startRunner(opts) {
|
|
|
24346
24901
|
let mcpUpstream = opts.mcpUpstreamUrl ? new HttpMcpUpstream(opts.mcpUpstreamUrl) : void 0;
|
|
24347
24902
|
let port = 0;
|
|
24348
24903
|
let stopped = false;
|
|
24349
|
-
const
|
|
24904
|
+
const journalEvent = (event) => {
|
|
24350
24905
|
if (stopped) return;
|
|
24351
24906
|
const msg = journal.append(event);
|
|
24352
24907
|
attached?.deliver(msg);
|
|
24353
24908
|
};
|
|
24909
|
+
const deltas = new DeltaBatcher(journalEvent, opts.deltaBatching);
|
|
24910
|
+
const emit = (event) => {
|
|
24911
|
+
if (!stopped) deltas.push(event);
|
|
24912
|
+
};
|
|
24354
24913
|
const stream = (data) => {
|
|
24355
24914
|
attached?.deliver({
|
|
24356
24915
|
type: "stream",
|
|
@@ -24481,7 +25040,13 @@ async function startRunner(opts) {
|
|
|
24481
25040
|
if ("closeAllConnections" in server) server.closeAllConnections();
|
|
24482
25041
|
});
|
|
24483
25042
|
process.off("exit", removeLock);
|
|
25043
|
+
deltas.flush();
|
|
24484
25044
|
stopped = true;
|
|
25045
|
+
try {
|
|
25046
|
+
journal.close();
|
|
25047
|
+
} catch (error) {
|
|
25048
|
+
log(`journal: ${String(error)}`);
|
|
25049
|
+
}
|
|
24485
25050
|
log("runner stopped");
|
|
24486
25051
|
})();
|
|
24487
25052
|
return closing;
|
|
@@ -24496,4 +25061,4 @@ async function startRunner(opts) {
|
|
|
24496
25061
|
}
|
|
24497
25062
|
export { isElectronRuntime as A, installClaudeSdk as C, acquireInstallLock as D, resolveNpm as E, import_websocket_server as M, wrapper_default as N, brokenInstallMessage as O, importClaudeSdkFrom as S, runNpm as T, claudeMemoryDir as _, bufferToFormData as a, claudeSdkSource as b, refreshProcessPath as c, ImageEngineRegistry as d, createImageEngineRegistry as f, ImageGenerationError as g, resolveImageOutput as h, tryDecodeURIComponent as i, ownNodeEnv as j, findExecutable as k, defaultShell as l, imageInfo as m, readLock as n, createAdaptorServer as o, IMAGE_ASPECT_RATIOS as p, Hono as r, upgradeWebSocket as s, startRunner as t, shellCommandLine as u, findGitRoot as v, isClaudeSdkInstalled as w, CLAUDE_SDK_VERSION as x, mainRepoRoot as y };
|
|
24498
25063
|
|
|
24499
|
-
//# sourceMappingURL=runner-
|
|
25064
|
+
//# sourceMappingURL=runner-CiaiUWOE.js.map
|