@cabane/companion 0.6.169 → 0.6.171
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 +368 -161
- package/dist/runtime.js +339 -147
- package/package.json +1 -1
package/dist/runtime.js
CHANGED
|
@@ -3056,9 +3056,15 @@ var resolvedMcpServerSchema = z8.union([
|
|
|
3056
3056
|
var resolvedMcpServersSchema = z8.record(z8.string(), resolvedMcpServerSchema);
|
|
3057
3057
|
var hostInjectedServersSchema = z8.record(z8.string(), z8.unknown());
|
|
3058
3058
|
var turnRequestSchema = z8.object({
|
|
3059
|
-
// Server-composed
|
|
3060
|
-
// charter)
|
|
3059
|
+
// Server-composed standing instructions: the core segment (body, tool naming, identity, agent
|
|
3060
|
+
// charter) and, after it, the conversation segment (workspace and room charters, skill indexes,
|
|
3061
|
+
// roster). One string to the adapter, current as of this dispatch.
|
|
3061
3062
|
systemPrompt: z8.string(),
|
|
3063
|
+
// The same text as its two ordered segments, so an adapter that writes standing text into the
|
|
3064
|
+
// conversation (Codex's input preamble) can tell which half changed. `systemPrompt` is
|
|
3065
|
+
// `joinStanding(standing)`. Optional: absent from an API older than the split, where
|
|
3066
|
+
// `systemPrompt` is the core alone and the conversation material rides `prompt`.
|
|
3067
|
+
standing: z8.object({ core: z8.string(), conversation: z8.string() }).optional(),
|
|
3062
3068
|
// Server-composed per-turn user text (anchor reminder + the triggering message).
|
|
3063
3069
|
prompt: z8.string(),
|
|
3064
3070
|
// The multi-block user-message body (text + vision).
|
|
@@ -3106,6 +3112,16 @@ var turnRequestSchema = z8.object({
|
|
|
3106
3112
|
})
|
|
3107
3113
|
});
|
|
3108
3114
|
|
|
3115
|
+
// packages/agent-runtime/src/standing.ts
|
|
3116
|
+
var STANDING_SEPARATOR = "\n\n---\n\n";
|
|
3117
|
+
function joinStanding(segments) {
|
|
3118
|
+
const conversation = segments.conversation.trim();
|
|
3119
|
+
return conversation.length > 0 ? `${segments.core}${STANDING_SEPARATOR}${conversation}` : segments.core;
|
|
3120
|
+
}
|
|
3121
|
+
function standingSegments(req) {
|
|
3122
|
+
return req.standing ?? { core: req.systemPrompt, conversation: "" };
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3109
3125
|
// packages/agent-runtime/src/conformance.ts
|
|
3110
3126
|
import { z as z9 } from "zod";
|
|
3111
3127
|
var conformanceFixtureSchema = z9.object({
|
|
@@ -3463,8 +3479,15 @@ var TURN_CONTROL_TOOLS = [
|
|
|
3463
3479
|
"withdraw_ask"
|
|
3464
3480
|
];
|
|
3465
3481
|
var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
3466
|
-
ask: `Ask a
|
|
3467
|
-
|
|
3482
|
+
ask: `Ask a person a structured question you need answered to continue, then end your turn; don't wait for the reply. \`targetUserId\` is a person on your roster; to bring in an agent, name it with \`@handle\` in your reply.
|
|
3483
|
+
- Form: one question (\`headline\`, optional \`question\` body, \`options\`) or \`questions\`, 1\u2013${MAX_ASK_ITEMS} \`{ headline, body?, options? }\`; never both. Decisions that arise together go in ONE call, in the order you want them answered. A second \`ask\` is a separate item, never an edit; raise one only for a genuinely new question.
|
|
3484
|
+
- \`headline\`: one sentence ending in \`?\`. Body (\`question\` or each item's \`body\`): a sentence or two of framing, not a report; inline Markdown only, no lists or headings. \`options\`: ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers (always for a yes/no go-ahead), each a distinct course of action you can carry out without asking back.
|
|
3485
|
+
- Voice, in headline, body and options: "I" for yourself, never your own name; name everyone else who decides or acts, as on your roster. Never "you", "your", "we", "us", "our" or their contractions, nor "he", "she" or "they" for a named actor. Each option reads correctly alone. A quotation stays accurate: say who said it.
|
|
3486
|
+
- It must stand alone, since it can arrive first: the reply carries the findings, the ask the question; don't repeat the reply in it.
|
|
3487
|
+
- Recorded when you call this; delivered when your turn ends, or after 3 minutes if it is still running. Ask as soon as you know. The answer reaches you on a later turn.
|
|
3488
|
+
- The person sends one response; unanswered questions end as Unanswered. A person can dismiss an ask; you aren't told and nothing wakes you. Check an ask's status before raising it again.
|
|
3489
|
+
- To replace a stale ask (yours or anyone's here), pass \`replaces: { askId, reason }\`: both land in one write and the person sees the reason. To withdraw without a replacement, use \`withdraw_ask\`.`,
|
|
3490
|
+
cancel_wake: `Stand down your own wake \u2014 the inverse of \`wake_me\`. No arguments: it cancels the wake you have armed in this conversation, NOW, when you call it. Reach for it when the thing you armed a wake to check has already happened, or the work it was watching is over \u2014 an armed wake you no longer need fires into a turn with nothing to do, and a supervision loop with no off switch is one you can only end by leaving it running. Whether you have one armed is printed in your turn context ("Wake armed: \u2026"); a wake you armed that is no longer printed there has fired or been dismissed. Calling this with nothing armed is a clean no-op, no error. Each call acts as it arrives: \`cancel_wake\` then \`wake_me\` leaves the NEW wake armed; \`wake_me\` then \`cancel_wake\` leaves nothing armed. It reaches only your own wake in this conversation \u2014 never a peer's, and never a recurring schedule (those are \`schedules.*\`).`,
|
|
3468
3491
|
skip_turn: `End your current turn WITHOUT posting a reply. Call this when you've been woken to read a message that genuinely doesn't need a response from you \u2014 a thanks/aside, a question already answered, chatter outside your lane, or something another participant clearly owns. Your turn ends silently: no message bubble is posted. The \`reason\` is a short free-text note for telemetry (e.g. "already answered by cabane", "thanks, nothing to add"). Prefer this over posting a low-value "ok!"/"got it" reply. Don't also write a reply when you skip \u2014 skipping IS the whole turn.`,
|
|
3469
3492
|
wake_me: `Wake yourself later \u2014 end this turn now and be re-dispatched at a time you pick, with a note you write to yourself. Use it for "wait until X": when the thing you need hasn't happened yet (a PR isn't merged, a human hasn't answered), arm a wake, end your turn, and you're woken later to CHECK \u2014 read the workspace, and either act or re-arm. Ground the delay before you arm it. Almost every wake is short \u2014 seconds to a couple of hours \u2014 waiting on a condition you can name: a session limit resetting, a PR merging. Reach past a few hours only when (a) a human asked for that timing, or (b) the wait is pinned to a real external event you can name \u2014 a report that only runs Mondays, a known reset time. A speculative far-future check-in you invented yourself is the one thing not to arm: if no one asked and you can't name both what clears the wait and why it takes that long, don't arm it \u2014 finish now, or raise an \`ask\`. Pass EXACTLY ONE of \`afterSeconds\` (a relative delay \u2014 \`300\` for five minutes) or \`at\` (an absolute ISO-8601 timestamp WITH a zone, e.g. \`2026-07-16T09:00:00-07:00\` \u2014 YOU compute it; the system never parses natural-language time). \`note\` is a message to your future self \u2014 it becomes the body of the wake message that re-dispatches you, so write the condition to re-check ("check whether CT441 merged yet"). The wake arms NOW, when you call this, and the delay counts from this call; a later \`wake_me\` in the same turn replaces it, and \`cancel_wake\` stands it down. A wake that comes due while your turn is still running waits and is delivered after your reply. This is the ONLY sanctioned way to schedule your own continuation; never reach for a host cron/scheduler. Guardrails: at least 60s out, at most 14 days \u2014 a wake that can't be armed returns an error here, in this call, so you can recompute; widen the interval as a loop ages (5m \u2192 15m \u2192 1h\u2026) rather than hammering; after many consecutive re-arms with no other activity you'll be steered to raise an \`ask\` to the human instead.`,
|
|
3470
3493
|
withdraw_ask: `Withdraw an open ask \u2014 yours or anyone's in this conversation \u2014 because the question is moot, was wrong, or has been overtaken. \`reason\` is required and is shown to the person and, if it wasn't your ask, to the agent who asked it. If there is a current question to put in its place, don't call this \u2014 call \`ask\` with \`replaces\` so the two land as one and the person never sees neither or both. Never withdraw to hurry an answer; to act on a deadline, arm a \`wake_me\` and decide then.`
|
|
@@ -3472,13 +3495,13 @@ var TURN_CONTROL_TOOL_DESCRIPTIONS = {
|
|
|
3472
3495
|
var TURN_CONTROL_FIELD_DESCRIPTIONS = {
|
|
3473
3496
|
ask: {
|
|
3474
3497
|
targetUserId: "The workspace member (human) to ask \u2014 a user id, from your turn context's roster.",
|
|
3475
|
-
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline
|
|
3476
|
-
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the
|
|
3477
|
-
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must
|
|
3498
|
+
question: `SINGLE-question form: a short body \u2014 one or two sentences of framing the headline can't hold. NOT a report (up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only). Provide EITHER this or \`questions\`, not both. Put the crisp one-sentence question in \`headline\`.`,
|
|
3499
|
+
headline: `SINGLE-question form: the question itself as ONE clear, capitalized sentence ending in \`?\` ("Should Ledger deploy once Mia approves?"), up to ${MAX_ASK_HEADLINE_BYTES} characters. What the person reads first in For you and on the chip \u2014 one scannable question, no elaboration (that goes in \`question\`). Strongly encouraged.`,
|
|
3500
|
+
options: `SINGLE-question form: optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} suggested one-click answers, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
3478
3501
|
questions: `MULTI-question form: 1\u2013${MAX_ASK_ITEMS} questions to ask at once, when a plan ends with several bounded decisions. Provide EITHER this or \`question\`/\`headline\`/\`options\`, not both.`,
|
|
3479
|
-
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters
|
|
3480
|
-
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters. NOT a report
|
|
3481
|
-
"questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must
|
|
3502
|
+
"questions.headline": `The one-sentence question ("Should Ledger deploy once Mia approves?") \u2014 required for each item, up to ${MAX_ASK_HEADLINE_BYTES} characters.`,
|
|
3503
|
+
"questions.body": `Optional short framing beneath the headline, up to ${MAX_ASK_QUESTION_BYTES} characters, inline markdown only. NOT a report.`,
|
|
3504
|
+
"questions.options": `Optional ${MIN_ASK_OPTIONS}\u2013${MAX_ASK_OPTIONS} one-click answers for this question, up to ${MAX_ASK_OPTION_BYTES} characters each. Each option must make sense on its own.`,
|
|
3482
3505
|
replaces: "Withdraw a stale ask in this conversation as part of raising this one. Both land in a single write, so the person goes from the old question to the new one and never sees neither or both.",
|
|
3483
3506
|
"replaces.askId": "The id of the open ask to withdraw.",
|
|
3484
3507
|
"replaces.reason": `Why the old question is no longer the question \u2014 shown to the person on their Done row. Up to ${MAX_WITHDRAW_REASON_BYTES} characters.`
|
|
@@ -6114,7 +6137,14 @@ function fingerprintPrompt(prompt) {
|
|
|
6114
6137
|
return `${prompt.length}.${(fnv >>> 0).toString(36)}.${(djb >>> 0).toString(36)}`;
|
|
6115
6138
|
}
|
|
6116
6139
|
function encodeSession3(state) {
|
|
6117
|
-
return JSON.stringify(
|
|
6140
|
+
return JSON.stringify({
|
|
6141
|
+
threadId: state.threadId,
|
|
6142
|
+
cwd: state.cwd,
|
|
6143
|
+
promptFingerprint: state.promptFingerprint,
|
|
6144
|
+
conversationFingerprint: state.conversationFingerprint ?? null,
|
|
6145
|
+
compactions: state.compactions ?? null,
|
|
6146
|
+
usageBaseline: state.usageBaseline
|
|
6147
|
+
});
|
|
6118
6148
|
}
|
|
6119
6149
|
function decideResume3(stored, currentCwd) {
|
|
6120
6150
|
if (!stored) return { fresh: true, reason: "no_session" };
|
|
@@ -6135,6 +6165,8 @@ function decideResume3(stored, currentCwd) {
|
|
|
6135
6165
|
return {
|
|
6136
6166
|
resume: state.threadId,
|
|
6137
6167
|
promptFingerprint: typeof state.promptFingerprint === "string" ? state.promptFingerprint : null,
|
|
6168
|
+
conversationFingerprint: typeof state.conversationFingerprint === "string" ? state.conversationFingerprint : null,
|
|
6169
|
+
compactions: typeof state.compactions === "number" && Number.isInteger(state.compactions) && state.compactions >= 0 ? state.compactions : null,
|
|
6138
6170
|
usageBaseline: readUsageBaseline(state.usageBaseline)
|
|
6139
6171
|
};
|
|
6140
6172
|
}
|
|
@@ -6167,6 +6199,7 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6167
6199
|
let usage;
|
|
6168
6200
|
let cumulativeUsage;
|
|
6169
6201
|
let threadIdForSession = ctx.resumedThreadId;
|
|
6202
|
+
let lastState = ctx.storedState ?? null;
|
|
6170
6203
|
let settled = false;
|
|
6171
6204
|
function* flushInterim() {
|
|
6172
6205
|
const sealed = sealHeld2(held, false);
|
|
@@ -6182,14 +6215,14 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6182
6215
|
const state = encodeSession3({
|
|
6183
6216
|
threadId,
|
|
6184
6217
|
cwd: ctx.cwd ?? "",
|
|
6185
|
-
promptFingerprint: ctx.promptFingerprint ?? null,
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
// closing write records the new baseline.
|
|
6218
|
+
promptFingerprint: ctx.heldStanding?.promptFingerprint ?? null,
|
|
6219
|
+
conversationFingerprint: ctx.heldStanding?.conversationFingerprint ?? null,
|
|
6220
|
+
compactions: ctx.heldStanding?.compactions ?? null,
|
|
6189
6221
|
usageBaseline: ctx.usageBaseline ?? null
|
|
6190
6222
|
});
|
|
6191
6223
|
if (threadId !== ctx.resumedThreadId || state !== ctx.storedState) {
|
|
6192
6224
|
sessionEmitted = true;
|
|
6225
|
+
lastState = state;
|
|
6193
6226
|
yield {
|
|
6194
6227
|
type: "session",
|
|
6195
6228
|
state,
|
|
@@ -6288,14 +6321,16 @@ async function* decodeCodexStream(events, ctx) {
|
|
|
6288
6321
|
if (sealed) yield sealed;
|
|
6289
6322
|
reason = "no_terminal";
|
|
6290
6323
|
}
|
|
6291
|
-
if (ok &&
|
|
6324
|
+
if (ok && threadIdForSession) {
|
|
6292
6325
|
const closing = encodeSession3({
|
|
6293
6326
|
threadId: threadIdForSession,
|
|
6294
6327
|
cwd: ctx.cwd ?? "",
|
|
6295
6328
|
promptFingerprint: ctx.promptFingerprint ?? null,
|
|
6296
|
-
|
|
6329
|
+
conversationFingerprint: ctx.conversationFingerprint ?? null,
|
|
6330
|
+
compactions: ctx.compactions ?? null,
|
|
6331
|
+
usageBaseline: cumulativeUsage ?? ctx.usageBaseline ?? null
|
|
6297
6332
|
});
|
|
6298
|
-
yield { type: "session", state: closing };
|
|
6333
|
+
if (closing !== lastState) yield { type: "session", state: closing };
|
|
6299
6334
|
}
|
|
6300
6335
|
const resolvedConfig = ctx.resolvedReasoningEffort ? { reasoningEffort: ctx.resolvedReasoningEffort } : void 0;
|
|
6301
6336
|
yield {
|
|
@@ -6365,15 +6400,27 @@ var ENV_ENVELOPE_KEYS = [
|
|
|
6365
6400
|
"CABANE_CONVERSATION_TITLE",
|
|
6366
6401
|
"CABANE_HOST_ACCESS"
|
|
6367
6402
|
];
|
|
6368
|
-
function buildRunSpec2(req, resumeThreadId, instructionsFile = null,
|
|
6403
|
+
function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadStanding = null, imagePaths = []) {
|
|
6369
6404
|
const { policy, config } = req;
|
|
6370
6405
|
const directory = req.local.cwd ?? "";
|
|
6371
6406
|
const dialect = readCodexDialect(config.runtimeOptions);
|
|
6372
6407
|
const baseInstructionsFile = !policy.hostFs && instructionsFile ? instructionsFile : null;
|
|
6373
6408
|
const model = config.model ? parseCodexModel(config.model) : null;
|
|
6374
|
-
const
|
|
6375
|
-
const
|
|
6376
|
-
const
|
|
6409
|
+
const segments = standingSegments(req);
|
|
6410
|
+
const conversationText = segments.conversation.trim();
|
|
6411
|
+
const fingerprints = {
|
|
6412
|
+
core: fingerprintPrompt(segments.core),
|
|
6413
|
+
conversation: conversationText.length > 0 ? fingerprintPrompt(conversationText) : null
|
|
6414
|
+
};
|
|
6415
|
+
const resumed = resumeThreadId !== null;
|
|
6416
|
+
const held = resumed ? threadStanding : null;
|
|
6417
|
+
const viaInput = baseInstructionsFile === null;
|
|
6418
|
+
const preamble = {
|
|
6419
|
+
core: viaInput && (held?.core ?? null) !== fingerprints.core,
|
|
6420
|
+
// Compared both ways: a held segment that is now empty differs from it, and nothing held
|
|
6421
|
+
// equals nothing to send.
|
|
6422
|
+
conversation: viaInput && (held?.conversation ?? null) !== fingerprints.conversation
|
|
6423
|
+
};
|
|
6377
6424
|
return {
|
|
6378
6425
|
resumeThreadId,
|
|
6379
6426
|
directory,
|
|
@@ -6382,9 +6429,9 @@ function buildRunSpec2(req, resumeThreadId, instructionsFile = null, threadPromp
|
|
|
6382
6429
|
skipGitRepoCheck: true,
|
|
6383
6430
|
...dialect.modelReasoningEffort ? { modelReasoningEffort: dialect.modelReasoningEffort } : {},
|
|
6384
6431
|
baseInstructionsFile,
|
|
6385
|
-
input: withImages(buildInput(req,
|
|
6386
|
-
|
|
6387
|
-
|
|
6432
|
+
input: withImages(buildInput(req, segments, preamble, resumed), imagePaths),
|
|
6433
|
+
preamble,
|
|
6434
|
+
fingerprints,
|
|
6388
6435
|
config: buildConfig(req, baseInstructionsFile),
|
|
6389
6436
|
secretEnv: codexSecretEnv(req)
|
|
6390
6437
|
};
|
|
@@ -6400,12 +6447,26 @@ function codexSecretEnv(req) {
|
|
|
6400
6447
|
}
|
|
6401
6448
|
return Object.keys(env).length > 0 ? env : null;
|
|
6402
6449
|
}
|
|
6403
|
-
|
|
6450
|
+
var CORE_REPLACES = "Your standing instructions changed since an earlier copy in this conversation, or that copy may have been compacted away. This copy replaces every earlier one: where they differ, this one holds, and anything an earlier copy said that this one does not no longer applies.";
|
|
6451
|
+
var CONVERSATION_REPLACES = "This conversation's standing context \u2014 its charters, skill indexes and roster \u2014 changed since an earlier copy in this conversation, or that copy may have been compacted away. This copy replaces every earlier one, including charters and skills an earlier turn carried in its own text: where they differ, this one holds, and anything an earlier copy said that this one does not no longer applies.";
|
|
6452
|
+
function buildInput(req, segments, preamble, resumed) {
|
|
6404
6453
|
const userText = req.content.filter((b) => b.type === "text").map((b) => b.text).join("\n\n");
|
|
6405
6454
|
const body = userText.trim().length > 0 ? userText : req.prompt;
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6455
|
+
const standing = [];
|
|
6456
|
+
if (!resumed) {
|
|
6457
|
+
if (preamble.core) standing.push(joinStanding(segments).trim());
|
|
6458
|
+
} else {
|
|
6459
|
+
if (preamble.core) standing.push(`${CORE_REPLACES}
|
|
6460
|
+
|
|
6461
|
+
${segments.core.trim()}`);
|
|
6462
|
+
if (preamble.conversation) {
|
|
6463
|
+
standing.push(`${CONVERSATION_REPLACES}
|
|
6464
|
+
|
|
6465
|
+
${segments.conversation.trim()}`.trim());
|
|
6466
|
+
}
|
|
6467
|
+
}
|
|
6468
|
+
const lead = standing.filter((s) => s.length > 0);
|
|
6469
|
+
return lead.length > 0 ? `${lead.join("\n\n---\n\n")}
|
|
6409
6470
|
|
|
6410
6471
|
${body}` : body;
|
|
6411
6472
|
}
|
|
@@ -6617,7 +6678,6 @@ function createCodexAdapter(deps = {}) {
|
|
|
6617
6678
|
const directory = req.local.cwd ?? "";
|
|
6618
6679
|
const decision = decideResume3(req.session, directory);
|
|
6619
6680
|
const resumeThreadId = "resume" in decision ? decision.resume : null;
|
|
6620
|
-
const threadPromptFingerprint = "resume" in decision ? decision.promptFingerprint : null;
|
|
6621
6681
|
const threadUsageBaseline = "resume" in decision ? decision.usageBaseline : null;
|
|
6622
6682
|
if ("fresh" in decision && decision.reason === "cwd_mismatch") {
|
|
6623
6683
|
deps.onWarn?.(
|
|
@@ -6634,6 +6694,32 @@ function createCodexAdapter(deps = {}) {
|
|
|
6634
6694
|
degraded = false;
|
|
6635
6695
|
}
|
|
6636
6696
|
}
|
|
6697
|
+
let compactions = 0;
|
|
6698
|
+
let threadStanding = null;
|
|
6699
|
+
if ("resume" in decision) {
|
|
6700
|
+
compactions = deps.countCompactions ? await deps.countCompactions(decision.resume) : null;
|
|
6701
|
+
if (signal.aborted) return;
|
|
6702
|
+
if (compactions === null) {
|
|
6703
|
+
deps.onWarn?.(
|
|
6704
|
+
"codex adapter: the thread's compaction count could not be read \u2014 re-sending the standing instructions (ENG210)",
|
|
6705
|
+
{ threadId: decision.resume }
|
|
6706
|
+
);
|
|
6707
|
+
} else if (decision.compactions === compactions) {
|
|
6708
|
+
threadStanding = {
|
|
6709
|
+
core: decision.promptFingerprint,
|
|
6710
|
+
conversation: decision.conversationFingerprint
|
|
6711
|
+
};
|
|
6712
|
+
} else if (decision.compactions !== null && compactions > decision.compactions) {
|
|
6713
|
+
deps.onWarn?.(
|
|
6714
|
+
"codex adapter: the thread compacted since the last turn \u2014 re-sending the standing instructions (ENG210)",
|
|
6715
|
+
{
|
|
6716
|
+
threadId: decision.resume,
|
|
6717
|
+
compactions,
|
|
6718
|
+
compactionsSince: compactions - decision.compactions
|
|
6719
|
+
}
|
|
6720
|
+
);
|
|
6721
|
+
}
|
|
6722
|
+
}
|
|
6637
6723
|
let instructions = null;
|
|
6638
6724
|
if (!req.policy.hostFs) {
|
|
6639
6725
|
if (!deps.writeInstructionsFile) {
|
|
@@ -6678,7 +6764,7 @@ function createCodexAdapter(deps = {}) {
|
|
|
6678
6764
|
req,
|
|
6679
6765
|
resumeThreadId,
|
|
6680
6766
|
instructions?.path ?? null,
|
|
6681
|
-
|
|
6767
|
+
threadStanding,
|
|
6682
6768
|
images?.paths ?? []
|
|
6683
6769
|
);
|
|
6684
6770
|
if (spec.model === null) {
|
|
@@ -6693,10 +6779,18 @@ function createCodexAdapter(deps = {}) {
|
|
|
6693
6779
|
cwd: req.local.cwd,
|
|
6694
6780
|
resumedThreadId: resumeThreadId,
|
|
6695
6781
|
degraded,
|
|
6696
|
-
// Which
|
|
6697
|
-
// later copy is what the model reads.
|
|
6698
|
-
//
|
|
6699
|
-
promptFingerprint: spec.
|
|
6782
|
+
// Which segments the thread carries once this turn lands. A segment sent is that one —
|
|
6783
|
+
// the later copy is what the model reads. One withheld is what the thread had. Read off
|
|
6784
|
+
// the spec, so the record can't disagree with what was sent.
|
|
6785
|
+
promptFingerprint: spec.preamble.core ? spec.fingerprints.core : threadStanding?.core ?? null,
|
|
6786
|
+
conversationFingerprint: spec.preamble.conversation ? spec.fingerprints.conversation : threadStanding?.conversation ?? null,
|
|
6787
|
+
compactions,
|
|
6788
|
+
// What the thread held before this turn, for the write at `thread.started`.
|
|
6789
|
+
heldStanding: {
|
|
6790
|
+
promptFingerprint: threadStanding?.core ?? null,
|
|
6791
|
+
conversationFingerprint: threadStanding?.conversation ?? null,
|
|
6792
|
+
compactions: threadStanding ? compactions : null
|
|
6793
|
+
},
|
|
6700
6794
|
storedState: req.session,
|
|
6701
6795
|
usageBaseline: threadUsageBaseline,
|
|
6702
6796
|
// Codex reports no model in-stream, so record what the thread was started with (null on the
|
|
@@ -6796,11 +6890,21 @@ var OP_FAILURE = {
|
|
|
6796
6890
|
message: "files.read returned more than the result ceiling"
|
|
6797
6891
|
}
|
|
6798
6892
|
};
|
|
6799
|
-
var
|
|
6893
|
+
var startedSession = (threadId, cwd = DIR2, degraded = false) => ({
|
|
6800
6894
|
type: "session",
|
|
6801
|
-
state: encodeSession3({ threadId, cwd, promptFingerprint, usageBaseline }),
|
|
6895
|
+
state: encodeSession3({ threadId, cwd, promptFingerprint: null, usageBaseline: null }),
|
|
6802
6896
|
...degraded ? { degraded: true } : {}
|
|
6803
6897
|
});
|
|
6898
|
+
var closingSession = (threadId, usageBaseline = null) => ({
|
|
6899
|
+
type: "session",
|
|
6900
|
+
state: encodeSession3({
|
|
6901
|
+
threadId,
|
|
6902
|
+
cwd: DIR2,
|
|
6903
|
+
promptFingerprint: fingerprintPrompt(PROMPT),
|
|
6904
|
+
usageBaseline,
|
|
6905
|
+
compactions: 0
|
|
6906
|
+
})
|
|
6907
|
+
});
|
|
6804
6908
|
var CODEX_CONFORMANCE_FIXTURES = [
|
|
6805
6909
|
{
|
|
6806
6910
|
// A plain text reply: the held agent message seals as the terminal final at `turn.completed`;
|
|
@@ -6823,11 +6927,11 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6823
6927
|
})
|
|
6824
6928
|
],
|
|
6825
6929
|
expected: [
|
|
6826
|
-
|
|
6930
|
+
startedSession(NEW_THREAD_ID),
|
|
6827
6931
|
{ type: "text", body: "Hello there.", terminal: true },
|
|
6828
6932
|
// The closing session write — the thread's ending totals, recorded as the baseline the next turn
|
|
6829
6933
|
// subtracts. A fresh thread, so the `result` usage below is the frame verbatim.
|
|
6830
|
-
|
|
6934
|
+
closingSession(NEW_THREAD_ID, {
|
|
6831
6935
|
inputTokens: 100,
|
|
6832
6936
|
outputTokens: 50,
|
|
6833
6937
|
cacheReadTokens: 20,
|
|
@@ -6874,7 +6978,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6874
6978
|
turnCompleted()
|
|
6875
6979
|
],
|
|
6876
6980
|
expected: [
|
|
6877
|
-
|
|
6981
|
+
startedSession(NEW_THREAD_ID),
|
|
6878
6982
|
{ type: "text", body: "Reading your notes.", terminal: false },
|
|
6879
6983
|
{
|
|
6880
6984
|
type: "tool",
|
|
@@ -6894,6 +6998,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6894
6998
|
result: [{ type: "text", text: "sleep early" }]
|
|
6895
6999
|
},
|
|
6896
7000
|
{ type: "text", body: "Here's the summary.", terminal: true },
|
|
7001
|
+
closingSession(NEW_THREAD_ID),
|
|
6897
7002
|
{ type: "result", ok: true }
|
|
6898
7003
|
]
|
|
6899
7004
|
},
|
|
@@ -6927,7 +7032,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6927
7032
|
turnCompleted()
|
|
6928
7033
|
],
|
|
6929
7034
|
expected: [
|
|
6930
|
-
|
|
7035
|
+
startedSession(NEW_THREAD_ID),
|
|
6931
7036
|
{
|
|
6932
7037
|
type: "tool",
|
|
6933
7038
|
id: "query-success",
|
|
@@ -6945,6 +7050,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6945
7050
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6946
7051
|
result: OP_SUCCESS
|
|
6947
7052
|
},
|
|
7053
|
+
closingSession(NEW_THREAD_ID),
|
|
6948
7054
|
{ type: "result", ok: true }
|
|
6949
7055
|
]
|
|
6950
7056
|
},
|
|
@@ -6980,7 +7086,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6980
7086
|
turnCompleted()
|
|
6981
7087
|
],
|
|
6982
7088
|
expected: [
|
|
6983
|
-
|
|
7089
|
+
startedSession(NEW_THREAD_ID),
|
|
6984
7090
|
{
|
|
6985
7091
|
type: "tool",
|
|
6986
7092
|
id: "query-failed",
|
|
@@ -6998,6 +7104,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
6998
7104
|
input: { op: "files.read", args: { path: "/ABOUT.md" } },
|
|
6999
7105
|
result: OP_FAILURE
|
|
7000
7106
|
},
|
|
7107
|
+
closingSession(NEW_THREAD_ID),
|
|
7001
7108
|
{ type: "result", ok: true }
|
|
7002
7109
|
]
|
|
7003
7110
|
},
|
|
@@ -7021,7 +7128,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7021
7128
|
turnCompleted()
|
|
7022
7129
|
],
|
|
7023
7130
|
expected: [
|
|
7024
|
-
|
|
7131
|
+
startedSession(NEW_THREAD_ID),
|
|
7025
7132
|
{
|
|
7026
7133
|
type: "tool",
|
|
7027
7134
|
id: "mcp-failed",
|
|
@@ -7039,6 +7146,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7039
7146
|
input: { q: "receipt" },
|
|
7040
7147
|
result: "connection closed"
|
|
7041
7148
|
},
|
|
7149
|
+
closingSession(NEW_THREAD_ID),
|
|
7042
7150
|
{ type: "result", ok: true }
|
|
7043
7151
|
]
|
|
7044
7152
|
},
|
|
@@ -7064,7 +7172,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7064
7172
|
turnCompleted()
|
|
7065
7173
|
],
|
|
7066
7174
|
expected: [
|
|
7067
|
-
|
|
7175
|
+
startedSession(NEW_THREAD_ID),
|
|
7068
7176
|
{
|
|
7069
7177
|
type: "tool",
|
|
7070
7178
|
id: "mcp-image",
|
|
@@ -7082,6 +7190,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7082
7190
|
input: {},
|
|
7083
7191
|
result: [{ type: "image", data: "base64-bytes", mimeType: "image/png" }]
|
|
7084
7192
|
},
|
|
7193
|
+
closingSession(NEW_THREAD_ID),
|
|
7085
7194
|
{ type: "result", ok: true }
|
|
7086
7195
|
]
|
|
7087
7196
|
},
|
|
@@ -7122,7 +7231,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7122
7231
|
turnCompleted()
|
|
7123
7232
|
],
|
|
7124
7233
|
expected: [
|
|
7125
|
-
|
|
7234
|
+
startedSession(NEW_THREAD_ID),
|
|
7126
7235
|
{ type: "tool", id: "t1", name: "list", phase: "start", summary: "", input: {} },
|
|
7127
7236
|
{
|
|
7128
7237
|
type: "tool",
|
|
@@ -7133,6 +7242,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7133
7242
|
input: { path: "/product/tasks" },
|
|
7134
7243
|
result: [{ type: "text", text: "CT481.md" }]
|
|
7135
7244
|
},
|
|
7245
|
+
closingSession(NEW_THREAD_ID),
|
|
7136
7246
|
{ type: "result", ok: true }
|
|
7137
7247
|
]
|
|
7138
7248
|
},
|
|
@@ -7150,7 +7260,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7150
7260
|
turnCompleted()
|
|
7151
7261
|
],
|
|
7152
7262
|
expected: [
|
|
7153
|
-
|
|
7263
|
+
startedSession(NEW_THREAD_ID),
|
|
7154
7264
|
{
|
|
7155
7265
|
type: "tool",
|
|
7156
7266
|
id: "ws1",
|
|
@@ -7167,6 +7277,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7167
7277
|
summary: "best pizza in nyc",
|
|
7168
7278
|
input: { query: "best pizza in nyc" }
|
|
7169
7279
|
},
|
|
7280
|
+
closingSession(NEW_THREAD_ID),
|
|
7170
7281
|
{ type: "result", ok: true }
|
|
7171
7282
|
]
|
|
7172
7283
|
},
|
|
@@ -7182,7 +7293,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7182
7293
|
turnCompleted()
|
|
7183
7294
|
],
|
|
7184
7295
|
expected: [
|
|
7185
|
-
|
|
7296
|
+
startedSession(NEW_THREAD_ID),
|
|
7186
7297
|
{
|
|
7187
7298
|
type: "tool",
|
|
7188
7299
|
id: "ws2",
|
|
@@ -7199,6 +7310,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7199
7310
|
summary: "",
|
|
7200
7311
|
input: { query: "" }
|
|
7201
7312
|
},
|
|
7313
|
+
closingSession(NEW_THREAD_ID),
|
|
7202
7314
|
{ type: "result", ok: true }
|
|
7203
7315
|
]
|
|
7204
7316
|
},
|
|
@@ -7229,7 +7341,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7229
7341
|
turnCompleted()
|
|
7230
7342
|
],
|
|
7231
7343
|
expected: [
|
|
7232
|
-
|
|
7344
|
+
startedSession(NEW_THREAD_ID),
|
|
7233
7345
|
{ type: "text", body: "Working on it.", terminal: false },
|
|
7234
7346
|
{
|
|
7235
7347
|
type: "tool",
|
|
@@ -7261,7 +7373,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7261
7373
|
turnFailed("provider exploded")
|
|
7262
7374
|
],
|
|
7263
7375
|
expected: [
|
|
7264
|
-
|
|
7376
|
+
startedSession(NEW_THREAD_ID),
|
|
7265
7377
|
{ type: "text", body: "Partial work.", terminal: false },
|
|
7266
7378
|
{ type: "runtime_notice", body: "provider exploded" },
|
|
7267
7379
|
{ type: "result", ok: false, reason: "error:provider exploded" }
|
|
@@ -7278,7 +7390,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7278
7390
|
fatalError("stream died")
|
|
7279
7391
|
],
|
|
7280
7392
|
expected: [
|
|
7281
|
-
|
|
7393
|
+
startedSession(NEW_THREAD_ID),
|
|
7282
7394
|
{ type: "text", body: "Starting.", terminal: false },
|
|
7283
7395
|
{ type: "runtime_notice", body: "stream died" },
|
|
7284
7396
|
{ type: "result", ok: false, reason: "error:stream died" }
|
|
@@ -7291,7 +7403,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7291
7403
|
request: makeRequest3(),
|
|
7292
7404
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429 Too Many Requests")],
|
|
7293
7405
|
expected: [
|
|
7294
|
-
|
|
7406
|
+
startedSession(NEW_THREAD_ID),
|
|
7295
7407
|
{ type: "runtime_notice", body: "429 Too Many Requests" },
|
|
7296
7408
|
{ type: "result", ok: false, reason: "rate_limited" }
|
|
7297
7409
|
]
|
|
@@ -7301,7 +7413,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7301
7413
|
request: makeRequest3(),
|
|
7302
7414
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("503 Service Unavailable")],
|
|
7303
7415
|
expected: [
|
|
7304
|
-
|
|
7416
|
+
startedSession(NEW_THREAD_ID),
|
|
7305
7417
|
{ type: "runtime_notice", body: "503 Service Unavailable" },
|
|
7306
7418
|
{ type: "result", ok: false, reason: "server_error" }
|
|
7307
7419
|
]
|
|
@@ -7314,7 +7426,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7314
7426
|
turnFailed("You've hit your session limit \xB7 resets 8:20pm (UTC)")
|
|
7315
7427
|
],
|
|
7316
7428
|
expected: [
|
|
7317
|
-
|
|
7429
|
+
startedSession(NEW_THREAD_ID),
|
|
7318
7430
|
{
|
|
7319
7431
|
type: "runtime_notice",
|
|
7320
7432
|
body: "You've hit your session limit \xB7 resets 8:20pm (UTC)"
|
|
@@ -7327,7 +7439,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7327
7439
|
request: makeRequest3(),
|
|
7328
7440
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("401 Unauthorized: invalid api key")],
|
|
7329
7441
|
expected: [
|
|
7330
|
-
|
|
7442
|
+
startedSession(NEW_THREAD_ID),
|
|
7331
7443
|
{ type: "runtime_notice", body: "401 Unauthorized: invalid api key" },
|
|
7332
7444
|
{ type: "result", ok: false, reason: "auth_expired" }
|
|
7333
7445
|
]
|
|
@@ -7339,7 +7451,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7339
7451
|
request: makeRequest3(),
|
|
7340
7452
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("something local broke")],
|
|
7341
7453
|
expected: [
|
|
7342
|
-
|
|
7454
|
+
startedSession(NEW_THREAD_ID),
|
|
7343
7455
|
{ type: "runtime_notice", body: "something local broke" },
|
|
7344
7456
|
{ type: "result", ok: false, reason: "error:something local broke" }
|
|
7345
7457
|
]
|
|
@@ -7352,7 +7464,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7352
7464
|
turnFailed(` ${BOUNDED_NOTICE2}${"y".repeat(20)} `)
|
|
7353
7465
|
],
|
|
7354
7466
|
expected: [
|
|
7355
|
-
|
|
7467
|
+
startedSession(NEW_THREAD_ID),
|
|
7356
7468
|
{ type: "runtime_notice", body: BOUNDED_NOTICE2 },
|
|
7357
7469
|
{ type: "result", ok: false, reason: "usage_capped" }
|
|
7358
7470
|
]
|
|
@@ -7363,26 +7475,32 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7363
7475
|
name: "empty failure text emits no runtime notice",
|
|
7364
7476
|
request: makeRequest3(),
|
|
7365
7477
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("")],
|
|
7366
|
-
expected: [
|
|
7478
|
+
expected: [
|
|
7479
|
+
startedSession(NEW_THREAD_ID),
|
|
7480
|
+
{ type: "result", ok: false, reason: "error:unknown" }
|
|
7481
|
+
]
|
|
7367
7482
|
},
|
|
7368
7483
|
{
|
|
7369
7484
|
name: "whitespace-only failure text emits no runtime notice",
|
|
7370
7485
|
request: makeRequest3(),
|
|
7371
7486
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed(" ")],
|
|
7372
|
-
expected: [
|
|
7487
|
+
expected: [startedSession(NEW_THREAD_ID), { type: "result", ok: false, reason: "error: " }]
|
|
7373
7488
|
},
|
|
7374
7489
|
{
|
|
7375
7490
|
name: "bare status token emits no runtime notice",
|
|
7376
7491
|
request: makeRequest3(),
|
|
7377
7492
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("429")],
|
|
7378
|
-
expected: [
|
|
7493
|
+
expected: [
|
|
7494
|
+
startedSession(NEW_THREAD_ID),
|
|
7495
|
+
{ type: "result", ok: false, reason: "rate_limited" }
|
|
7496
|
+
]
|
|
7379
7497
|
},
|
|
7380
7498
|
{
|
|
7381
7499
|
name: "single-word provider sentence emits a runtime notice",
|
|
7382
7500
|
request: makeRequest3(),
|
|
7383
7501
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnFailed("Unavailable.")],
|
|
7384
7502
|
expected: [
|
|
7385
|
-
|
|
7503
|
+
startedSession(NEW_THREAD_ID),
|
|
7386
7504
|
{ type: "runtime_notice", body: "Unavailable." },
|
|
7387
7505
|
{ type: "result", ok: false, reason: "error:Unavailable." }
|
|
7388
7506
|
]
|
|
@@ -7410,7 +7528,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7410
7528
|
turnCompleted()
|
|
7411
7529
|
],
|
|
7412
7530
|
expected: [
|
|
7413
|
-
|
|
7531
|
+
startedSession(NEW_THREAD_ID),
|
|
7414
7532
|
{
|
|
7415
7533
|
type: "result",
|
|
7416
7534
|
ok: false,
|
|
@@ -7430,8 +7548,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7430
7548
|
turnCompleted()
|
|
7431
7549
|
],
|
|
7432
7550
|
expected: [
|
|
7433
|
-
|
|
7551
|
+
startedSession(NEW_THREAD_ID),
|
|
7434
7552
|
{ type: "text", body: "All good now.", terminal: true },
|
|
7553
|
+
closingSession(NEW_THREAD_ID),
|
|
7435
7554
|
{ type: "result", ok: true }
|
|
7436
7555
|
]
|
|
7437
7556
|
},
|
|
@@ -7443,7 +7562,11 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7443
7562
|
name: "zero-content success (empty runtime result)",
|
|
7444
7563
|
request: makeRequest3(),
|
|
7445
7564
|
nativeStream: [threadStarted(NEW_THREAD_ID), turnCompleted()],
|
|
7446
|
-
expected: [
|
|
7565
|
+
expected: [
|
|
7566
|
+
startedSession(NEW_THREAD_ID),
|
|
7567
|
+
closingSession(NEW_THREAD_ID),
|
|
7568
|
+
{ type: "result", ok: true }
|
|
7569
|
+
]
|
|
7447
7570
|
},
|
|
7448
7571
|
{
|
|
7449
7572
|
// Empty-final: a clean turn that ended on a tool call with no closing message.
|
|
@@ -7468,7 +7591,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7468
7591
|
turnCompleted()
|
|
7469
7592
|
],
|
|
7470
7593
|
expected: [
|
|
7471
|
-
|
|
7594
|
+
startedSession(NEW_THREAD_ID),
|
|
7472
7595
|
{
|
|
7473
7596
|
type: "tool",
|
|
7474
7597
|
id: "t1",
|
|
@@ -7486,6 +7609,7 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7486
7609
|
input: { changes: [{ path: "a.md", kind: "add" }] },
|
|
7487
7610
|
result: [{ path: "a.md", kind: "add" }]
|
|
7488
7611
|
},
|
|
7612
|
+
closingSession(NEW_THREAD_ID),
|
|
7489
7613
|
{ type: "result", ok: true }
|
|
7490
7614
|
]
|
|
7491
7615
|
},
|
|
@@ -7501,9 +7625,10 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7501
7625
|
turnCompleted()
|
|
7502
7626
|
],
|
|
7503
7627
|
expected: [
|
|
7504
|
-
|
|
7628
|
+
startedSession(NEW_THREAD_ID),
|
|
7505
7629
|
{ type: "thinking", text: "Let me check the notes." },
|
|
7506
7630
|
{ type: "text", body: "All set.", terminal: true },
|
|
7631
|
+
closingSession(NEW_THREAD_ID),
|
|
7507
7632
|
{ type: "result", ok: true }
|
|
7508
7633
|
]
|
|
7509
7634
|
},
|
|
@@ -7521,8 +7646,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7521
7646
|
}),
|
|
7522
7647
|
nativeStream: [threadStarted(NEW_THREAD_ID), agentMessage("m1", "Done."), turnCompleted()],
|
|
7523
7648
|
expected: [
|
|
7524
|
-
|
|
7649
|
+
startedSession(NEW_THREAD_ID),
|
|
7525
7650
|
{ type: "text", body: "Done.", terminal: true },
|
|
7651
|
+
closingSession(NEW_THREAD_ID),
|
|
7526
7652
|
{
|
|
7527
7653
|
type: "result",
|
|
7528
7654
|
ok: true,
|
|
@@ -7544,7 +7670,8 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7544
7670
|
threadId: "th1",
|
|
7545
7671
|
cwd: DIR2,
|
|
7546
7672
|
promptFingerprint: fingerprintPrompt(PROMPT),
|
|
7547
|
-
usageBaseline: null
|
|
7673
|
+
usageBaseline: null,
|
|
7674
|
+
compactions: 0
|
|
7548
7675
|
})
|
|
7549
7676
|
}),
|
|
7550
7677
|
nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
|
|
@@ -7566,8 +7693,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7566
7693
|
}),
|
|
7567
7694
|
nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
|
|
7568
7695
|
expected: [
|
|
7569
|
-
|
|
7696
|
+
startedSession("th1"),
|
|
7570
7697
|
{ type: "text", body: "Back again.", terminal: true },
|
|
7698
|
+
closingSession("th1"),
|
|
7571
7699
|
{ type: "result", ok: true }
|
|
7572
7700
|
]
|
|
7573
7701
|
},
|
|
@@ -7589,8 +7717,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7589
7717
|
}),
|
|
7590
7718
|
nativeStream: [threadStarted("th1"), agentMessage("m1", "Back again."), turnCompleted()],
|
|
7591
7719
|
expected: [
|
|
7592
|
-
|
|
7720
|
+
startedSession("th1"),
|
|
7593
7721
|
{ type: "text", body: "Back again.", terminal: true },
|
|
7722
|
+
closingSession("th1"),
|
|
7594
7723
|
{ type: "result", ok: true }
|
|
7595
7724
|
]
|
|
7596
7725
|
},
|
|
@@ -7616,8 +7745,9 @@ var CODEX_CONFORMANCE_FIXTURES = [
|
|
|
7616
7745
|
turnCompleted()
|
|
7617
7746
|
],
|
|
7618
7747
|
expected: [
|
|
7619
|
-
|
|
7748
|
+
startedSession(NEW_THREAD_ID, DIR2, true),
|
|
7620
7749
|
{ type: "text", body: "Fresh start.", terminal: true },
|
|
7750
|
+
closingSession(NEW_THREAD_ID),
|
|
7621
7751
|
{ type: "result", ok: true }
|
|
7622
7752
|
]
|
|
7623
7753
|
}
|
|
@@ -7699,7 +7829,7 @@ var ConnectorHealthStore = class {
|
|
|
7699
7829
|
// src/dispatcher.ts
|
|
7700
7830
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
7701
7831
|
import { existsSync as existsSync12, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
|
|
7702
|
-
import { join as
|
|
7832
|
+
import { join as join19 } from "path";
|
|
7703
7833
|
|
|
7704
7834
|
// src/runner/launch.ts
|
|
7705
7835
|
import { createInterface } from "readline";
|
|
@@ -9097,6 +9227,7 @@ function buildCompanionTurnRequest(params) {
|
|
|
9097
9227
|
const { turnContext: t } = params;
|
|
9098
9228
|
return {
|
|
9099
9229
|
systemPrompt: t.systemPrompt,
|
|
9230
|
+
...t.standing ? { standing: t.standing } : {},
|
|
9100
9231
|
prompt: t.prompt,
|
|
9101
9232
|
content: t.content,
|
|
9102
9233
|
config: t.config,
|
|
@@ -9208,6 +9339,62 @@ function message(err) {
|
|
|
9208
9339
|
return err instanceof Error ? err.message : String(err);
|
|
9209
9340
|
}
|
|
9210
9341
|
|
|
9342
|
+
// src/codex-compactions.ts
|
|
9343
|
+
import { createReadStream } from "fs";
|
|
9344
|
+
import { readdir } from "fs/promises";
|
|
9345
|
+
import { homedir as homedir6 } from "os";
|
|
9346
|
+
import { join as join16 } from "path";
|
|
9347
|
+
import { createInterface as createInterface2 } from "readline";
|
|
9348
|
+
var DAY_MS = 864e5;
|
|
9349
|
+
var UUID_V7 = /^([0-9a-f]{8})-([0-9a-f]{4})-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9350
|
+
var RECORD_HEAD_CHARS = 200;
|
|
9351
|
+
function createCodexCompactionCounter(codexHome = process.env.CODEX_HOME || join16(homedir6(), ".codex")) {
|
|
9352
|
+
return async (threadId) => {
|
|
9353
|
+
try {
|
|
9354
|
+
const rollout = await findRollout(codexHome, threadId);
|
|
9355
|
+
return rollout ? await countCompactions(rollout) : null;
|
|
9356
|
+
} catch {
|
|
9357
|
+
return null;
|
|
9358
|
+
}
|
|
9359
|
+
};
|
|
9360
|
+
}
|
|
9361
|
+
async function findRollout(codexHome, threadId) {
|
|
9362
|
+
const id = UUID_V7.exec(threadId);
|
|
9363
|
+
if (!id) return null;
|
|
9364
|
+
const createdAt = parseInt(`${id[1]}${id[2]}`, 16);
|
|
9365
|
+
const suffix = `-${threadId}.jsonl`;
|
|
9366
|
+
for (const offset of [0, -DAY_MS, DAY_MS]) {
|
|
9367
|
+
const day = new Date(createdAt + offset);
|
|
9368
|
+
const dir2 = join16(
|
|
9369
|
+
codexHome,
|
|
9370
|
+
"sessions",
|
|
9371
|
+
String(day.getUTCFullYear()),
|
|
9372
|
+
String(day.getUTCMonth() + 1).padStart(2, "0"),
|
|
9373
|
+
String(day.getUTCDate()).padStart(2, "0")
|
|
9374
|
+
);
|
|
9375
|
+
let names;
|
|
9376
|
+
try {
|
|
9377
|
+
names = await readdir(dir2);
|
|
9378
|
+
} catch {
|
|
9379
|
+
continue;
|
|
9380
|
+
}
|
|
9381
|
+
const name = names.find((n) => n.startsWith("rollout-") && n.endsWith(suffix));
|
|
9382
|
+
if (name) return join16(dir2, name);
|
|
9383
|
+
}
|
|
9384
|
+
return null;
|
|
9385
|
+
}
|
|
9386
|
+
async function countCompactions(rollout) {
|
|
9387
|
+
const lines2 = createInterface2({
|
|
9388
|
+
input: createReadStream(rollout, { encoding: "utf8" }),
|
|
9389
|
+
crlfDelay: Infinity
|
|
9390
|
+
});
|
|
9391
|
+
let count = 0;
|
|
9392
|
+
for await (const line of lines2) {
|
|
9393
|
+
if (line.slice(0, RECORD_HEAD_CHARS).includes('"type":"compacted"')) count += 1;
|
|
9394
|
+
}
|
|
9395
|
+
return count;
|
|
9396
|
+
}
|
|
9397
|
+
|
|
9211
9398
|
// src/turn-seq-floor.ts
|
|
9212
9399
|
var SeqFloorUnavailable = class extends Error {
|
|
9213
9400
|
constructor(detail) {
|
|
@@ -9305,15 +9492,15 @@ function reportFailure(config, body) {
|
|
|
9305
9492
|
|
|
9306
9493
|
// src/prepared.ts
|
|
9307
9494
|
import { mkdirSync as mkdirSync7, readFileSync as readFileSync8, rmSync as rmSync5, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
|
|
9308
|
-
import { join as
|
|
9495
|
+
import { join as join17 } from "path";
|
|
9309
9496
|
function dirFor(workspaceId) {
|
|
9310
|
-
return
|
|
9497
|
+
return join17(cabaneDir(), "prepared", encodeURIComponent(workspaceId));
|
|
9311
9498
|
}
|
|
9312
9499
|
function conversationDir(workspaceId, conversationId) {
|
|
9313
|
-
return
|
|
9500
|
+
return join17(dirFor(workspaceId), encodeURIComponent(conversationId));
|
|
9314
9501
|
}
|
|
9315
9502
|
function pathFor2(workspaceId, conversationId, agentId) {
|
|
9316
|
-
return
|
|
9503
|
+
return join17(conversationDir(workspaceId, conversationId), `${encodeURIComponent(agentId)}.json`);
|
|
9317
9504
|
}
|
|
9318
9505
|
function readPrepared(workspaceId, conversationId, agentId) {
|
|
9319
9506
|
const path = pathFor2(workspaceId, conversationId, agentId);
|
|
@@ -9345,9 +9532,9 @@ function clearPrepared(workspaceId, conversationId, agentId) {
|
|
|
9345
9532
|
|
|
9346
9533
|
// src/transcript-writer.ts
|
|
9347
9534
|
import { appendFileSync, chmodSync as chmodSync4, copyFileSync, mkdirSync as mkdirSync8, readdirSync, rmSync as rmSync6 } from "fs";
|
|
9348
|
-
import { basename as basename3, dirname as dirname7, join as
|
|
9535
|
+
import { basename as basename3, dirname as dirname7, join as join18 } from "path";
|
|
9349
9536
|
function transcriptsDir() {
|
|
9350
|
-
return
|
|
9537
|
+
return join18(runnerStateDir(), "transcripts");
|
|
9351
9538
|
}
|
|
9352
9539
|
var RETAIN = 200;
|
|
9353
9540
|
var ANOMALY_RETAIN = 50;
|
|
@@ -9357,7 +9544,7 @@ var TranscriptWriter = class {
|
|
|
9357
9544
|
onWarn;
|
|
9358
9545
|
constructor(dir2, meta, onWarn) {
|
|
9359
9546
|
this.onWarn = onWarn;
|
|
9360
|
-
this.path =
|
|
9547
|
+
this.path = join18(dir2, fileName(meta));
|
|
9361
9548
|
try {
|
|
9362
9549
|
mkdirSync8(dir2, { recursive: true });
|
|
9363
9550
|
try {
|
|
@@ -9386,10 +9573,10 @@ var TranscriptWriter = class {
|
|
|
9386
9573
|
preserveAnomaly() {
|
|
9387
9574
|
if (this.broken) return;
|
|
9388
9575
|
try {
|
|
9389
|
-
const dir2 =
|
|
9576
|
+
const dir2 = join18(dirname7(this.path), "anomalies");
|
|
9390
9577
|
mkdirSync8(dir2, { recursive: true, mode: 448 });
|
|
9391
9578
|
chmodSync4(dir2, 448);
|
|
9392
|
-
const target =
|
|
9579
|
+
const target = join18(dir2, basename3(this.path));
|
|
9393
9580
|
copyFileSync(this.path, target);
|
|
9394
9581
|
chmodSync4(target, 384);
|
|
9395
9582
|
pruneOld(dir2, ANOMALY_RETAIN);
|
|
@@ -9435,7 +9622,7 @@ function pruneOld(dir2, retain) {
|
|
|
9435
9622
|
const drop = files.sort().slice(0, files.length - retain);
|
|
9436
9623
|
for (const f of drop) {
|
|
9437
9624
|
try {
|
|
9438
|
-
rmSync6(
|
|
9625
|
+
rmSync6(join18(dir2, f), { force: true });
|
|
9439
9626
|
} catch {
|
|
9440
9627
|
}
|
|
9441
9628
|
}
|
|
@@ -10344,6 +10531,8 @@ var TurnExecution = class {
|
|
|
10344
10531
|
turnId: this.turnId,
|
|
10345
10532
|
warn: (meta, msg) => turnLog.warn(meta, msg)
|
|
10346
10533
|
}),
|
|
10534
|
+
// Whether a resumed thread compacted since its standing preamble was sent.
|
|
10535
|
+
countCompactions: createCodexCompactionCounter(),
|
|
10347
10536
|
...codexPath ? { transport: createSdkCodexTransport({ codexPathOverride: codexPath }) } : {}
|
|
10348
10537
|
})
|
|
10349
10538
|
);
|
|
@@ -10404,6 +10593,9 @@ var TurnExecution = class {
|
|
|
10404
10593
|
);
|
|
10405
10594
|
return {
|
|
10406
10595
|
systemPrompt: context.systemPrompt,
|
|
10596
|
+
// Replaced even when absent, so the segments can't outlive the
|
|
10597
|
+
// `systemPrompt` they were split from.
|
|
10598
|
+
standing: context.standing,
|
|
10407
10599
|
prompt: context.prompt,
|
|
10408
10600
|
content: context.content
|
|
10409
10601
|
};
|
|
@@ -10815,7 +11007,7 @@ function checkoutState(cwd) {
|
|
|
10815
11007
|
if (entries.length === 0) {
|
|
10816
11008
|
return { ok: false, reason: `${cwd} is empty \u2014 a recreated shell, not a prepared directory` };
|
|
10817
11009
|
}
|
|
10818
|
-
const gitPath =
|
|
11010
|
+
const gitPath = join19(cwd, ".git");
|
|
10819
11011
|
if (!existsSync12(gitPath)) return { ok: true, reason: "usable" };
|
|
10820
11012
|
let stat;
|
|
10821
11013
|
try {
|
|
@@ -10823,7 +11015,7 @@ function checkoutState(cwd) {
|
|
|
10823
11015
|
} catch (error) {
|
|
10824
11016
|
return { ok: false, reason: `${gitPath} is unreadable (${error.message})` };
|
|
10825
11017
|
}
|
|
10826
|
-
if (stat.isDirectory() && !existsSync12(
|
|
11018
|
+
if (stat.isDirectory() && !existsSync12(join19(gitPath, "HEAD")))
|
|
10827
11019
|
return { ok: false, reason: `${gitPath} has no HEAD \u2014 an empty shell, not a checkout` };
|
|
10828
11020
|
return { ok: true, reason: "usable" };
|
|
10829
11021
|
}
|
|
@@ -11030,7 +11222,7 @@ function createLocalProcessProvider(opts = {}) {
|
|
|
11030
11222
|
import { createHash as createHash4, randomUUID as randomUUID5 } from "crypto";
|
|
11031
11223
|
import { constants as constants3, tmpdir as tmpdir3 } from "os";
|
|
11032
11224
|
import {
|
|
11033
|
-
createReadStream,
|
|
11225
|
+
createReadStream as createReadStream2,
|
|
11034
11226
|
createWriteStream as createWriteStream2,
|
|
11035
11227
|
existsSync as existsSync15,
|
|
11036
11228
|
mkdirSync as mkdirSync10,
|
|
@@ -11041,7 +11233,7 @@ import {
|
|
|
11041
11233
|
statSync as statSync4,
|
|
11042
11234
|
writeFileSync as writeFileSync6
|
|
11043
11235
|
} from "fs";
|
|
11044
|
-
import { join as
|
|
11236
|
+
import { join as join22 } from "path";
|
|
11045
11237
|
import { Writable } from "stream";
|
|
11046
11238
|
|
|
11047
11239
|
// src/providers/docker-cli.ts
|
|
@@ -11235,7 +11427,7 @@ import {
|
|
|
11235
11427
|
renameSync as renameSync5,
|
|
11236
11428
|
rmSync as rmSync8
|
|
11237
11429
|
} from "fs";
|
|
11238
|
-
import { join as
|
|
11430
|
+
import { join as join20 } from "path";
|
|
11239
11431
|
function routes(t) {
|
|
11240
11432
|
const conv = `/api/workspaces/${t.workspaceId}/conversations/${t.conversationId}`;
|
|
11241
11433
|
const agent = `${conv}/participants/agents/${t.agentId}`;
|
|
@@ -11281,7 +11473,7 @@ function publishStaged(from, to, move = atomicMove) {
|
|
|
11281
11473
|
const staged = readdirSync3(from).filter((n) => n.endsWith(".json")).map((name) => {
|
|
11282
11474
|
let entry = {};
|
|
11283
11475
|
try {
|
|
11284
|
-
entry = JSON.parse(readFileSync9(
|
|
11476
|
+
entry = JSON.parse(readFileSync9(join20(from, name), "utf8"));
|
|
11285
11477
|
} catch {
|
|
11286
11478
|
}
|
|
11287
11479
|
return { name, entry };
|
|
@@ -11294,7 +11486,7 @@ function publishStaged(from, to, move = atomicMove) {
|
|
|
11294
11486
|
let published = 0;
|
|
11295
11487
|
for (const { name } of staged) {
|
|
11296
11488
|
try {
|
|
11297
|
-
move(
|
|
11489
|
+
move(join20(from, name), join20(to, name));
|
|
11298
11490
|
} catch {
|
|
11299
11491
|
return { published, complete: false };
|
|
11300
11492
|
}
|
|
@@ -11307,7 +11499,7 @@ function publishStaged(from, to, move = atomicMove) {
|
|
|
11307
11499
|
// src/providers/runner-base.ts
|
|
11308
11500
|
import { createHash as createHash3 } from "crypto";
|
|
11309
11501
|
import { existsSync as existsSync14, realpathSync as realpathSync4 } from "fs";
|
|
11310
|
-
import { dirname as dirname8, join as
|
|
11502
|
+
import { dirname as dirname8, join as join21, sep as sep3 } from "path";
|
|
11311
11503
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
11312
11504
|
var COMPANION_MOUNT = "/opt/cabane/companion";
|
|
11313
11505
|
var BASE_DOCKERFILE = `FROM node:22-slim
|
|
@@ -11355,11 +11547,11 @@ async function ensureBaseImage(docker, image, log) {
|
|
|
11355
11547
|
function ownInstall(moduleFile = fileURLToPath5(import.meta.url)) {
|
|
11356
11548
|
let dir2 = dirname8(moduleFile);
|
|
11357
11549
|
for (let i = 0; i < 4; i++) {
|
|
11358
|
-
if (existsSync14(
|
|
11550
|
+
if (existsSync14(join21(dir2, "dist", "cli.js")) && existsSync14(join21(dir2, "package.json"))) break;
|
|
11359
11551
|
dir2 = dirname8(dir2);
|
|
11360
11552
|
}
|
|
11361
|
-
if (!existsSync14(
|
|
11362
|
-
const sdk =
|
|
11553
|
+
if (!existsSync14(join21(dir2, "dist", "cli.js"))) return null;
|
|
11554
|
+
const sdk = join21(dir2, "node_modules", "@anthropic-ai", "claude-agent-sdk");
|
|
11363
11555
|
if (!existsSync14(sdk)) return null;
|
|
11364
11556
|
const root = realpathSync4(dir2);
|
|
11365
11557
|
return realpathSync4(sdk).startsWith(root + sep3) ? root : null;
|
|
@@ -11498,39 +11690,39 @@ function networkCreateError(stderr) {
|
|
|
11498
11690
|
}
|
|
11499
11691
|
function sessionArchivePath(deviceDir, attemptId) {
|
|
11500
11692
|
if (!UUID_RE.test(attemptId)) throw new Error(`docker: not an attempt id: ${attemptId}`);
|
|
11501
|
-
return
|
|
11693
|
+
return join22(deviceDir, "attempts", attemptId, "session.tar");
|
|
11502
11694
|
}
|
|
11503
11695
|
function pruneOtherAttempts(deviceDir, pair, keep) {
|
|
11504
|
-
const root =
|
|
11696
|
+
const root = join22(deviceDir, "attempts");
|
|
11505
11697
|
if (!existsSync15(root)) return;
|
|
11506
11698
|
for (const id of readdirSync4(root)) {
|
|
11507
11699
|
if (id === keep || !UUID_RE.test(id)) continue;
|
|
11508
11700
|
let pairOf = "";
|
|
11509
11701
|
try {
|
|
11510
|
-
pairOf = readFileSync10(
|
|
11702
|
+
pairOf = readFileSync10(join22(root, id, "pair"), "utf8");
|
|
11511
11703
|
} catch {
|
|
11512
11704
|
continue;
|
|
11513
11705
|
}
|
|
11514
11706
|
if (pairOf === `${pair.conversationId}/${pair.agentId}`) {
|
|
11515
|
-
rmSync9(
|
|
11707
|
+
rmSync9(join22(root, id), { recursive: true, force: true });
|
|
11516
11708
|
}
|
|
11517
11709
|
}
|
|
11518
11710
|
}
|
|
11519
11711
|
var IDLE_SESSION_ARCHIVE_MS = 30 * 24 * 60 * 6e4;
|
|
11520
11712
|
function pruneIdleSessionArchives(deviceDir, now = Date.now(), maxAgeMs = IDLE_SESSION_ARCHIVE_MS) {
|
|
11521
|
-
const root =
|
|
11713
|
+
const root = join22(deviceDir, "attempts");
|
|
11522
11714
|
if (!existsSync15(root)) return [];
|
|
11523
11715
|
const removed = [];
|
|
11524
11716
|
for (const id of readdirSync4(root)) {
|
|
11525
11717
|
if (!UUID_RE.test(id)) continue;
|
|
11526
11718
|
let kept;
|
|
11527
11719
|
try {
|
|
11528
|
-
kept = statSync4(
|
|
11720
|
+
kept = statSync4(join22(root, id)).mtimeMs;
|
|
11529
11721
|
} catch {
|
|
11530
11722
|
continue;
|
|
11531
11723
|
}
|
|
11532
11724
|
if (now - kept < maxAgeMs) continue;
|
|
11533
|
-
rmSync9(
|
|
11725
|
+
rmSync9(join22(root, id), { recursive: true, force: true });
|
|
11534
11726
|
removed.push(id);
|
|
11535
11727
|
}
|
|
11536
11728
|
return removed;
|
|
@@ -11731,11 +11923,11 @@ function createDockerProvider(opts) {
|
|
|
11731
11923
|
await must(["network", "disconnect", "-f", other, name], "moving the container");
|
|
11732
11924
|
if (others.includes(sharedNetwork)) await retireSharedNetwork();
|
|
11733
11925
|
};
|
|
11734
|
-
const socketRoot = opts.socketRoot ??
|
|
11926
|
+
const socketRoot = opts.socketRoot ?? join22(
|
|
11735
11927
|
process.env.XDG_RUNTIME_DIR && existsSync15(process.env.XDG_RUNTIME_DIR) ? process.env.XDG_RUNTIME_DIR : tmpdir3(),
|
|
11736
11928
|
`cabane-runners-${opts.deviceKey}`
|
|
11737
11929
|
);
|
|
11738
|
-
const socketDir = (name) =>
|
|
11930
|
+
const socketDir = (name) => join22(socketRoot, createHash4("sha256").update(name).digest("hex").slice(0, 12));
|
|
11739
11931
|
const create = async (name, attempt) => {
|
|
11740
11932
|
mkdirSync10(socketRoot, { recursive: true, mode: 448 });
|
|
11741
11933
|
mkdirSync10(socketDir(name), { recursive: true, mode: 457 });
|
|
@@ -11877,7 +12069,7 @@ function createDockerProvider(opts) {
|
|
|
11877
12069
|
}
|
|
11878
12070
|
};
|
|
11879
12071
|
const removeIn = (name, from, names) => names.length ? execIn(name, ["sh", "-c", 'cd "$1" && shift && rm -f -- "$@"', "sh", from, ...names]) : Promise.resolve(null);
|
|
11880
|
-
const pendingPath = (name) =>
|
|
12072
|
+
const pendingPath = (name) => join22(opts.deviceDir, "runners-pending", `${name}.json`);
|
|
11881
12073
|
const readPending = (name) => {
|
|
11882
12074
|
try {
|
|
11883
12075
|
const ids = JSON.parse(readFileSync10(pendingPath(name), "utf8"));
|
|
@@ -11891,12 +12083,12 @@ function createDockerProvider(opts) {
|
|
|
11891
12083
|
rmSync9(pendingPath(name), { force: true });
|
|
11892
12084
|
return;
|
|
11893
12085
|
}
|
|
11894
|
-
mkdirSync10(
|
|
12086
|
+
mkdirSync10(join22(opts.deviceDir, "runners-pending"), { recursive: true });
|
|
11895
12087
|
writeFileSync6(pendingPath(name), JSON.stringify([...new Set(ids)]));
|
|
11896
12088
|
};
|
|
11897
12089
|
const keepSession = async (name, attempt, attemptId) => {
|
|
11898
12090
|
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
11899
|
-
mkdirSync10(
|
|
12091
|
+
mkdirSync10(join22(archive, ".."), { recursive: true });
|
|
11900
12092
|
const partial = `${archive}.${process.pid}.partial`;
|
|
11901
12093
|
const file = new CappedFile(partial, sessionCap);
|
|
11902
12094
|
const r = await execIn(
|
|
@@ -11913,7 +12105,7 @@ function createDockerProvider(opts) {
|
|
|
11913
12105
|
);
|
|
11914
12106
|
if (r.code === 0 && !file.over) {
|
|
11915
12107
|
renameSync6(partial, archive);
|
|
11916
|
-
writeFileSync6(
|
|
12108
|
+
writeFileSync6(join22(archive, "..", "pair"), `${attempt.conversationId}/${attempt.agentId}`);
|
|
11917
12109
|
pruneOtherAttempts(opts.deviceDir, attempt, attemptId);
|
|
11918
12110
|
await writeMarker(name, attemptId);
|
|
11919
12111
|
log.debug({ container: name, attemptId, bytes: file.bytes }, "runners: session files kept");
|
|
@@ -11927,7 +12119,7 @@ function createDockerProvider(opts) {
|
|
|
11927
12119
|
file.over ? "runners: the session files are over the size cap and weren't kept; the next new container starts the session over" : "runners: couldn't keep the session files; the next new container starts the session over"
|
|
11928
12120
|
);
|
|
11929
12121
|
};
|
|
11930
|
-
const stagingDir = (name) =>
|
|
12122
|
+
const stagingDir = (name) => join22(opts.deviceDir, "runners-pending", `${name}.outbox`);
|
|
11931
12123
|
const ensureRunning = async (name) => {
|
|
11932
12124
|
const seen = await docker.run(["inspect", "-f", "{{.State.Running}}", name]);
|
|
11933
12125
|
if (seen.code !== 0) return false;
|
|
@@ -11935,7 +12127,7 @@ function createDockerProvider(opts) {
|
|
|
11935
12127
|
return (await docker.run(["start", name])).code === 0;
|
|
11936
12128
|
};
|
|
11937
12129
|
const dropArchive = (attemptId) => {
|
|
11938
|
-
rmSync9(
|
|
12130
|
+
rmSync9(join22(sessionArchivePath(opts.deviceDir, attemptId), ".."), {
|
|
11939
12131
|
recursive: true,
|
|
11940
12132
|
force: true
|
|
11941
12133
|
});
|
|
@@ -12004,7 +12196,7 @@ function createDockerProvider(opts) {
|
|
|
12004
12196
|
if (seen.stdout.trim().split(" ")[0] !== "true")
|
|
12005
12197
|
await must(["start", name], "starting the container");
|
|
12006
12198
|
const git = await serveGitCredentials({
|
|
12007
|
-
socketPath:
|
|
12199
|
+
socketPath: join22(socketDir(name), "git.sock"),
|
|
12008
12200
|
hosts: opts.gitHosts,
|
|
12009
12201
|
fill: opts.gitFill,
|
|
12010
12202
|
log
|
|
@@ -12040,7 +12232,7 @@ function createDockerProvider(opts) {
|
|
|
12040
12232
|
const archive = sessionArchivePath(opts.deviceDir, attemptId);
|
|
12041
12233
|
if (existsSync15(archive)) {
|
|
12042
12234
|
const r = await execIn(name, ["tar", "-x", "-C", CONTAINER_HOME, "-f", "-"], {
|
|
12043
|
-
input:
|
|
12235
|
+
input: createReadStream2(archive)
|
|
12044
12236
|
});
|
|
12045
12237
|
if (r.code !== 0) {
|
|
12046
12238
|
log.warn(
|
|
@@ -12153,15 +12345,15 @@ function createDockerProvider(opts) {
|
|
|
12153
12345
|
const transcripts = await readDir(name, `${CONTAINER_STATE}/transcripts`);
|
|
12154
12346
|
if (transcripts) {
|
|
12155
12347
|
const kept = transcripts.files.filter((f) => SAFE_NAME_RE.test(f.name));
|
|
12156
|
-
if (kept.length) mkdirSync10(
|
|
12157
|
-
for (const f of kept) writeFileSync6(
|
|
12348
|
+
if (kept.length) mkdirSync10(join22(opts.hostStateDir, "transcripts"), { recursive: true });
|
|
12349
|
+
for (const f of kept) writeFileSync6(join22(opts.hostStateDir, "transcripts", f.name), f.data);
|
|
12158
12350
|
await removeIn(
|
|
12159
12351
|
name,
|
|
12160
12352
|
`${CONTAINER_STATE}/transcripts`,
|
|
12161
12353
|
kept.map((f) => f.name)
|
|
12162
12354
|
);
|
|
12163
12355
|
}
|
|
12164
|
-
const outboxRel =
|
|
12356
|
+
const outboxRel = join22("outbox", encodeURIComponent(attempt.agentId));
|
|
12165
12357
|
const outbox = await readDir(name, `${CONTAINER_STATE}/${outboxRel}`);
|
|
12166
12358
|
let unpublished = false;
|
|
12167
12359
|
if (outbox) {
|
|
@@ -12187,7 +12379,7 @@ function createDockerProvider(opts) {
|
|
|
12187
12379
|
const staged = stagingDir(name);
|
|
12188
12380
|
mkdirSync10(staged, { recursive: true });
|
|
12189
12381
|
writeFileSync6(
|
|
12190
|
-
|
|
12382
|
+
join22(staged, `${encodeURIComponent(entry.turnId)}__${entry.seq}.json`),
|
|
12191
12383
|
`${JSON.stringify(entry)}
|
|
12192
12384
|
`
|
|
12193
12385
|
);
|
|
@@ -12200,7 +12392,7 @@ function createDockerProvider(opts) {
|
|
|
12200
12392
|
if (outbox.complete) {
|
|
12201
12393
|
const published = publishStaged(
|
|
12202
12394
|
stagingDir(name),
|
|
12203
|
-
|
|
12395
|
+
join22(opts.hostStateDir, outboxRel),
|
|
12204
12396
|
opts.moveFile
|
|
12205
12397
|
);
|
|
12206
12398
|
queued = published.published;
|
|
@@ -12415,8 +12607,8 @@ import {
|
|
|
12415
12607
|
} from "fs";
|
|
12416
12608
|
import { createRequire as createRequire2 } from "module";
|
|
12417
12609
|
import { isIP } from "net";
|
|
12418
|
-
import { homedir as
|
|
12419
|
-
import { dirname as dirname9, join as
|
|
12610
|
+
import { homedir as homedir7, networkInterfaces, tmpdir as tmpdir4, userInfo as userInfo2 } from "os";
|
|
12611
|
+
import { dirname as dirname9, join as join23, sep as sep4 } from "path";
|
|
12420
12612
|
import { fileURLToPath as fileURLToPath6 } from "url";
|
|
12421
12613
|
import { SandboxManager } from "@anthropic-ai/sandbox-runtime";
|
|
12422
12614
|
function accountHome() {
|
|
@@ -12513,7 +12705,7 @@ function companionStoreWatch(log) {
|
|
|
12513
12705
|
return new CompanionStoreWatch({
|
|
12514
12706
|
log,
|
|
12515
12707
|
roots: () => {
|
|
12516
|
-
const home =
|
|
12708
|
+
const home = homedir7();
|
|
12517
12709
|
return storeWalkRoots([.../* @__PURE__ */ new Set([home, accountHome() ?? home])], cabaneDir());
|
|
12518
12710
|
}
|
|
12519
12711
|
});
|
|
@@ -12612,8 +12804,8 @@ function emptyHarnessVersions() {
|
|
|
12612
12804
|
}
|
|
12613
12805
|
|
|
12614
12806
|
// src/harness-copy.ts
|
|
12615
|
-
import { homedir as
|
|
12616
|
-
import { join as
|
|
12807
|
+
import { homedir as homedir8 } from "os";
|
|
12808
|
+
import { join as join24 } from "path";
|
|
12617
12809
|
var HARNESS_LABELS = {
|
|
12618
12810
|
"claude-code": "Claude Code",
|
|
12619
12811
|
codex: "Codex",
|
|
@@ -12627,7 +12819,7 @@ function failureText(runtime, reason) {
|
|
|
12627
12819
|
case "spawn":
|
|
12628
12820
|
return `Couldn't check whether ${label} is signed in: Cabane's bundled ${label} wouldn't start. Repair it with ${repairSteps()} and try again.${spaced(repairCost())}`;
|
|
12629
12821
|
case "config":
|
|
12630
|
-
return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(
|
|
12822
|
+
return `Couldn't check whether ${label} is signed in: its configuration file didn't load${runtime === "codex" ? ` (${tildePath(join24(homedir8(), ".codex", "config.toml"))})` : ""}. Fix it, then try again.`;
|
|
12631
12823
|
case "unreadable":
|
|
12632
12824
|
return `Couldn't check whether ${label} is signed in: it answered in a way Cabane couldn't read. Try again, and if it keeps happening, update Cabane.`;
|
|
12633
12825
|
}
|
|
@@ -12967,7 +13159,7 @@ import {
|
|
|
12967
13159
|
rmSync as rmSync11,
|
|
12968
13160
|
writeFileSync as writeFileSync8
|
|
12969
13161
|
} from "fs";
|
|
12970
|
-
import { join as
|
|
13162
|
+
import { join as join25 } from "path";
|
|
12971
13163
|
var MAX_ENTRIES = 2e3;
|
|
12972
13164
|
var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
12973
13165
|
var Outbox = class {
|
|
@@ -12980,10 +13172,10 @@ var Outbox = class {
|
|
|
12980
13172
|
// Resolved lazily (per call) so tests that swap HOME between
|
|
12981
13173
|
// cases route writes at the right tmpdir.
|
|
12982
13174
|
dir() {
|
|
12983
|
-
return
|
|
13175
|
+
return join25(runnerStateDir(), "outbox", encodeURIComponent(this.workspaceId));
|
|
12984
13176
|
}
|
|
12985
13177
|
fileFor(turnId, seq) {
|
|
12986
|
-
return
|
|
13178
|
+
return join25(this.dir(), `${encodeURIComponent(turnId)}__${seq}.json`);
|
|
12987
13179
|
}
|
|
12988
13180
|
// Persist a commit for later draining. Atomic (temp file + rename) so a
|
|
12989
13181
|
// concurrent `list()` never reads a half-written entry, then enforces the
|
|
@@ -13025,7 +13217,7 @@ var Outbox = class {
|
|
|
13025
13217
|
const entries = [];
|
|
13026
13218
|
for (const name of names) {
|
|
13027
13219
|
if (!name.endsWith(".json")) continue;
|
|
13028
|
-
const full =
|
|
13220
|
+
const full = join25(dir2, name);
|
|
13029
13221
|
try {
|
|
13030
13222
|
const parsed = JSON.parse(readFileSync12(full, "utf8"));
|
|
13031
13223
|
if (parsed && typeof parsed.turnId === "string" && typeof parsed.seq === "number" && typeof parsed.path === "string") {
|
|
@@ -13249,7 +13441,7 @@ var SseSubscriber = class {
|
|
|
13249
13441
|
// src/updater.ts
|
|
13250
13442
|
import { spawn as spawn8 } from "child_process";
|
|
13251
13443
|
import { copyFileSync as copyFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync16, renameSync as renameSync11, rmSync as rmSync14 } from "fs";
|
|
13252
|
-
import { dirname as dirname13, join as
|
|
13444
|
+
import { dirname as dirname13, join as join29 } from "path";
|
|
13253
13445
|
|
|
13254
13446
|
// src/boot-rule.ts
|
|
13255
13447
|
import {
|
|
@@ -13261,7 +13453,7 @@ import {
|
|
|
13261
13453
|
symlinkSync,
|
|
13262
13454
|
writeFileSync as writeFileSync9
|
|
13263
13455
|
} from "fs";
|
|
13264
|
-
import { dirname as dirname10, join as
|
|
13456
|
+
import { dirname as dirname10, join as join26 } from "path";
|
|
13265
13457
|
var UNCONFIRMED_WINDOW_MS = 10 * 6e4;
|
|
13266
13458
|
var MAX_STAGE_ATTEMPTS = 3;
|
|
13267
13459
|
function superseded(pending, ...running) {
|
|
@@ -13294,26 +13486,26 @@ import {
|
|
|
13294
13486
|
rmSync as rmSync13,
|
|
13295
13487
|
symlinkSync as symlinkSync2
|
|
13296
13488
|
} from "fs";
|
|
13297
|
-
import { dirname as dirname11, join as
|
|
13489
|
+
import { dirname as dirname11, join as join27 } from "path";
|
|
13298
13490
|
import { fileURLToPath as fileURLToPath7 } from "url";
|
|
13299
13491
|
function versionsDir() {
|
|
13300
|
-
return
|
|
13492
|
+
return join27(cabaneDir(), "versions");
|
|
13301
13493
|
}
|
|
13302
13494
|
function currentLink() {
|
|
13303
|
-
return
|
|
13495
|
+
return join27(cabaneDir(), "current");
|
|
13304
13496
|
}
|
|
13305
13497
|
function bootGuardPath() {
|
|
13306
|
-
return
|
|
13498
|
+
return join27(cabaneDir(), "boot-guard.mjs");
|
|
13307
13499
|
}
|
|
13308
13500
|
function versionPackageRoot(version) {
|
|
13309
|
-
return
|
|
13501
|
+
return join27(versionsDir(), version, "node_modules", "@cabane", "companion");
|
|
13310
13502
|
}
|
|
13311
13503
|
function ownPackageRoot(moduleUrl = import.meta.url) {
|
|
13312
13504
|
return dirname11(dirname11(fileURLToPath7(moduleUrl)));
|
|
13313
13505
|
}
|
|
13314
13506
|
function packageVersion(root) {
|
|
13315
13507
|
try {
|
|
13316
|
-
const pkg2 = JSON.parse(readFileSync14(
|
|
13508
|
+
const pkg2 = JSON.parse(readFileSync14(join27(root, "package.json"), "utf8"));
|
|
13317
13509
|
return typeof pkg2.version === "string" ? pkg2.version : null;
|
|
13318
13510
|
} catch {
|
|
13319
13511
|
return null;
|
|
@@ -13322,7 +13514,7 @@ function packageVersion(root) {
|
|
|
13322
13514
|
function currentRoot() {
|
|
13323
13515
|
try {
|
|
13324
13516
|
const root = realpathSync7(currentLink());
|
|
13325
|
-
return existsSync17(
|
|
13517
|
+
return existsSync17(join27(root, "dist", "cli.js")) ? root : null;
|
|
13326
13518
|
} catch {
|
|
13327
13519
|
return null;
|
|
13328
13520
|
}
|
|
@@ -13345,7 +13537,7 @@ function pruneVersions(keep) {
|
|
|
13345
13537
|
const removed = [];
|
|
13346
13538
|
for (const name of readdirSync6(dir2)) {
|
|
13347
13539
|
if (keep.includes(name)) continue;
|
|
13348
|
-
rmSync13(
|
|
13540
|
+
rmSync13(join27(dir2, name), { recursive: true, force: true });
|
|
13349
13541
|
removed.push(name);
|
|
13350
13542
|
}
|
|
13351
13543
|
return removed;
|
|
@@ -13364,7 +13556,7 @@ function compareVersions(a, b) {
|
|
|
13364
13556
|
|
|
13365
13557
|
// src/update-state.ts
|
|
13366
13558
|
import { mkdirSync as mkdirSync15, readFileSync as readFileSync15, renameSync as renameSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
13367
|
-
import { dirname as dirname12, join as
|
|
13559
|
+
import { dirname as dirname12, join as join28 } from "path";
|
|
13368
13560
|
import { z as z18 } from "zod";
|
|
13369
13561
|
var resultSchema = z18.object({
|
|
13370
13562
|
kind: z18.enum(["updated", "rolled_back", "failed"]),
|
|
@@ -13388,7 +13580,7 @@ var stateSchema = z18.object({
|
|
|
13388
13580
|
failed: z18.object({ version: z18.string(), attempts: z18.number().int(), at: z18.string() }).optional()
|
|
13389
13581
|
});
|
|
13390
13582
|
function updateStatePath(dir2 = cabaneDir()) {
|
|
13391
|
-
return
|
|
13583
|
+
return join28(dir2, "update.json");
|
|
13392
13584
|
}
|
|
13393
13585
|
function readUpdateState(path = updateStatePath()) {
|
|
13394
13586
|
try {
|
|
@@ -13437,7 +13629,7 @@ function rollBack(pending, reason, now = /* @__PURE__ */ new Date()) {
|
|
|
13437
13629
|
return updateUpdateState((state) => rolledBackState(state, pending, reason, now));
|
|
13438
13630
|
}
|
|
13439
13631
|
function writeBootGuard(root = ownPackageRoot()) {
|
|
13440
|
-
const built =
|
|
13632
|
+
const built = join29(root, "dist", "boot-guard.js");
|
|
13441
13633
|
if (!existsSync18(built)) return false;
|
|
13442
13634
|
const target = bootGuardPath();
|
|
13443
13635
|
mkdirSync16(dirname13(target), { recursive: true });
|
|
@@ -13477,17 +13669,17 @@ var runProcess = (command, args, opts = {}) => new Promise((resolve2) => {
|
|
|
13477
13669
|
async function stageVersion(version, harnesses, deps = {}) {
|
|
13478
13670
|
const run = deps.run ?? runProcess;
|
|
13479
13671
|
const node = deps.execPath ?? process.execPath;
|
|
13480
|
-
const target =
|
|
13672
|
+
const target = join29(versionsDir(), version);
|
|
13481
13673
|
const placed = versionPackageRoot(version);
|
|
13482
13674
|
if (existsSync18(placed)) {
|
|
13483
13675
|
if (!await verifyInstall(placed, version, harnesses, { run, execPath: node }))
|
|
13484
13676
|
return { ok: true, root: placed };
|
|
13485
13677
|
}
|
|
13486
|
-
const npm =
|
|
13678
|
+
const npm = join29(dirname13(node), "npm");
|
|
13487
13679
|
if (!existsSync18(npm))
|
|
13488
13680
|
return { ok: false, reason: `there is no npm beside Node at ${dirname13(node)}` };
|
|
13489
13681
|
mkdirSync16(versionsDir(), { recursive: true });
|
|
13490
|
-
const staging =
|
|
13682
|
+
const staging = join29(versionsDir(), `.${version}.staging-${process.pid}`);
|
|
13491
13683
|
rmSync14(staging, { recursive: true, force: true });
|
|
13492
13684
|
const installed = await run(
|
|
13493
13685
|
npm,
|
|
@@ -13516,7 +13708,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
13516
13708
|
return { ok: false, reason: `version ${version} isn't published yet`, unpublished: true };
|
|
13517
13709
|
return { ok: false, reason: `npm couldn't install it: ${lastLine(installed.stderr)}` };
|
|
13518
13710
|
}
|
|
13519
|
-
const root =
|
|
13711
|
+
const root = join29(staging, "node_modules", "@cabane", "companion");
|
|
13520
13712
|
const verdict = await verifyInstall(root, version, harnesses, { run, execPath: node });
|
|
13521
13713
|
if (verdict) {
|
|
13522
13714
|
rmSync14(staging, { recursive: true, force: true });
|
|
@@ -13528,7 +13720,7 @@ async function stageVersion(version, harnesses, deps = {}) {
|
|
|
13528
13720
|
}
|
|
13529
13721
|
async function verifyInstall(root, version, harnesses, deps) {
|
|
13530
13722
|
if (packageVersion(root) !== version) return `the installed package isn't version ${version}`;
|
|
13531
|
-
const answered = await deps.run(deps.execPath, [
|
|
13723
|
+
const answered = await deps.run(deps.execPath, [join29(root, "dist", "bin.js"), "--version"], {
|
|
13532
13724
|
timeoutMs: 6e4,
|
|
13533
13725
|
env: { ...process.env, CABANE_LAUNCH_SELF: "1" }
|
|
13534
13726
|
});
|
|
@@ -13536,7 +13728,7 @@ async function verifyInstall(root, version, harnesses, deps) {
|
|
|
13536
13728
|
return `it doesn't start: ${lastLine(answered.stderr) || "no version printed"}`;
|
|
13537
13729
|
}
|
|
13538
13730
|
if (harnesses.length === 0) return null;
|
|
13539
|
-
const probed = await deps.run(deps.execPath, [
|
|
13731
|
+
const probed = await deps.run(deps.execPath, [join29(root, "dist", "postinstall.js"), "--json"], {
|
|
13540
13732
|
timeoutMs: 6e4
|
|
13541
13733
|
});
|
|
13542
13734
|
let resolutions = {};
|
|
@@ -15337,9 +15529,9 @@ function handleUncaught(log, err, origin) {
|
|
|
15337
15529
|
|
|
15338
15530
|
// src/crash-marker.ts
|
|
15339
15531
|
import { existsSync as existsSync19, mkdirSync as mkdirSync17, readFileSync as readFileSync16, rmSync as rmSync15, writeFileSync as writeFileSync11 } from "fs";
|
|
15340
|
-
import { join as
|
|
15532
|
+
import { join as join30 } from "path";
|
|
15341
15533
|
function crashMarkerPath() {
|
|
15342
|
-
return
|
|
15534
|
+
return join30(cabaneDir(), "last-error.json");
|
|
15343
15535
|
}
|
|
15344
15536
|
function recordCrash(rec) {
|
|
15345
15537
|
try {
|
|
@@ -15359,7 +15551,7 @@ function clearCrash() {
|
|
|
15359
15551
|
// src/stray-companion.ts
|
|
15360
15552
|
import { spawnSync } from "child_process";
|
|
15361
15553
|
import { readFileSync as readFileSync17 } from "fs";
|
|
15362
|
-
import { join as
|
|
15554
|
+
import { join as join31 } from "path";
|
|
15363
15555
|
var COMPANION_ENTRY = /@cabane\/companion\/|cabane-companion|(^|[\s/])cabane\s|\.cabane\/(versions|current|boot-guard)/;
|
|
15364
15556
|
var START = /\sstart(\s|$)/;
|
|
15365
15557
|
function companionStarts(psOutput) {
|
|
@@ -15376,7 +15568,7 @@ function companionStarts(psOutput) {
|
|
|
15376
15568
|
}
|
|
15377
15569
|
function stateDirOf(env) {
|
|
15378
15570
|
const home = env.CABANE_COMPANION_HOME || env.HOME;
|
|
15379
|
-
return home ?
|
|
15571
|
+
return home ? join31(home, ".cabane") : null;
|
|
15380
15572
|
}
|
|
15381
15573
|
function envFromPsE(output, command) {
|
|
15382
15574
|
const line = output.replace(/\n$/, "");
|