@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.
@@ -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
  };
@@ -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); never blocks exit.
1522
- if (ctx.sessionKeyId) {
1523
- await revokeSessionGatewayKey(defaultSessionKeyDeps(say), ctx.sessionKeyId);
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
- if (mayBeHandover(h !== undefined, signal) && h) {
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 its own
1538
- // hard deadline inside W1.3's exit budget; neither changes the exit code.
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)) {
@@ -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
  }
@@ -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 { RUNNER_EXIT_CODES, RUNNER_FAILED_PHASE_RE, RUNNER_FAILED_REASONS, RUNNER_OUTCOME_FIELDS, RUNNER_REFUSED_REASONS, } from "../../vendor/durable-contract/index.js";
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 (RUNNER_FAILED_REASONS.includes(reason) ||
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, tests only — real callers never set these, so the real
28
- * 10s/3s bounds below always apply in production. Exists so a test can exercise "never
29
- * settles" against a short, fast timeout rather than waiting out the real one.
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
- "session.failed": obj({ reason: ENUM([...FAILED_REASONS]) }),
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
- "session.ready": obj({ cause: ENUM(READY_CAUSES) }),
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
- "session.failed": obj({ reason: ENUM([...FAILED_REASONS]) }),
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
- "session.ready": obj({ cause: ENUM(READY_CAUSES) }),
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
- export declare const FAILED_REASONS: readonly ["transcript_lost"];
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
- export const FAILED_REASONS = ["transcript_lost"];
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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mnemom/mnemom",
3
- "version": "0.17.34-next.3",
3
+ "version": "0.17.34-next.4",
4
4
  "description": "Transparent AI agent tracing",
5
5
  "type": "module",
6
6
  "bin": {