@mnemom/mnemom 0.17.34-next.3 → 0.17.34-next.4
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/commands/agent-run.d.ts +2 -0
- package/dist/commands/agent-run.js +8 -0
- package/dist/commands/agent.js +20 -9
- package/dist/lib/runner/job.d.ts +2 -0
- package/dist/lib/runner/job.js +16 -1
- package/dist/lib/runner/outcome.js +2 -3
- package/dist/lib/session-key.d.ts +4 -3
- package/dist/vendor/durable-contract/index.js +207 -169
- package/dist/vendor/shared/durable-contract/doors.d.ts +26 -1
- package/dist/vendor/shared/durable-contract/doors.js +37 -1
- package/dist/vendor/shared/durable-contract/events.d.ts +1 -1
- package/dist/vendor/shared/durable-contract/events.js +13 -3
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +2 -2
- package/dist/vendor/shared/durable-contract/schema.d.ts +5 -0
- package/dist/vendor/shared/durable-contract/schema.js +6 -0
- package/dist/vendor/shared/durable-contract/states.d.ts +2 -1
- package/dist/vendor/shared/durable-contract/states.js +2 -1
- package/package.json +1 -1
|
@@ -42,6 +42,8 @@ export declare function liveGit(env: NodeJS.ProcessEnv): Git;
|
|
|
42
42
|
/** A fresh clone on a job branch, plus a fresh, empty HOME for the phases. */
|
|
43
43
|
export declare function prepareWorkspace(git: Git, cloneUrl: string, sessionId: string, root?: string): Promise<Workspace>;
|
|
44
44
|
export declare function cleanupWorkspace(ws: Workspace): Promise<void>;
|
|
45
|
+
/** Commits on the job branch that no origin ref has: the clone's base is origin's default branch. */
|
|
46
|
+
export declare function commitsAhead(git: Git, ws: Workspace): Promise<number>;
|
|
45
47
|
/** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
|
|
46
48
|
export declare function openPr(git: Git, fetch: FetchFn, token: string | null, repo: string, ws: Workspace, title: string, apiBase?: string): Promise<string>;
|
|
47
49
|
/** merge_on_green: GitHub's auto-merge on the PR (squash). */
|
|
@@ -138,6 +138,13 @@ async function github(fetch, token, method, path, body, apiBase) {
|
|
|
138
138
|
}
|
|
139
139
|
return { status: res.status, json };
|
|
140
140
|
}
|
|
141
|
+
/** Commits on the job branch that no origin ref has: the clone's base is origin's default branch. */
|
|
142
|
+
export async function commitsAhead(git, ws) {
|
|
143
|
+
const n = Number(await git(ws.dir, ["rev-list", "--count", "HEAD", "--not", "--remotes=origin"]));
|
|
144
|
+
if (!Number.isInteger(n) || n < 0)
|
|
145
|
+
throw new Error(`git rev-list answered ${n}`);
|
|
146
|
+
return n;
|
|
147
|
+
}
|
|
141
148
|
/** Push the job branch and open its PR against the repo's default branch; resolves its URL. */
|
|
142
149
|
export async function openPr(git, fetch, token, repo, ws, title, apiBase = GITHUB_BASES.api) {
|
|
143
150
|
await git(ws.dir, ["push", "--quiet", "origin", `HEAD:refs/heads/${ws.branch}`]);
|
|
@@ -211,6 +218,7 @@ export function liveJobDeps(api, opts) {
|
|
|
211
218
|
prepareWorkspace: (repo, sessionId) => prepareWorkspace(git, `${gh.web}/${repo}.git`, sessionId),
|
|
212
219
|
runPhase: (inv, ws, modelUrl, signal) => runPhaseProcess(inv, ws, modelUrl, signal, opts.env),
|
|
213
220
|
openPr: (repo, ws, title) => openPr(git, opts.fetch, token, repo, ws, title, gh.api),
|
|
221
|
+
commitsAhead: (ws) => commitsAhead(git, ws),
|
|
214
222
|
enableAutoMerge: (prUrl) => enableAutoMerge(opts.fetch, token, prUrl, gh),
|
|
215
223
|
cleanup: cleanupWorkspace,
|
|
216
224
|
};
|
package/dist/commands/agent.js
CHANGED
|
@@ -48,7 +48,7 @@ import { isLoggedIn } from "../lib/auth.js";
|
|
|
48
48
|
import { loadCliConfig } from "../lib/cli-config.js";
|
|
49
49
|
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
50
50
|
import { experimentalNotice } from "../lib/experimental.js";
|
|
51
|
-
import { conductReceiptFor } from "../lib/conduct-receipt.js";
|
|
51
|
+
import { conductReceiptFor, RECEIPT_DEADLINE_MS, } from "../lib/conduct-receipt.js";
|
|
52
52
|
import { fmt } from "../lib/format.js";
|
|
53
53
|
import { askInput, askSelect, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
54
54
|
import { authConfigured, chooseAuth, detectClaudeLogin, readClaudeLoginState, } from "../lib/login-detect.js";
|
|
@@ -1518,28 +1518,39 @@ function launchTerminal(ctx) {
|
|
|
1518
1518
|
child.on("exit", (code, signal) => {
|
|
1519
1519
|
void (async () => {
|
|
1520
1520
|
// MNE-8245: best-effort revoke of the minted session-scoped key — a failure
|
|
1521
|
-
// here is non-fatal (the key is 24h-TTL'd regardless)
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
}
|
|
1521
|
+
// here is non-fatal (the key is 24h-TTL'd regardless). It starts now and is awaited only
|
|
1522
|
+
// before the process ends; with a receipt to print it shares the receipt's deadline, so
|
|
1523
|
+
// with the API down the two bounds overlap instead of adding up (W1.3).
|
|
1525
1524
|
// A lock handover SIGKILLs the frozen claude once the daemon's seat runs the conversation:
|
|
1526
1525
|
// attach to it instead of dying with the child (C02-R26 step 7). Never relaunches claude.
|
|
1527
1526
|
const h = ctx.durableHandover;
|
|
1528
|
-
|
|
1527
|
+
const handover = mayBeHandover(h !== undefined, signal);
|
|
1528
|
+
// Only an exit that goes straight to the receipt shortens the revoke; a signal or a
|
|
1529
|
+
// handover prints none, so it keeps the full bound.
|
|
1530
|
+
const sharesReceiptDeadline = Boolean(ctx.receipt) && !signal && !handover;
|
|
1531
|
+
const revoked = ctx.sessionKeyId
|
|
1532
|
+
? revokeSessionGatewayKey({
|
|
1533
|
+
...defaultSessionKeyDeps(say),
|
|
1534
|
+
...(sharesReceiptDeadline ? { revokeTimeoutMs: RECEIPT_DEADLINE_MS } : {}),
|
|
1535
|
+
}, ctx.sessionKeyId)
|
|
1536
|
+
: Promise.resolve();
|
|
1537
|
+
if (handover && h) {
|
|
1538
|
+
await revoked;
|
|
1529
1539
|
const attached = await liveParentAfterChildEnded(h.row, h.name, h.launcherKey, savedTty);
|
|
1530
1540
|
if (attached !== null)
|
|
1531
1541
|
process.exit(attached);
|
|
1532
1542
|
}
|
|
1533
1543
|
if (signal) {
|
|
1544
|
+
await revoked;
|
|
1534
1545
|
process.kill(process.pid, signal);
|
|
1535
1546
|
return;
|
|
1536
1547
|
}
|
|
1537
|
-
// The conduct receipt and the last commit sweep run side by side, each under
|
|
1538
|
-
// hard deadline inside W1.3's exit budget;
|
|
1548
|
+
// The revoke, the conduct receipt and the last commit sweep run side by side, each under
|
|
1549
|
+
// its own hard deadline inside W1.3's exit budget; none changes the exit code.
|
|
1539
1550
|
const swept = commits?.finish().catch(() => undefined);
|
|
1540
1551
|
if (ctx.receipt)
|
|
1541
1552
|
await sayConductReceipt({ ...ctx.receipt, statePath: recordStatePath });
|
|
1542
|
-
await swept;
|
|
1553
|
+
await Promise.all([swept, revoked]);
|
|
1543
1554
|
// Claude Code's own last line is "Resume this session with: claude --resume <id>",
|
|
1544
1555
|
// which reopens it OUTSIDE Mnemom (ungoverned, unbilled). Ours goes after it, last.
|
|
1545
1556
|
if (code === 0 && ctx.resumeHint && wantsResumeHint(ctx.cli, args)) {
|
package/dist/lib/runner/job.d.ts
CHANGED
|
@@ -43,6 +43,8 @@ export interface JobDeps {
|
|
|
43
43
|
runPhase(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal): Promise<number>;
|
|
44
44
|
/** Push the job branch and open its PR; resolves the PR URL. */
|
|
45
45
|
openPr(repo: string, ws: Workspace, title: string): Promise<string>;
|
|
46
|
+
/** Commits on the job branch that the base does not have; 0 means there is nothing to open. */
|
|
47
|
+
commitsAhead(ws: Workspace): Promise<number>;
|
|
46
48
|
enableAutoMerge(prUrl: string): Promise<void>;
|
|
47
49
|
cleanup(ws: Workspace): Promise<void>;
|
|
48
50
|
}
|
package/dist/lib/runner/job.js
CHANGED
|
@@ -348,6 +348,12 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
348
348
|
}
|
|
349
349
|
if (opensPr) {
|
|
350
350
|
try {
|
|
351
|
+
// Checked before the push, so an empty diff never reaches GitHub's 422 (amendment 59).
|
|
352
|
+
if ((await deps.commitsAhead(ws)) === 0) {
|
|
353
|
+
deps.log("no commits ahead of the base: there is no PR to open");
|
|
354
|
+
await reportFail(deps, sessionId, claim.epoch, "runner_failed", "pr_no_changes");
|
|
355
|
+
return { kind: "outcome", outcome: failedOutcome(facts, "pr_no_changes") };
|
|
356
|
+
}
|
|
351
357
|
facts.pr_url = await deps.openPr(job.repo, ws, `mnemom: ${bundle.workflow.id} job ${sessionId.slice(0, 8)}`);
|
|
352
358
|
}
|
|
353
359
|
catch (e) {
|
|
@@ -477,8 +483,17 @@ export async function reportFail(deps, sessionId, epoch, kind, reason) {
|
|
|
477
483
|
reason: kind,
|
|
478
484
|
refs: { reason },
|
|
479
485
|
});
|
|
480
|
-
if (!ok2xx(r))
|
|
486
|
+
if (!ok2xx(r)) {
|
|
481
487
|
deps.log(`exec_fail ${kind}/${reason}: ${errorCode(r.body) ?? r.networkError ?? r.status}`);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
// Amendment 59: the api decides retry or fail; the runner only says which.
|
|
491
|
+
const b = (r.body ?? {});
|
|
492
|
+
const job = `job ${sessionId.slice(0, 8)}`;
|
|
493
|
+
if (b.state === "ready" && Number.isInteger(b.attempt) && Number.isInteger(b.retries))
|
|
494
|
+
deps.log(`${job} returned to the pool for retry (attempt ${b.attempt} of ${b.retries})`);
|
|
495
|
+
else if (b.state === "failed" && Number.isInteger(b.attempt))
|
|
496
|
+
deps.log(`${job} failed after ${b.attempt} attempt${b.attempt === 1 ? "" : "s"}`);
|
|
482
497
|
}
|
|
483
498
|
async function release(deps, sessionId, epoch) {
|
|
484
499
|
const r = await deps.api.release(sessionId, { epoch, nonce: deps.nonce });
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* a reason outside S1's closed lists is a bug in the runner, so `outcomeLine` throws on one rather
|
|
7
7
|
* than print a line no consumer can read.
|
|
8
8
|
*/
|
|
9
|
-
import {
|
|
9
|
+
import { isRunnerFailedReason, RUNNER_EXIT_CODES, RUNNER_OUTCOME_FIELDS, RUNNER_REFUSED_REASONS, } from "../../vendor/durable-contract/index.js";
|
|
10
10
|
/** `phase_<order>_<name>`: the name lowercased, anything outside [a-z0-9_] replaced by `_`. */
|
|
11
11
|
export function phaseReason(order, name) {
|
|
12
12
|
return `phase_${order}_${name.toLowerCase().replace(/[^a-z0-9_]/g, "_")}`;
|
|
@@ -20,8 +20,7 @@ function reasonOk(outcome, reason) {
|
|
|
20
20
|
if (reason === null)
|
|
21
21
|
return false;
|
|
22
22
|
if (outcome === "failed")
|
|
23
|
-
return (
|
|
24
|
-
RUNNER_FAILED_PHASE_RE.test(reason));
|
|
23
|
+
return isRunnerFailedReason(reason);
|
|
25
24
|
return isRefusedReason(reason);
|
|
26
25
|
}
|
|
27
26
|
const pick = (facts, keep) => ({
|
|
@@ -24,9 +24,10 @@ export interface SessionKeyDeps {
|
|
|
24
24
|
revoke: (keyId: string) => Promise<void>;
|
|
25
25
|
say: (line: string) => void;
|
|
26
26
|
/**
|
|
27
|
-
* Override the mint/revoke timeout
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* Override the mint/revoke timeout. Tests use it; the one real caller is the exit path of a
|
|
28
|
+
* launch that prints a conduct receipt, which bounds the revoke by the receipt's deadline
|
|
29
|
+
* (agent.ts, Doors W1.3). Otherwise the 10s/3s bounds below apply. Tests use it to exercise
|
|
30
|
+
* "never settles" against a short, fast timeout rather than waiting out the real one.
|
|
30
31
|
*/
|
|
31
32
|
mintTimeoutMs?: number;
|
|
32
33
|
revokeTimeoutMs?: number;
|
|
@@ -199,7 +199,7 @@ var SUSPENDED_REASONS = [
|
|
|
199
199
|
"approval_denied"
|
|
200
200
|
];
|
|
201
201
|
var PAUSED_REASONS = ["owner", "agent_contained", "breakglass"];
|
|
202
|
-
var FAILED_REASONS = ["transcript_lost"];
|
|
202
|
+
var FAILED_REASONS = ["transcript_lost", "runner_failed"];
|
|
203
203
|
var KILLED_REASONS = ["agent_killed"];
|
|
204
204
|
var STATE_REASONS = {
|
|
205
205
|
resuming: [null],
|
|
@@ -2034,6 +2034,183 @@ function grantWarningDays(remainingSeconds, warningWindowSeconds) {
|
|
|
2034
2034
|
return Math.ceil(remainingSeconds / 86400);
|
|
2035
2035
|
}
|
|
2036
2036
|
|
|
2037
|
+
// ../shared/durable-contract/src/doors.ts
|
|
2038
|
+
var DURABLE_POOL_CLAIM_FLAG = "DURABLE_POOL_CLAIM";
|
|
2039
|
+
function poolClaimEnabled(flagValue, orgId) {
|
|
2040
|
+
return poolClaimOrgs(flagValue).includes(orgId);
|
|
2041
|
+
}
|
|
2042
|
+
function isPoolSession(s, flagValue) {
|
|
2043
|
+
return s.home_executor_id === null && s.durable_mode === "locked" && poolClaimEnabled(flagValue, s.org_id);
|
|
2044
|
+
}
|
|
2045
|
+
function poolClaimOrgs(flagValue) {
|
|
2046
|
+
if (flagValue === void 0) return [];
|
|
2047
|
+
const orgs = flagValue.split(",").map((s) => s.trim());
|
|
2048
|
+
if (orgs.some((o) => !ID_RE.test(o))) return [];
|
|
2049
|
+
return [...new Set(orgs)];
|
|
2050
|
+
}
|
|
2051
|
+
var EXECUTOR_SCOPES = ["user", "org"];
|
|
2052
|
+
var ORG_SCOPE_EXECUTOR_KINDS = ["container", "fleet"];
|
|
2053
|
+
var OIDC_EXCHANGE_REFUSALS = [
|
|
2054
|
+
"bad_issuer",
|
|
2055
|
+
"repo_not_enrolled",
|
|
2056
|
+
"ref_not_allowed",
|
|
2057
|
+
"pool_flag_off"
|
|
2058
|
+
];
|
|
2059
|
+
var OIDC_EXCHANGE_AUDIENCE = "mnemom-executor";
|
|
2060
|
+
var ORG_POLICY_LIMIT_KEYS = ["per_session_budget_usd", "live_lease_cap"];
|
|
2061
|
+
var LIVE_LEASE_CAP_MIN = 1;
|
|
2062
|
+
var LIVE_LEASE_CAP_MAX = 1e3;
|
|
2063
|
+
var ORG_CAP_LOCK_PREFIX = "durable_org_cap:";
|
|
2064
|
+
var DEFAULT_LEASE_CAP_ENV = "DURABLE_DEFAULT_LEASE_CAP";
|
|
2065
|
+
var capOk = (v) => Number.isInteger(v) && v >= LIVE_LEASE_CAP_MIN && v <= LIVE_LEASE_CAP_MAX;
|
|
2066
|
+
function parseDefaultLeaseCap(envValue) {
|
|
2067
|
+
if (envValue === void 0 || !/^[1-9][0-9]{0,3}$/.test(envValue)) return null;
|
|
2068
|
+
const n = Number(envValue);
|
|
2069
|
+
return capOk(n) ? n : null;
|
|
2070
|
+
}
|
|
2071
|
+
function effectiveLeaseCap(orgLimits, defaultRowLimits, defaultCap) {
|
|
2072
|
+
const present = [orgLimits?.live_lease_cap, defaultRowLimits?.live_lease_cap, defaultCap].filter(
|
|
2073
|
+
(v) => v !== void 0 && v !== null
|
|
2074
|
+
);
|
|
2075
|
+
for (const v of present)
|
|
2076
|
+
if (!capOk(v)) throw new RangeError(`live lease cap out of range: ${String(v)}`);
|
|
2077
|
+
if (present.length === 0)
|
|
2078
|
+
throw new RangeError("no live lease cap: DURABLE_DEFAULT_LEASE_CAP is unset");
|
|
2079
|
+
return present[0];
|
|
2080
|
+
}
|
|
2081
|
+
var DISPATCH_SESSION_CID_PREFIX = "dispatch-";
|
|
2082
|
+
var DISPATCH_SESSION_NAME_PREFIX = "job-";
|
|
2083
|
+
var CREATE_READY_SESSION_REFUSALS = [
|
|
2084
|
+
"no_dispatch_agent",
|
|
2085
|
+
"seal_not_found",
|
|
2086
|
+
"cell_unmapped",
|
|
2087
|
+
// 34.10.1 (4): the api-built charter v1 or its envelope does not match the seal; nothing written
|
|
2088
|
+
"charter_mismatch"
|
|
2089
|
+
];
|
|
2090
|
+
var CREATE_READY_SESSION_OUTCOMES = ["created", "existing", "refused"];
|
|
2091
|
+
var CREATE_READY_SESSION_ANSWERS = Object.freeze({
|
|
2092
|
+
created: ["outcome", "session_id"],
|
|
2093
|
+
existing: ["outcome", "session_id"],
|
|
2094
|
+
refused: ["outcome", "reason"]
|
|
2095
|
+
});
|
|
2096
|
+
function createReadySessionAnswerOk(v) {
|
|
2097
|
+
if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
|
|
2098
|
+
const a = v;
|
|
2099
|
+
if (typeof a.outcome !== "string" || !Object.prototype.hasOwnProperty.call(CREATE_READY_SESSION_ANSWERS, a.outcome))
|
|
2100
|
+
return false;
|
|
2101
|
+
const keys = CREATE_READY_SESSION_ANSWERS[a.outcome];
|
|
2102
|
+
if (Object.keys(a).length !== keys.length || !keys.every((k) => k in a)) return false;
|
|
2103
|
+
return a.outcome === "refused" ? CREATE_READY_SESSION_REFUSALS.includes(a.reason) : typeof a.session_id === "string" && UUID_RE.test(a.session_id);
|
|
2104
|
+
}
|
|
2105
|
+
function sealIdOk(sealId) {
|
|
2106
|
+
if (!UUID_RE.test(sealId)) throw new RangeError(`seal id must be a lowercase uuid: ${sealId}`);
|
|
2107
|
+
return sealId;
|
|
2108
|
+
}
|
|
2109
|
+
function dispatchConversationId(sealId) {
|
|
2110
|
+
return DISPATCH_SESSION_CID_PREFIX + sealIdOk(sealId);
|
|
2111
|
+
}
|
|
2112
|
+
function dispatchSessionName(sealId) {
|
|
2113
|
+
return DISPATCH_SESSION_NAME_PREFIX + sealIdOk(sealId).replace(/-/g, "").slice(0, 12);
|
|
2114
|
+
}
|
|
2115
|
+
var RUNNER_OUTCOMES = ["done", "no_job", "failed", "refused"];
|
|
2116
|
+
var RUNNER_OUTCOME_FIELDS = [
|
|
2117
|
+
"v",
|
|
2118
|
+
"outcome",
|
|
2119
|
+
"session_id",
|
|
2120
|
+
"lease_epoch",
|
|
2121
|
+
"contract_sha256",
|
|
2122
|
+
"contract_version",
|
|
2123
|
+
"merge_strategy",
|
|
2124
|
+
"pr_url",
|
|
2125
|
+
"reason"
|
|
2126
|
+
];
|
|
2127
|
+
var RUNNER_EXIT_CODES = Object.freeze({
|
|
2128
|
+
done: 0,
|
|
2129
|
+
no_job: 0,
|
|
2130
|
+
failed: 1,
|
|
2131
|
+
usage: 2,
|
|
2132
|
+
refused: 3
|
|
2133
|
+
});
|
|
2134
|
+
var RUNNER_FAILED_REASONS = ["pr_open_failed", "pr_no_changes"];
|
|
2135
|
+
var RUNNER_FAILED_PHASE_RE = /^phase_[0-9]+_[a-z0-9_]+$/;
|
|
2136
|
+
var RUNNER_FAIL_REFS = ["reason"];
|
|
2137
|
+
var RUNNER_FAILED_REASON_PATTERN = `^(${RUNNER_FAILED_REASONS.join("|")}|phase_[0-9]+_[a-z0-9_]+)$`;
|
|
2138
|
+
function isRunnerFailedReason(v) {
|
|
2139
|
+
return typeof v === "string" && (RUNNER_FAILED_REASONS.includes(v) || RUNNER_FAILED_PHASE_RE.test(v));
|
|
2140
|
+
}
|
|
2141
|
+
var POOL_JOB_RETRIES_DEFAULT = 1;
|
|
2142
|
+
var POOL_JOB_RETRIES_MIN = 0;
|
|
2143
|
+
var POOL_JOB_RETRIES_MAX = 5;
|
|
2144
|
+
var POOL_JOB_RETRIES_CONFIG_CAUSE = "pool_job_retries_config";
|
|
2145
|
+
function poolJobRetriesOf(limit) {
|
|
2146
|
+
if (limit === void 0 || limit === null) return POOL_JOB_RETRIES_DEFAULT;
|
|
2147
|
+
return Number.isInteger(limit) && limit >= POOL_JOB_RETRIES_MIN && limit <= POOL_JOB_RETRIES_MAX ? limit : null;
|
|
2148
|
+
}
|
|
2149
|
+
var RUNNER_REFUSED_REASONS = [
|
|
2150
|
+
"bundle_too_large",
|
|
2151
|
+
"bundle_malformed",
|
|
2152
|
+
"bundle_wrong_typ",
|
|
2153
|
+
"bundle_schema",
|
|
2154
|
+
"bundle_unknown_key",
|
|
2155
|
+
"bundle_unusable_key",
|
|
2156
|
+
"bundle_retired_key",
|
|
2157
|
+
"bundle_bad_signature",
|
|
2158
|
+
"bundle_code_digest_mismatch",
|
|
2159
|
+
"bundle_not_yet_valid",
|
|
2160
|
+
"bundle_expired",
|
|
2161
|
+
"bundle_binding_mismatch",
|
|
2162
|
+
"bundle_unavailable",
|
|
2163
|
+
"job_unavailable",
|
|
2164
|
+
"contract_bad_sha",
|
|
2165
|
+
"contract_too_large",
|
|
2166
|
+
"contract_sha_mismatch",
|
|
2167
|
+
"contract_not_a_contract",
|
|
2168
|
+
"contract_mismatch_unrecoverable",
|
|
2169
|
+
"claim_needs_key",
|
|
2170
|
+
"claim_ineligible",
|
|
2171
|
+
"claim_unavailable",
|
|
2172
|
+
"lease_lost",
|
|
2173
|
+
"executor_revoked",
|
|
2174
|
+
"executor_unauthorized",
|
|
2175
|
+
"oidc_bad_issuer",
|
|
2176
|
+
"oidc_repo_not_enrolled",
|
|
2177
|
+
"oidc_ref_not_allowed",
|
|
2178
|
+
"oidc_pool_flag_off",
|
|
2179
|
+
"oidc_unavailable",
|
|
2180
|
+
"api_unavailable"
|
|
2181
|
+
];
|
|
2182
|
+
var RUNNER_OPEN_MODE_TOLERATED = [
|
|
2183
|
+
"bundle_unavailable",
|
|
2184
|
+
"job_unavailable",
|
|
2185
|
+
"api_unavailable"
|
|
2186
|
+
];
|
|
2187
|
+
var RUNNER_MERGE_STRATEGIES = ["external", "merge_on_green"];
|
|
2188
|
+
var POOL_CLAIM_FIELDS = ["contract_sha256", "contract_version", "cap"];
|
|
2189
|
+
var HEX64_RE2 = /^[0-9a-f]{64}$/;
|
|
2190
|
+
function poolClaimFieldsOk(body, pool) {
|
|
2191
|
+
if (!pool) return POOL_CLAIM_FIELDS.every((k) => !(k in body));
|
|
2192
|
+
return typeof body.contract_sha256 === "string" && HEX64_RE2.test(body.contract_sha256) && Number.isInteger(body.contract_version) && body.contract_version >= 1 && capOk(body.cap);
|
|
2193
|
+
}
|
|
2194
|
+
var WAIT_REF_KEYS = Object.freeze({
|
|
2195
|
+
ci: ["repo_id", "head_sha"],
|
|
2196
|
+
question: ["approval_id"],
|
|
2197
|
+
budget: ["approval_id"]
|
|
2198
|
+
});
|
|
2199
|
+
var HEX40_RE = /^[0-9a-f]{40}$/;
|
|
2200
|
+
function waitRefOk(reason, ref) {
|
|
2201
|
+
if (!Object.prototype.hasOwnProperty.call(WAIT_REF_KEYS, reason)) return false;
|
|
2202
|
+
if (ref === null || typeof ref !== "object" || Array.isArray(ref)) return false;
|
|
2203
|
+
const r = ref;
|
|
2204
|
+
const want = WAIT_REF_KEYS[reason];
|
|
2205
|
+
if (Object.keys(r).length !== want.length || !want.every((k) => k in r)) return false;
|
|
2206
|
+
if (reason === "ci")
|
|
2207
|
+
return Number.isSafeInteger(r.repo_id) && r.repo_id > 0 && typeof r.head_sha === "string" && HEX40_RE.test(r.head_sha);
|
|
2208
|
+
return typeof r.approval_id === "string" && UUID_RE.test(r.approval_id);
|
|
2209
|
+
}
|
|
2210
|
+
var CLEAR_WAIT_OUTCOMES = ["cleared", "stale_ref", "already_clear"];
|
|
2211
|
+
var READY_SESSIONS_LIMIT_MIN = 1;
|
|
2212
|
+
var READY_SESSIONS_LIMIT_MAX = 50;
|
|
2213
|
+
|
|
2037
2214
|
// ../shared/durable-contract/src/events.ts
|
|
2038
2215
|
var EVENT_GROUPS = [
|
|
2039
2216
|
"lifecycle",
|
|
@@ -2337,6 +2514,7 @@ var ENUM = (values) => ({
|
|
|
2337
2514
|
});
|
|
2338
2515
|
var obj = (properties, required = Object.keys(properties), if_then) => if_then ? { type: "object", additionalProperties: false, required, properties, if_then } : { type: "object", additionalProperties: false, required, properties };
|
|
2339
2516
|
var EMPTY = obj({});
|
|
2517
|
+
var RUNNER_REASON = S({ pattern: RUNNER_FAILED_REASON_PATTERN });
|
|
2340
2518
|
var APPROVAL_KINDS = [
|
|
2341
2519
|
"question",
|
|
2342
2520
|
"ask",
|
|
@@ -2347,7 +2525,7 @@ var APPROVAL_KINDS = [
|
|
|
2347
2525
|
"grant_renewal",
|
|
2348
2526
|
"limit_raise"
|
|
2349
2527
|
];
|
|
2350
|
-
var READY_CAUSES = ["dispatched", "lease_expired", "wait_cleared"];
|
|
2528
|
+
var READY_CAUSES = ["dispatched", "lease_expired", "wait_cleared", "job_failed"];
|
|
2351
2529
|
var OWNER_MUTATION_SUBJECTS = [
|
|
2352
2530
|
"unlock",
|
|
2353
2531
|
"unpause_contained",
|
|
@@ -2404,7 +2582,15 @@ var KIND_REFS = {
|
|
|
2404
2582
|
}
|
|
2405
2583
|
]
|
|
2406
2584
|
),
|
|
2407
|
-
|
|
2585
|
+
// S1 amendment 59: runner_reason is required with runner_failed and absent otherwise.
|
|
2586
|
+
"session.failed": obj(
|
|
2587
|
+
{ reason: ENUM([...FAILED_REASONS]), runner_reason: RUNNER_REASON },
|
|
2588
|
+
["reason"],
|
|
2589
|
+
[
|
|
2590
|
+
{ field: "reason", equals: "runner_failed", require: ["runner_reason"] },
|
|
2591
|
+
{ field: "reason", equals: "transcript_lost", forbid: ["runner_reason"] }
|
|
2592
|
+
]
|
|
2593
|
+
),
|
|
2408
2594
|
"session.killed": obj({ reason: ENUM(["owner", "agent_killed"]) }),
|
|
2409
2595
|
"seat.hello": obj({ spawn_id: UUID }),
|
|
2410
2596
|
"charter.sealed": obj({
|
|
@@ -2477,7 +2663,14 @@ var KIND_REFS = {
|
|
|
2477
2663
|
"containment.kill": obj({ action: ENUM(["kill"]) }),
|
|
2478
2664
|
"never.held": obj({ sh_decision_id: UUID }),
|
|
2479
2665
|
// ---- Doors amendment (rulings 34.02, 34.06) ----
|
|
2480
|
-
|
|
2666
|
+
// S1 amendment 59: a retried job's ready names the runner's reason and the attempt it ends.
|
|
2667
|
+
"session.ready": obj(
|
|
2668
|
+
{ cause: ENUM(READY_CAUSES), runner_reason: RUNNER_REASON, attempt: INT(1) },
|
|
2669
|
+
["cause"],
|
|
2670
|
+
READY_CAUSES.map(
|
|
2671
|
+
(cause) => cause === "job_failed" ? { field: "cause", equals: cause, require: ["runner_reason", "attempt"] } : { field: "cause", equals: cause, forbid: ["runner_reason", "attempt"] }
|
|
2672
|
+
)
|
|
2673
|
+
),
|
|
2481
2674
|
"session.waiting": obj(
|
|
2482
2675
|
{
|
|
2483
2676
|
wait_reason: ENUM(PARKED_ONLY_WAIT_REASONS),
|
|
@@ -3457,7 +3650,8 @@ var M1_COLUMNS = {
|
|
|
3457
3650
|
"created_at",
|
|
3458
3651
|
"updated_at",
|
|
3459
3652
|
"wait_ref",
|
|
3460
|
-
"new_session"
|
|
3653
|
+
"new_session",
|
|
3654
|
+
"job_failures"
|
|
3461
3655
|
],
|
|
3462
3656
|
durable_lease_keys: [
|
|
3463
3657
|
"key_hash",
|
|
@@ -4430,170 +4624,6 @@ function gatewayEventsBodyOk(v) {
|
|
|
4430
4624
|
return validateRefs(kind, v.refs ?? {});
|
|
4431
4625
|
}
|
|
4432
4626
|
|
|
4433
|
-
// ../shared/durable-contract/src/doors.ts
|
|
4434
|
-
var DURABLE_POOL_CLAIM_FLAG = "DURABLE_POOL_CLAIM";
|
|
4435
|
-
function poolClaimEnabled(flagValue, orgId) {
|
|
4436
|
-
return poolClaimOrgs(flagValue).includes(orgId);
|
|
4437
|
-
}
|
|
4438
|
-
function isPoolSession(s, flagValue) {
|
|
4439
|
-
return s.home_executor_id === null && s.durable_mode === "locked" && poolClaimEnabled(flagValue, s.org_id);
|
|
4440
|
-
}
|
|
4441
|
-
function poolClaimOrgs(flagValue) {
|
|
4442
|
-
if (flagValue === void 0) return [];
|
|
4443
|
-
const orgs = flagValue.split(",").map((s) => s.trim());
|
|
4444
|
-
if (orgs.some((o) => !ID_RE.test(o))) return [];
|
|
4445
|
-
return [...new Set(orgs)];
|
|
4446
|
-
}
|
|
4447
|
-
var EXECUTOR_SCOPES = ["user", "org"];
|
|
4448
|
-
var ORG_SCOPE_EXECUTOR_KINDS = ["container", "fleet"];
|
|
4449
|
-
var OIDC_EXCHANGE_REFUSALS = [
|
|
4450
|
-
"bad_issuer",
|
|
4451
|
-
"repo_not_enrolled",
|
|
4452
|
-
"ref_not_allowed",
|
|
4453
|
-
"pool_flag_off"
|
|
4454
|
-
];
|
|
4455
|
-
var OIDC_EXCHANGE_AUDIENCE = "mnemom-executor";
|
|
4456
|
-
var ORG_POLICY_LIMIT_KEYS = ["per_session_budget_usd", "live_lease_cap"];
|
|
4457
|
-
var LIVE_LEASE_CAP_MIN = 1;
|
|
4458
|
-
var LIVE_LEASE_CAP_MAX = 1e3;
|
|
4459
|
-
var ORG_CAP_LOCK_PREFIX = "durable_org_cap:";
|
|
4460
|
-
var DEFAULT_LEASE_CAP_ENV = "DURABLE_DEFAULT_LEASE_CAP";
|
|
4461
|
-
var capOk = (v) => Number.isInteger(v) && v >= LIVE_LEASE_CAP_MIN && v <= LIVE_LEASE_CAP_MAX;
|
|
4462
|
-
function parseDefaultLeaseCap(envValue) {
|
|
4463
|
-
if (envValue === void 0 || !/^[1-9][0-9]{0,3}$/.test(envValue)) return null;
|
|
4464
|
-
const n = Number(envValue);
|
|
4465
|
-
return capOk(n) ? n : null;
|
|
4466
|
-
}
|
|
4467
|
-
function effectiveLeaseCap(orgLimits, defaultRowLimits, defaultCap) {
|
|
4468
|
-
const present = [orgLimits?.live_lease_cap, defaultRowLimits?.live_lease_cap, defaultCap].filter(
|
|
4469
|
-
(v) => v !== void 0 && v !== null
|
|
4470
|
-
);
|
|
4471
|
-
for (const v of present)
|
|
4472
|
-
if (!capOk(v)) throw new RangeError(`live lease cap out of range: ${String(v)}`);
|
|
4473
|
-
if (present.length === 0)
|
|
4474
|
-
throw new RangeError("no live lease cap: DURABLE_DEFAULT_LEASE_CAP is unset");
|
|
4475
|
-
return present[0];
|
|
4476
|
-
}
|
|
4477
|
-
var DISPATCH_SESSION_CID_PREFIX = "dispatch-";
|
|
4478
|
-
var DISPATCH_SESSION_NAME_PREFIX = "job-";
|
|
4479
|
-
var CREATE_READY_SESSION_REFUSALS = [
|
|
4480
|
-
"no_dispatch_agent",
|
|
4481
|
-
"seal_not_found",
|
|
4482
|
-
"cell_unmapped",
|
|
4483
|
-
// 34.10.1 (4): the api-built charter v1 or its envelope does not match the seal; nothing written
|
|
4484
|
-
"charter_mismatch"
|
|
4485
|
-
];
|
|
4486
|
-
var CREATE_READY_SESSION_OUTCOMES = ["created", "existing", "refused"];
|
|
4487
|
-
var CREATE_READY_SESSION_ANSWERS = Object.freeze({
|
|
4488
|
-
created: ["outcome", "session_id"],
|
|
4489
|
-
existing: ["outcome", "session_id"],
|
|
4490
|
-
refused: ["outcome", "reason"]
|
|
4491
|
-
});
|
|
4492
|
-
function createReadySessionAnswerOk(v) {
|
|
4493
|
-
if (v === null || typeof v !== "object" || Array.isArray(v)) return false;
|
|
4494
|
-
const a = v;
|
|
4495
|
-
if (typeof a.outcome !== "string" || !Object.prototype.hasOwnProperty.call(CREATE_READY_SESSION_ANSWERS, a.outcome))
|
|
4496
|
-
return false;
|
|
4497
|
-
const keys = CREATE_READY_SESSION_ANSWERS[a.outcome];
|
|
4498
|
-
if (Object.keys(a).length !== keys.length || !keys.every((k) => k in a)) return false;
|
|
4499
|
-
return a.outcome === "refused" ? CREATE_READY_SESSION_REFUSALS.includes(a.reason) : typeof a.session_id === "string" && UUID_RE.test(a.session_id);
|
|
4500
|
-
}
|
|
4501
|
-
function sealIdOk(sealId) {
|
|
4502
|
-
if (!UUID_RE.test(sealId)) throw new RangeError(`seal id must be a lowercase uuid: ${sealId}`);
|
|
4503
|
-
return sealId;
|
|
4504
|
-
}
|
|
4505
|
-
function dispatchConversationId(sealId) {
|
|
4506
|
-
return DISPATCH_SESSION_CID_PREFIX + sealIdOk(sealId);
|
|
4507
|
-
}
|
|
4508
|
-
function dispatchSessionName(sealId) {
|
|
4509
|
-
return DISPATCH_SESSION_NAME_PREFIX + sealIdOk(sealId).replace(/-/g, "").slice(0, 12);
|
|
4510
|
-
}
|
|
4511
|
-
var RUNNER_OUTCOMES = ["done", "no_job", "failed", "refused"];
|
|
4512
|
-
var RUNNER_OUTCOME_FIELDS = [
|
|
4513
|
-
"v",
|
|
4514
|
-
"outcome",
|
|
4515
|
-
"session_id",
|
|
4516
|
-
"lease_epoch",
|
|
4517
|
-
"contract_sha256",
|
|
4518
|
-
"contract_version",
|
|
4519
|
-
"merge_strategy",
|
|
4520
|
-
"pr_url",
|
|
4521
|
-
"reason"
|
|
4522
|
-
];
|
|
4523
|
-
var RUNNER_EXIT_CODES = Object.freeze({
|
|
4524
|
-
done: 0,
|
|
4525
|
-
no_job: 0,
|
|
4526
|
-
failed: 1,
|
|
4527
|
-
usage: 2,
|
|
4528
|
-
refused: 3
|
|
4529
|
-
});
|
|
4530
|
-
var RUNNER_FAILED_REASONS = ["pr_open_failed"];
|
|
4531
|
-
var RUNNER_FAILED_PHASE_RE = /^phase_[0-9]+_[a-z0-9_]+$/;
|
|
4532
|
-
var RUNNER_REFUSED_REASONS = [
|
|
4533
|
-
"bundle_too_large",
|
|
4534
|
-
"bundle_malformed",
|
|
4535
|
-
"bundle_wrong_typ",
|
|
4536
|
-
"bundle_schema",
|
|
4537
|
-
"bundle_unknown_key",
|
|
4538
|
-
"bundle_unusable_key",
|
|
4539
|
-
"bundle_retired_key",
|
|
4540
|
-
"bundle_bad_signature",
|
|
4541
|
-
"bundle_code_digest_mismatch",
|
|
4542
|
-
"bundle_not_yet_valid",
|
|
4543
|
-
"bundle_expired",
|
|
4544
|
-
"bundle_binding_mismatch",
|
|
4545
|
-
"bundle_unavailable",
|
|
4546
|
-
"job_unavailable",
|
|
4547
|
-
"contract_bad_sha",
|
|
4548
|
-
"contract_too_large",
|
|
4549
|
-
"contract_sha_mismatch",
|
|
4550
|
-
"contract_not_a_contract",
|
|
4551
|
-
"contract_mismatch_unrecoverable",
|
|
4552
|
-
"claim_needs_key",
|
|
4553
|
-
"claim_ineligible",
|
|
4554
|
-
"claim_unavailable",
|
|
4555
|
-
"lease_lost",
|
|
4556
|
-
"executor_revoked",
|
|
4557
|
-
"executor_unauthorized",
|
|
4558
|
-
"oidc_bad_issuer",
|
|
4559
|
-
"oidc_repo_not_enrolled",
|
|
4560
|
-
"oidc_ref_not_allowed",
|
|
4561
|
-
"oidc_pool_flag_off",
|
|
4562
|
-
"oidc_unavailable",
|
|
4563
|
-
"api_unavailable"
|
|
4564
|
-
];
|
|
4565
|
-
var RUNNER_OPEN_MODE_TOLERATED = [
|
|
4566
|
-
"bundle_unavailable",
|
|
4567
|
-
"job_unavailable",
|
|
4568
|
-
"api_unavailable"
|
|
4569
|
-
];
|
|
4570
|
-
var RUNNER_MERGE_STRATEGIES = ["external", "merge_on_green"];
|
|
4571
|
-
var POOL_CLAIM_FIELDS = ["contract_sha256", "contract_version", "cap"];
|
|
4572
|
-
var HEX64_RE2 = /^[0-9a-f]{64}$/;
|
|
4573
|
-
function poolClaimFieldsOk(body, pool) {
|
|
4574
|
-
if (!pool) return POOL_CLAIM_FIELDS.every((k) => !(k in body));
|
|
4575
|
-
return typeof body.contract_sha256 === "string" && HEX64_RE2.test(body.contract_sha256) && Number.isInteger(body.contract_version) && body.contract_version >= 1 && capOk(body.cap);
|
|
4576
|
-
}
|
|
4577
|
-
var WAIT_REF_KEYS = Object.freeze({
|
|
4578
|
-
ci: ["repo_id", "head_sha"],
|
|
4579
|
-
question: ["approval_id"],
|
|
4580
|
-
budget: ["approval_id"]
|
|
4581
|
-
});
|
|
4582
|
-
var HEX40_RE = /^[0-9a-f]{40}$/;
|
|
4583
|
-
function waitRefOk(reason, ref) {
|
|
4584
|
-
if (!Object.prototype.hasOwnProperty.call(WAIT_REF_KEYS, reason)) return false;
|
|
4585
|
-
if (ref === null || typeof ref !== "object" || Array.isArray(ref)) return false;
|
|
4586
|
-
const r = ref;
|
|
4587
|
-
const want = WAIT_REF_KEYS[reason];
|
|
4588
|
-
if (Object.keys(r).length !== want.length || !want.every((k) => k in r)) return false;
|
|
4589
|
-
if (reason === "ci")
|
|
4590
|
-
return Number.isSafeInteger(r.repo_id) && r.repo_id > 0 && typeof r.head_sha === "string" && HEX40_RE.test(r.head_sha);
|
|
4591
|
-
return typeof r.approval_id === "string" && UUID_RE.test(r.approval_id);
|
|
4592
|
-
}
|
|
4593
|
-
var CLEAR_WAIT_OUTCOMES = ["cleared", "stale_ref", "already_clear"];
|
|
4594
|
-
var READY_SESSIONS_LIMIT_MIN = 1;
|
|
4595
|
-
var READY_SESSIONS_LIMIT_MAX = 50;
|
|
4596
|
-
|
|
4597
4627
|
// ../shared/durable-contract/src/journal-approvals.ts
|
|
4598
4628
|
var JOURNAL_APPROVAL_KINDS = ["approval.resolved", "approval.consumed"];
|
|
4599
4629
|
var idOf = (e2) => e2.refs !== null && typeof e2.refs.approval_id === "string" ? e2.refs.approval_id : null;
|
|
@@ -5584,6 +5614,10 @@ export {
|
|
|
5584
5614
|
PLUGIN_MANIFEST_SCHEMA,
|
|
5585
5615
|
PLUGIN_TOKEN,
|
|
5586
5616
|
POOL_CLAIM_FIELDS,
|
|
5617
|
+
POOL_JOB_RETRIES_CONFIG_CAUSE,
|
|
5618
|
+
POOL_JOB_RETRIES_DEFAULT,
|
|
5619
|
+
POOL_JOB_RETRIES_MAX,
|
|
5620
|
+
POOL_JOB_RETRIES_MIN,
|
|
5587
5621
|
POSTURES,
|
|
5588
5622
|
PRE_M1_LAUNCHER_SUBVERBS,
|
|
5589
5623
|
PRINCIPAL_REF_VECTOR,
|
|
@@ -5620,6 +5654,8 @@ export {
|
|
|
5620
5654
|
RUNNER_EXIT_CODES,
|
|
5621
5655
|
RUNNER_FAILED_PHASE_RE,
|
|
5622
5656
|
RUNNER_FAILED_REASONS,
|
|
5657
|
+
RUNNER_FAILED_REASON_PATTERN,
|
|
5658
|
+
RUNNER_FAIL_REFS,
|
|
5623
5659
|
RUNNER_MERGE_STRATEGIES,
|
|
5624
5660
|
RUNNER_OPEN_MODE_TOLERATED,
|
|
5625
5661
|
RUNNER_OUTCOMES,
|
|
@@ -5794,6 +5830,7 @@ export {
|
|
|
5794
5830
|
isLoopbackUrl,
|
|
5795
5831
|
isPoolSession,
|
|
5796
5832
|
isRuled,
|
|
5833
|
+
isRunnerFailedReason,
|
|
5797
5834
|
isSessionState,
|
|
5798
5835
|
isTerminalState,
|
|
5799
5836
|
isTestAllowlisted,
|
|
@@ -5831,6 +5868,7 @@ export {
|
|
|
5831
5868
|
poolClaimEnabled,
|
|
5832
5869
|
poolClaimFieldsOk,
|
|
5833
5870
|
poolClaimOrgs,
|
|
5871
|
+
poolJobRetriesOf,
|
|
5834
5872
|
principalRef,
|
|
5835
5873
|
protectionRefusals,
|
|
5836
5874
|
proxyLockPath,
|
|
@@ -87,8 +87,33 @@ export declare const RUNNER_EXIT_CODES: Readonly<{
|
|
|
87
87
|
readonly refused: 3;
|
|
88
88
|
}>;
|
|
89
89
|
/** failed reasons besides the phase pattern RUNNER_FAILED_PHASE_RE. */
|
|
90
|
-
export declare const RUNNER_FAILED_REASONS: readonly ["pr_open_failed"];
|
|
90
|
+
export declare const RUNNER_FAILED_REASONS: readonly ["pr_open_failed", "pr_no_changes"];
|
|
91
91
|
export declare const RUNNER_FAILED_PHASE_RE: RegExp;
|
|
92
|
+
/**
|
|
93
|
+
* S1 amendment 59 (signed by Wassim 2026-10-31): a failed pool job is retried, then failed with the
|
|
94
|
+
* runner's reason. exec_fail {reason: "runner_failed"} carries exactly `refs: {reason}`, where
|
|
95
|
+
* refs.reason passes isRunnerFailedReason; anything else is 400 durable_reason_invalid
|
|
96
|
+
* {field: "refs.reason"} with nothing written. `pr_no_changes` is found before any push (no commit
|
|
97
|
+
* ahead of the base) and is retried like any other failure.
|
|
98
|
+
*/
|
|
99
|
+
export declare const RUNNER_FAIL_REFS: readonly ["reason"];
|
|
100
|
+
/** RUNNER_FAILED_REASONS and RUNNER_FAILED_PHASE_RE as one anchored pattern (the refs schemas'). */
|
|
101
|
+
export declare const RUNNER_FAILED_REASON_PATTERN: string;
|
|
102
|
+
/** 59 (2): the single check the CLI, the api and E4 share for a runner's failed reason. */
|
|
103
|
+
export declare function isRunnerFailedReason(v: unknown): boolean;
|
|
104
|
+
/**
|
|
105
|
+
* 59 (3): how many times a failed pool job goes back to ready before it is failed. Configured per
|
|
106
|
+
* org as organizations.limits.pool_job_retries (operating policy, never the charter); absent means
|
|
107
|
+
* the default, and a present value outside MIN..MAX fails closed (503 durable_rpc_unavailable
|
|
108
|
+
* {cause: "pool_job_retries_config"}, nothing written), as 34.05.1 (3) does for the lease cap. A
|
|
109
|
+
* lease lapse never spends a retry: it stays resume_failures / respawn_failed.
|
|
110
|
+
*/
|
|
111
|
+
export declare const POOL_JOB_RETRIES_DEFAULT = 1;
|
|
112
|
+
export declare const POOL_JOB_RETRIES_MIN = 0;
|
|
113
|
+
export declare const POOL_JOB_RETRIES_MAX = 5;
|
|
114
|
+
export declare const POOL_JOB_RETRIES_CONFIG_CAUSE = "pool_job_retries_config";
|
|
115
|
+
/** The org's retry count from its limits.pool_job_retries: the default when absent, null when invalid. */
|
|
116
|
+
export declare function poolJobRetriesOf(limit: unknown): number | null;
|
|
92
117
|
/** The closed refused list (34.12 (8)). */
|
|
93
118
|
export declare const RUNNER_REFUSED_REASONS: readonly ["bundle_too_large", "bundle_malformed", "bundle_wrong_typ", "bundle_schema", "bundle_unknown_key", "bundle_unusable_key", "bundle_retired_key", "bundle_bad_signature", "bundle_code_digest_mismatch", "bundle_not_yet_valid", "bundle_expired", "bundle_binding_mismatch", "bundle_unavailable", "job_unavailable", "contract_bad_sha", "contract_too_large", "contract_sha_mismatch", "contract_not_a_contract", "contract_mismatch_unrecoverable", "claim_needs_key", "claim_ineligible", "claim_unavailable", "lease_lost", "executor_revoked", "executor_unauthorized", "oidc_bad_issuer", "oidc_repo_not_enrolled", "oidc_ref_not_allowed", "oidc_pool_flag_off", "oidc_unavailable", "api_unavailable"];
|
|
94
119
|
/** Under --fail-mode open, only these may be ridden through, and only once a verified bundle is held (34.12 (2)). */
|
|
@@ -154,8 +154,44 @@ export const RUNNER_EXIT_CODES = Object.freeze({
|
|
|
154
154
|
refused: 3,
|
|
155
155
|
});
|
|
156
156
|
/** failed reasons besides the phase pattern RUNNER_FAILED_PHASE_RE. */
|
|
157
|
-
export const RUNNER_FAILED_REASONS = ["pr_open_failed"];
|
|
157
|
+
export const RUNNER_FAILED_REASONS = ["pr_open_failed", "pr_no_changes"];
|
|
158
158
|
export const RUNNER_FAILED_PHASE_RE = /^phase_[0-9]+_[a-z0-9_]+$/;
|
|
159
|
+
/**
|
|
160
|
+
* S1 amendment 59 (signed by Wassim 2026-10-31): a failed pool job is retried, then failed with the
|
|
161
|
+
* runner's reason. exec_fail {reason: "runner_failed"} carries exactly `refs: {reason}`, where
|
|
162
|
+
* refs.reason passes isRunnerFailedReason; anything else is 400 durable_reason_invalid
|
|
163
|
+
* {field: "refs.reason"} with nothing written. `pr_no_changes` is found before any push (no commit
|
|
164
|
+
* ahead of the base) and is retried like any other failure.
|
|
165
|
+
*/
|
|
166
|
+
export const RUNNER_FAIL_REFS = ["reason"];
|
|
167
|
+
/** RUNNER_FAILED_REASONS and RUNNER_FAILED_PHASE_RE as one anchored pattern (the refs schemas'). */
|
|
168
|
+
export const RUNNER_FAILED_REASON_PATTERN = `^(${RUNNER_FAILED_REASONS.join("|")}|phase_[0-9]+_[a-z0-9_]+)$`;
|
|
169
|
+
/** 59 (2): the single check the CLI, the api and E4 share for a runner's failed reason. */
|
|
170
|
+
export function isRunnerFailedReason(v) {
|
|
171
|
+
return (typeof v === "string" &&
|
|
172
|
+
(RUNNER_FAILED_REASONS.includes(v) || RUNNER_FAILED_PHASE_RE.test(v)));
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* 59 (3): how many times a failed pool job goes back to ready before it is failed. Configured per
|
|
176
|
+
* org as organizations.limits.pool_job_retries (operating policy, never the charter); absent means
|
|
177
|
+
* the default, and a present value outside MIN..MAX fails closed (503 durable_rpc_unavailable
|
|
178
|
+
* {cause: "pool_job_retries_config"}, nothing written), as 34.05.1 (3) does for the lease cap. A
|
|
179
|
+
* lease lapse never spends a retry: it stays resume_failures / respawn_failed.
|
|
180
|
+
*/
|
|
181
|
+
export const POOL_JOB_RETRIES_DEFAULT = 1;
|
|
182
|
+
export const POOL_JOB_RETRIES_MIN = 0;
|
|
183
|
+
export const POOL_JOB_RETRIES_MAX = 5;
|
|
184
|
+
export const POOL_JOB_RETRIES_CONFIG_CAUSE = "pool_job_retries_config";
|
|
185
|
+
/** The org's retry count from its limits.pool_job_retries: the default when absent, null when invalid. */
|
|
186
|
+
export function poolJobRetriesOf(limit) {
|
|
187
|
+
if (limit === undefined || limit === null)
|
|
188
|
+
return POOL_JOB_RETRIES_DEFAULT;
|
|
189
|
+
return Number.isInteger(limit) &&
|
|
190
|
+
limit >= POOL_JOB_RETRIES_MIN &&
|
|
191
|
+
limit <= POOL_JOB_RETRIES_MAX
|
|
192
|
+
? limit
|
|
193
|
+
: null;
|
|
194
|
+
}
|
|
159
195
|
/** The closed refused list (34.12 (8)). */
|
|
160
196
|
export const RUNNER_REFUSED_REASONS = [
|
|
161
197
|
"bundle_too_large",
|
|
@@ -90,7 +90,7 @@ export interface RefsSchema {
|
|
|
90
90
|
/** durable_approvals.kind values, as the approval.* refs enums carry them. */
|
|
91
91
|
export declare const APPROVAL_KINDS: readonly ["question", "ask", "tool_confirmation", "intent_rerun", "card_drift", "owner_mutation", "grant_renewal", "limit_raise"];
|
|
92
92
|
/** session.ready refs.cause: how a session entered ready (ruling 34.02 (3)). */
|
|
93
|
-
export declare const READY_CAUSES: readonly ["dispatched", "lease_expired", "wait_cleared"];
|
|
93
|
+
export declare const READY_CAUSES: readonly ["dispatched", "lease_expired", "wait_cleared", "job_failed"];
|
|
94
94
|
/** refs_schema for every M1 kind; kinds without a specific schema take EMPTY (refs must be {}). */
|
|
95
95
|
export declare function refsSchemaOf(kind: DurableEventKind): RefsSchema;
|
|
96
96
|
/** One refs property against its schema (pure; exported for its direct test). An integer with no minimum accepts any safe integer. */
|
|
@@ -25,6 +25,7 @@ import { canonicalJson, hmacSha256Hex, sha256Hex, HEX64_RE } from "./canonical.j
|
|
|
25
25
|
import { DETACHED_REASONS, PAUSED_REASONS, SUSPENDED_REASONS, FAILED_REASONS, PARKED_ONLY_WAIT_REASONS, } from "./states.js";
|
|
26
26
|
import { ATTESTATION_CHECKS_M1, CWD_CHECK_CAUSES } from "./codes.js";
|
|
27
27
|
import { EVENT_DETAIL_MAX_CHARS } from "./constants.js";
|
|
28
|
+
import { RUNNER_FAILED_REASON_PATTERN } from "./doors.js";
|
|
28
29
|
export const EVENT_GROUPS = [
|
|
29
30
|
"lifecycle",
|
|
30
31
|
"charter",
|
|
@@ -352,6 +353,8 @@ const obj = (properties, required = Object.keys(properties), if_then) => if_then
|
|
|
352
353
|
? { type: "object", additionalProperties: false, required, properties, if_then }
|
|
353
354
|
: { type: "object", additionalProperties: false, required, properties };
|
|
354
355
|
const EMPTY = obj({});
|
|
356
|
+
/** A runner's failed reason (S1 amendment 59): RUNNER_FAILED_REASONS or the phase pattern. */
|
|
357
|
+
const RUNNER_REASON = S({ pattern: RUNNER_FAILED_REASON_PATTERN });
|
|
355
358
|
/** durable_approvals.kind values, as the approval.* refs enums carry them. */
|
|
356
359
|
export const APPROVAL_KINDS = [
|
|
357
360
|
"question",
|
|
@@ -364,7 +367,7 @@ export const APPROVAL_KINDS = [
|
|
|
364
367
|
"limit_raise",
|
|
365
368
|
];
|
|
366
369
|
/** session.ready refs.cause: how a session entered ready (ruling 34.02 (3)). */
|
|
367
|
-
export const READY_CAUSES = ["dispatched", "lease_expired", "wait_cleared"];
|
|
370
|
+
export const READY_CAUSES = ["dispatched", "lease_expired", "wait_cleared", "job_failed"];
|
|
368
371
|
// question joins the M1 four (ruling 34.06.1.1): its wait parks on an owner_mutation approval.
|
|
369
372
|
const OWNER_MUTATION_SUBJECTS = [
|
|
370
373
|
"unlock",
|
|
@@ -415,7 +418,11 @@ const KIND_REFS = {
|
|
|
415
418
|
},
|
|
416
419
|
},
|
|
417
420
|
]),
|
|
418
|
-
|
|
421
|
+
// S1 amendment 59: runner_reason is required with runner_failed and absent otherwise.
|
|
422
|
+
"session.failed": obj({ reason: ENUM([...FAILED_REASONS]), runner_reason: RUNNER_REASON }, ["reason"], [
|
|
423
|
+
{ field: "reason", equals: "runner_failed", require: ["runner_reason"] },
|
|
424
|
+
{ field: "reason", equals: "transcript_lost", forbid: ["runner_reason"] },
|
|
425
|
+
]),
|
|
419
426
|
"session.killed": obj({ reason: ENUM(["owner", "agent_killed"]) }),
|
|
420
427
|
"seat.hello": obj({ spawn_id: UUID }),
|
|
421
428
|
"charter.sealed": obj({
|
|
@@ -478,7 +485,10 @@ const KIND_REFS = {
|
|
|
478
485
|
"containment.kill": obj({ action: ENUM(["kill"]) }),
|
|
479
486
|
"never.held": obj({ sh_decision_id: UUID }),
|
|
480
487
|
// ---- Doors amendment (rulings 34.02, 34.06) ----
|
|
481
|
-
|
|
488
|
+
// S1 amendment 59: a retried job's ready names the runner's reason and the attempt it ends.
|
|
489
|
+
"session.ready": obj({ cause: ENUM(READY_CAUSES), runner_reason: RUNNER_REASON, attempt: INT(1) }, ["cause"], READY_CAUSES.map((cause) => cause === "job_failed"
|
|
490
|
+
? { field: "cause", equals: cause, require: ["runner_reason", "attempt"] }
|
|
491
|
+
: { field: "cause", equals: cause, forbid: ["runner_reason", "attempt"] })),
|
|
482
492
|
"session.waiting": obj({
|
|
483
493
|
wait_reason: ENUM(PARKED_ONLY_WAIT_REASONS),
|
|
484
494
|
head_sha: S({ pattern: HEX40_PATTERN, maxLength: 40 }),
|
|
@@ -709,7 +709,7 @@ export declare const FREEZE_STRUCTURED: Readonly<{
|
|
|
709
709
|
}>;
|
|
710
710
|
}>;
|
|
711
711
|
GATEWAY_EVENTS_BODY_KEYS: readonly ["session_id", "kind", "epoch", "idempotency_key", "refs"];
|
|
712
|
-
READY_CAUSES: readonly ["dispatched", "lease_expired", "wait_cleared"];
|
|
712
|
+
READY_CAUSES: readonly ["dispatched", "lease_expired", "wait_cleared", "job_failed"];
|
|
713
713
|
WAIT_REASONS: readonly ["approval", "moving", "mnemom_unavailable", "ci", "question", "budget"];
|
|
714
714
|
PARKED_ONLY_WAIT_REASONS: readonly ["ci", "question", "budget"];
|
|
715
715
|
WAIT_REF_KEYS: Readonly<{
|
|
@@ -740,7 +740,7 @@ export declare const FREEZE_STRUCTURED: Readonly<{
|
|
|
740
740
|
readonly usage: 2;
|
|
741
741
|
readonly refused: 3;
|
|
742
742
|
}>;
|
|
743
|
-
RUNNER_FAILED_REASONS: readonly ["pr_open_failed"];
|
|
743
|
+
RUNNER_FAILED_REASONS: readonly ["pr_open_failed", "pr_no_changes"];
|
|
744
744
|
RUNNER_REFUSED_REASONS: readonly ["bundle_too_large", "bundle_malformed", "bundle_wrong_typ", "bundle_schema", "bundle_unknown_key", "bundle_unusable_key", "bundle_retired_key", "bundle_bad_signature", "bundle_code_digest_mismatch", "bundle_not_yet_valid", "bundle_expired", "bundle_binding_mismatch", "bundle_unavailable", "job_unavailable", "contract_bad_sha", "contract_too_large", "contract_sha_mismatch", "contract_not_a_contract", "contract_mismatch_unrecoverable", "claim_needs_key", "claim_ineligible", "claim_unavailable", "lease_lost", "executor_revoked", "executor_unauthorized", "oidc_bad_issuer", "oidc_repo_not_enrolled", "oidc_ref_not_allowed", "oidc_pool_flag_off", "oidc_unavailable", "api_unavailable"];
|
|
745
745
|
RUNNER_OPEN_MODE_TOLERATED: readonly ["bundle_unavailable", "job_unavailable", "api_unavailable"];
|
|
746
746
|
RUNNER_MERGE_STRATEGIES: readonly ["external", "merge_on_green"];
|
|
@@ -22,6 +22,11 @@
|
|
|
22
22
|
* transaction with the lock); a plain lock never sets it. The executor learns it only from the
|
|
23
23
|
* claim success answer (`new_session: true`, while no seat hello was ever accepted for the
|
|
24
24
|
* session); EXEC_SESSIONS_RESPONSE item keys are unchanged.
|
|
25
|
+
*
|
|
26
|
+
* Amendment 59 (signed by Wassim 2026-10-31), appended to its table:
|
|
27
|
+
* - agent_sessions.job_failures (INTEGER NOT NULL DEFAULT 0, CHECK >= 0): the pool job's accepted
|
|
28
|
+
* runner_failed count, never reset. Separate from resume_failures, so a lease lapse never spends
|
|
29
|
+
* a job retry and a job retry never counts towards respawn_failed.
|
|
25
30
|
*/
|
|
26
31
|
export declare const M1_TABLES: readonly ["agent_sessions", "durable_lease_keys", "agent_session_charters", "agent_session_events", "durable_event_kinds", "agent_session_chains", "agent_executors", "agent_durable_profiles", "durable_cell_state", "durable_org_policy", "durable_user_policy", "durable_approvals", "durable_approval_attestations"];
|
|
27
32
|
export type M1Table = (typeof M1_TABLES)[number];
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
* transaction with the lock); a plain lock never sets it. The executor learns it only from the
|
|
24
24
|
* claim success answer (`new_session: true`, while no seat hello was ever accepted for the
|
|
25
25
|
* session); EXEC_SESSIONS_RESPONSE item keys are unchanged.
|
|
26
|
+
*
|
|
27
|
+
* Amendment 59 (signed by Wassim 2026-10-31), appended to its table:
|
|
28
|
+
* - agent_sessions.job_failures (INTEGER NOT NULL DEFAULT 0, CHECK >= 0): the pool job's accepted
|
|
29
|
+
* runner_failed count, never reset. Separate from resume_failures, so a lease lapse never spends
|
|
30
|
+
* a job retry and a job retry never counts towards respawn_failed.
|
|
26
31
|
*/
|
|
27
32
|
import { canonicalJson, sha256Hex, UUID_RE } from "./canonical.js";
|
|
28
33
|
import { CANONICAL_CELL_IDS } from "./cell-id.js";
|
|
@@ -84,6 +89,7 @@ export const M1_COLUMNS = {
|
|
|
84
89
|
"updated_at",
|
|
85
90
|
"wait_ref",
|
|
86
91
|
"new_session",
|
|
92
|
+
"job_failures",
|
|
87
93
|
],
|
|
88
94
|
durable_lease_keys: [
|
|
89
95
|
"key_hash",
|
|
@@ -12,7 +12,8 @@ export declare const DETACHED_REASONS: readonly ["lease_expired", "hello_missed"
|
|
|
12
12
|
export declare const SUSPENDED_REASONS: readonly ["budget_reached", "enforce_refused", "charter_breach", "account_balance", "identity_mismatch", "grant_expired", "card_drift", "attestation_failed", "owner_departed", "respawn_failed", "approval_expired", "approval_denied"];
|
|
13
13
|
/** breakglass is P0a-B; it stays in the enum so the CHECK never needs widening. */
|
|
14
14
|
export declare const PAUSED_REASONS: readonly ["owner", "agent_contained", "breakglass"];
|
|
15
|
-
|
|
15
|
+
/** S1 amendment 59: runner_failed is a pool job's terminal failure, after its retries (doors.ts). */
|
|
16
|
+
export declare const FAILED_REASONS: readonly ["transcript_lost", "runner_failed"];
|
|
16
17
|
export declare const KILLED_REASONS: readonly ["agent_killed"];
|
|
17
18
|
export type DetachedReason = (typeof DETACHED_REASONS)[number];
|
|
18
19
|
export type SuspendedReason = (typeof SUSPENDED_REASONS)[number];
|
|
@@ -51,7 +51,8 @@ export const SUSPENDED_REASONS = [
|
|
|
51
51
|
];
|
|
52
52
|
/** breakglass is P0a-B; it stays in the enum so the CHECK never needs widening. */
|
|
53
53
|
export const PAUSED_REASONS = ["owner", "agent_contained", "breakglass"];
|
|
54
|
-
|
|
54
|
+
/** S1 amendment 59: runner_failed is a pool job's terminal failure, after its retries (doors.ts). */
|
|
55
|
+
export const FAILED_REASONS = ["transcript_lost", "runner_failed"];
|
|
55
56
|
export const KILLED_REASONS = ["agent_killed"];
|
|
56
57
|
/**
|
|
57
58
|
* The closed state_reason enum per state. `null` in a list means NULL is allowed there and what it
|