@mnemom/mnemom 0.17.34-next.2 → 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.
Files changed (87) hide show
  1. package/dist/commands/agent-run.d.ts +16 -6
  2. package/dist/commands/agent-run.js +52 -27
  3. package/dist/commands/agent-share-test-issuer.d.ts +67 -0
  4. package/dist/commands/agent-share-test-issuer.js +139 -0
  5. package/dist/commands/agent-share.d.ts +7 -1
  6. package/dist/commands/agent-share.js +14 -4
  7. package/dist/commands/agent.d.ts +7 -5
  8. package/dist/commands/agent.js +84 -45
  9. package/dist/commands/durable-lock.d.ts +8 -1
  10. package/dist/commands/durable-lock.js +9 -4
  11. package/dist/commands/scan.d.ts +12 -1
  12. package/dist/commands/scan.js +22 -3
  13. package/dist/commands/wrap.js +1 -1
  14. package/dist/durable/adopt-live.js +5 -8
  15. package/dist/durable/adopt.d.ts +8 -0
  16. package/dist/durable/adopt.js +27 -6
  17. package/dist/durable/cwd-rule.d.ts +9 -1
  18. package/dist/durable/cwd-rule.js +18 -0
  19. package/dist/durable/launch-spec.d.ts +2 -0
  20. package/dist/durable/launch-spec.js +1 -1
  21. package/dist/durable/test-mode.mjs +25 -1
  22. package/dist/host/client.d.ts +3 -1
  23. package/dist/host/daemon.d.ts +7 -3
  24. package/dist/host/daemon.js +13 -3
  25. package/dist/host/owner.d.ts +3 -0
  26. package/dist/host/owner.js +2 -0
  27. package/dist/host/run.d.ts +2 -3
  28. package/dist/host/run.js +5 -20
  29. package/dist/host/s1-names.d.ts +1 -0
  30. package/dist/host/s1-names.js +1 -0
  31. package/dist/host/verbs.d.ts +2 -0
  32. package/dist/host/verbs.js +11 -0
  33. package/dist/index.js +13 -9
  34. package/dist/lib/agent.d.ts +5 -3
  35. package/dist/lib/agent.js +6 -3
  36. package/dist/lib/claude-bin.d.ts +6 -0
  37. package/dist/lib/claude-bin.js +26 -0
  38. package/dist/lib/commit-report.d.ts +5 -4
  39. package/dist/lib/commit-report.js +6 -9
  40. package/dist/lib/config.d.ts +8 -1
  41. package/dist/lib/config.js +28 -0
  42. package/dist/lib/durable/launch.d.ts +28 -11
  43. package/dist/lib/durable/launch.js +33 -15
  44. package/dist/lib/durable/live.d.ts +15 -19
  45. package/dist/lib/durable/live.js +41 -43
  46. package/dist/lib/launcher-config.d.ts +6 -0
  47. package/dist/lib/launcher-config.js +11 -0
  48. package/dist/lib/runner/exec-append.d.ts +15 -0
  49. package/dist/lib/runner/exec-append.js +2 -0
  50. package/dist/lib/runner/executor-client.d.ts +2 -1
  51. package/dist/lib/runner/executor-state.d.ts +10 -0
  52. package/dist/lib/runner/executor-state.js +24 -0
  53. package/dist/lib/runner/github-base.d.ts +24 -0
  54. package/dist/lib/runner/github-base.js +27 -0
  55. package/dist/lib/runner/job.d.ts +11 -4
  56. package/dist/lib/runner/job.js +59 -16
  57. package/dist/lib/runner/oidc.d.ts +3 -2
  58. package/dist/lib/runner/oidc.js +7 -4
  59. package/dist/lib/runner/options.d.ts +6 -0
  60. package/dist/lib/runner/options.js +19 -1
  61. package/dist/lib/runner/outcome.js +2 -3
  62. package/dist/lib/runner/phases.d.ts +7 -4
  63. package/dist/lib/runner/phases.js +8 -4
  64. package/dist/lib/runner/proxy.d.ts +5 -1
  65. package/dist/lib/runner/proxy.js +7 -1
  66. package/dist/lib/scan/cost-per-pr.d.ts +93 -0
  67. package/dist/lib/scan/cost-per-pr.js +101 -0
  68. package/dist/lib/scan/record.d.ts +27 -1
  69. package/dist/lib/scan/record.js +114 -16
  70. package/dist/lib/scan/report-email.d.ts +25 -5
  71. package/dist/lib/scan/report-email.js +23 -2
  72. package/dist/lib/scan/scan.d.ts +3 -0
  73. package/dist/lib/scan/scan.js +13 -1
  74. package/dist/lib/session-key.d.ts +4 -3
  75. package/dist/vendor/durable-contract/index.js +207 -169
  76. package/dist/vendor/shared/durable-contract/doors.d.ts +26 -1
  77. package/dist/vendor/shared/durable-contract/doors.js +37 -1
  78. package/dist/vendor/shared/durable-contract/events.d.ts +1 -1
  79. package/dist/vendor/shared/durable-contract/events.js +13 -3
  80. package/dist/vendor/shared/durable-contract/freeze-record.d.ts +2 -2
  81. package/dist/vendor/shared/durable-contract/schema.d.ts +5 -0
  82. package/dist/vendor/shared/durable-contract/schema.js +6 -0
  83. package/dist/vendor/shared/durable-contract/states.d.ts +2 -1
  84. package/dist/vendor/shared/durable-contract/states.js +2 -1
  85. package/package.json +1 -1
  86. package/dist/lib/durable/first-turn.d.ts +0 -50
  87. package/dist/lib/durable/first-turn.js +0 -102
@@ -332,17 +332,30 @@ export async function planDurableLaunch(i, deps) {
332
332
  }
333
333
  }
334
334
  }
335
+ // ---- a tracked spawn with no terminal on stdin (durable gate on) -------------------------------
336
+ /**
337
+ * The one stderr line a gate-on launch prints before its tracked Claude Code spawn when stdin is
338
+ * not a TTY and the passthrough has no -p/--print: Claude Code started that way waits forever and
339
+ * draws nothing. The spawn itself stays as today (C02-R23 a, b, f fallback); gate off prints
340
+ * nothing (C17-R11). No command line in it, so no placeholder (C11-R27). Null: print nothing.
341
+ */
342
+ export function trackedNoTtyLine(i) {
343
+ if (!i.gateOn || i.stdinIsTTY || i.cliName !== "claude")
344
+ return null;
345
+ if (i.passthrough.some((a) => a === "-p" || a === "--print" || a.startsWith("--print=")))
346
+ return null;
347
+ return `${i.name}: Claude Code needs a terminal on stdin and will wait without one; run this from a terminal, or pass Claude's -p flag after --.`;
348
+ }
335
349
  // ---- lock refused on the launcher path (C02-R23 c, e; C02-R24) ---------------------------------
336
350
  /**
337
- * What a launcher does after its lock attempt refused: a durable-only agent gets the
338
- * durable_only_agent refusal and exit 1, never a tracked spawn (C02-R24, final F02); any other
339
- * agent prints the lock refusal and its fix, then spawns tracked (exit null).
351
+ * What a launcher does after its lock attempt refused: it exits non-zero with the refusal's own fix
352
+ * line and never falls back to an attended (tracked) spawn, for every agent (Durable lead ruling
353
+ * 2026-10-30 on C02-R23 c, e). A durable-only agent gets the durable_only_agent refusal (C02-R24,
354
+ * final F02); any other agent prints the lock refusal itself.
340
355
  */
341
356
  export function afterLockRefusal(refusal, durableOnly, launcherKey, name) {
342
- if (!durableOnly)
343
- return { print: refusal, exit: null };
344
- // cwd_untrusted is CLI-local (no S1 precondition clause): a durable-only launch prints it as itself,
345
- // its fix shaped for the launch (rerun it), not for `mnemom agent lock`.
357
+ // cwd_untrusted is CLI-local (no S1 precondition clause): a launch prints it as itself, its fix
358
+ // shaped for the launch (rerun it), not for `mnemom agent lock`.
346
359
  const root = refusal.details.cwd;
347
360
  if (refusal.code === "cwd_untrusted" && typeof root === "string") {
348
361
  const r = new DurableRefusal({
@@ -353,21 +366,26 @@ export function afterLockRefusal(refusal, durableOnly, launcherKey, name) {
353
366
  });
354
367
  return { print: r, exit: r.exit };
355
368
  }
369
+ if (!durableOnly)
370
+ return { print: refusal, exit: refusal.exit };
356
371
  const r = durableOnlyRefusal(launcherKey, name, refusal, refusal.fixParams);
357
372
  return { print: r, exit: r.exit };
358
373
  }
359
- // ---- lock after the first turn (dogfood B5) -----------------------------------------------------
374
+ // ---- a new session locks before claude starts (ruling A) --------------------------------------
360
375
  /**
361
- * A lock on a NEW Claude Code session (no transcript yet) waits for the session's first turn: the
362
- * seat resumes the transcript, so a lock before it exists fails transcript_lost. The launcher
363
- * spawns claude attended and locks once that turn has completed (lib/durable/first-turn.ts). A
364
- * continued session already has its transcript and locks instead of spawning, as before.
376
+ * A lock on a NEW Claude Code session (no transcript yet) is a create-and-lock (ruling A, which
377
+ * widens B5a to every agent): the session is born locked and its seat starts the transcript, so no
378
+ * attended claude ever runs first. A continued session already has its transcript and locks its
379
+ * registered row instead of spawning, as before.
365
380
  */
366
- export function locksAfterFirstTurn(plan) {
381
+ export function locksAtCreate(plan) {
367
382
  return plan.lock && plan.newCcSessionId !== null;
368
383
  }
369
- /** The row the first-turn lock commits: the registered row, naming the CC session this launch pins. */
370
- export function firstTurnRow(row, plan) {
384
+ /**
385
+ * The registered row of a tracked launch, naming the CC session this launch pins: what a later lock
386
+ * from another terminal hands over (C02-R26).
387
+ */
388
+ export function launchRow(row, plan) {
371
389
  return typeof row.cc_session_id === "string"
372
390
  ? row
373
391
  : { ...row, cc_session_id: plan.newCcSessionId };
@@ -15,7 +15,6 @@ import { type DurableLaunchPlan, type PlanDeps, type RegistryReads, type SeatDep
15
15
  import { type HandoverDeps } from "./handover.js";
16
16
  import { type HostInstance } from "../../host/instance.js";
17
17
  import { type OwnerHttp } from "../../host/owner.js";
18
- import { type FirstTurnLock } from "./first-turn.js";
19
18
  import type { HomeFacts } from "./view.js";
20
19
  export declare function liveDurableApi(): DurableApi;
21
20
  /** launcher.toml `durable`, defaulting to off (only read while the gate is on); unreadable is off. */
@@ -156,19 +155,24 @@ export interface LockInsteadDeps {
156
155
  lockDeps: LockDeps;
157
156
  seat: SeatDeps;
158
157
  }
159
- export declare function lockInsteadOfSpawn(row: RegistryRow, name: string, launcherKey: LauncherKey, durableOnly: true, d?: LockInsteadDeps): Promise<number>;
160
- export declare function lockInsteadOfSpawn(row: RegistryRow, name: string, launcherKey: LauncherKey, durableOnly: boolean, d?: LockInsteadDeps): Promise<number | null>;
158
+ export declare function lockInsteadOfSpawn(row: RegistryRow, name: string, launcherKey: LauncherKey, durableOnly: boolean, d?: LockInsteadDeps): Promise<number>;
159
+ /** The api's create-and-lock refusal before migration 06a (or after its rollback): falls back to tracked. */
160
+ export declare const PRE_06A_CREATE_LOCK_CODE = "create_lock_not_durable_only";
161
161
  /** The row a create-and-lock is about to make: what the idle-source lock reads (cwd, cc id). */
162
162
  export declare function bornRow(plan: Extract<DurableLaunchPlan, {
163
163
  kind: "spawn";
164
164
  }>, name: string, agentId: string, cwd: string): RegistryRow;
165
165
  /**
166
- * B5a (as amended): `--new` under `always` on a durable-only agent creates the session locked in
167
- * one request (the create route with the lock verb's body; the api commits both in one
168
- * transaction), then attaches to the seat like any lock instead of spawning. No tracked row, no
169
- * attended claude, so no turn ever runs on a plain key. The lock is the lock verb's own
170
- * (lockResolvedRow: cwd rule, executor id, spec); only its POST goes to the create route. A
171
- * refusal is the durable_only_agent refusal and exit 1 (C02-R24), never a tracked spawn.
166
+ * Ruling A (B5a as amended, widened to every agent): a NEW session under `always` / ask-yes is
167
+ * created locked in one request (the create route with the lock verb's body; the api commits both
168
+ * in one transaction), then attaches to the seat like any lock instead of spawning. No tracked row,
169
+ * no attended claude, so no turn runs before the lock. The lock is the lock verb's own
170
+ * (lockResolvedRow: cwd rule, executor id, spec); only its POST goes to the create route. A refusal
171
+ * leaves no row and exits 1: on a durable-only agent with the durable_only_agent refusal (C02-R24),
172
+ * on any other agent with the lock refusal and its fix; the launcher never spawns tracked instead,
173
+ * with one exception (ruling 2026-10-31, option 1): an api without migration 06a, or with it rolled
174
+ * back, answers create_lock_not_durable_only for an agent that is not durable-only. That answer
175
+ * resolves to null, and the launch registers tracked and spawns exactly as before ruling A.
172
176
  *
173
177
  * No claude runs before the lock, so the spec cannot be read from a live original as the lock after
174
178
  * the first turn does: the launch's own `--model` / `--effort` stand in for it (a passthrough
@@ -181,14 +185,6 @@ export declare function createLockedInsteadOfSpawn(plan: Extract<DurableLaunchPl
181
185
  model?: string;
182
186
  effort?: string;
183
187
  trimMcp?: boolean;
184
- }, d?: LockInsteadDeps & {
188
+ }, durableOnly: boolean, d?: LockInsteadDeps & {
185
189
  api: Pick<DurableApi, "createSession">;
186
- }): Promise<number>;
187
- /**
188
- * Dogfood B5: the live lock after the first turn of a new session (lib/durable/first-turn.ts). The
189
- * transcript is read under `${CLAUDE_CONFIG_DIR:-~/.claude}/projects`; the lock is the lock verb's
190
- * (cwd rule, transcript slug check, executor id) under the running-session handover, so a commit
191
- * freezes the attended claude, waits for the seat's hello and ends it, and the launcher's exit path
192
- * attaches to the seat (C02-R26 step 7). A refusal is printed with its fix line; claude goes on.
193
- */
194
- export declare function liveLockAfterFirstTurn(row: RegistryRow, name: string, launcherKey: LauncherKey, childAlive: () => boolean): Promise<FirstTurnLock>;
190
+ }): Promise<number | null>;
@@ -4,7 +4,7 @@
4
4
  * every decision lives in the injected modules and is unit-tested there.
5
5
  */
6
6
  import { spawnSync } from "node:child_process";
7
- import { existsSync, readFileSync } from "node:fs";
7
+ import { existsSync } from "node:fs";
8
8
  import { randomUUID } from "node:crypto";
9
9
  import { homedir } from "node:os";
10
10
  import { getAccessToken, resolveAuth } from "../auth.js";
@@ -24,11 +24,9 @@ import { liveCwdProbe, liveTranscriptDirs, liveTrustProbe, lockFactsOf, lockReso
24
24
  import { DurableApi, RegistryUnavailable, } from "./api.js";
25
25
  import { afterLockRefusal, attachToSeat, createBody, createOutcome, flagValue, } from "./launch.js";
26
26
  import { helloLanded, ledgerCcSessionIdFrom, lockCommitted, parentAfterChildEnded, processTree, sessionRowOf, } from "./handover.js";
27
- import { ccProjectsDir } from "../../host/transcript.js";
28
27
  import { resolveInstance } from "../../host/instance.js";
29
28
  import { ownerApi, parseExecutorList } from "../../host/owner.js";
30
29
  import { seatTmuxSession } from "../../host/tmux.js";
31
- import { lockAfterFirstTurn, transcriptReader } from "./first-turn.js";
32
30
  import { DurableRefusal, printDurableRefusal } from "./refusal.js";
33
31
  export function liveDurableApi() {
34
32
  return new DurableApi({
@@ -530,28 +528,37 @@ function liveLockInsteadDeps(launcherKey) {
530
528
  seat: liveSeatDeps(launcherKey),
531
529
  };
532
530
  }
531
+ // A launch that locks never falls back to a spawn: afterLockRefusal always names its exit.
533
532
  export async function lockInsteadOfSpawn(row, name, launcherKey, durableOnly, d = liveLockInsteadDeps(launcherKey)) {
534
- const deps = d.lockDeps;
535
- const o = await d.lockRow(row, name, deps);
536
- if (!o.ok) {
537
- const next = afterLockRefusal(o.refusal, durableOnly, launcherKey, name);
538
- printDurableRefusal(next.print, false, deps);
539
- return next.exit;
540
- }
533
+ const o = await d.lockRow(row, name, d.lockDeps);
534
+ if (!o.ok)
535
+ return printLockRefusal(o.refusal, durableOnly, launcherKey, name, d.lockDeps);
536
+ return attachLocked(o, row, name, d);
537
+ }
538
+ /** A lock refusal: printed with its fix; the exit status afterLockRefusal gives. */
539
+ function printLockRefusal(refusal, durableOnly, launcherKey, name, deps) {
540
+ const next = afterLockRefusal(refusal, durableOnly, launcherKey, name);
541
+ printDurableRefusal(next.print, false, deps);
542
+ return next.exit;
543
+ }
544
+ /** A landed lock: print it (interactive), then attach to the seat. */
545
+ async function attachLocked(o, row, name, d) {
541
546
  const seat = d.seat;
542
547
  if (seat.interactive)
543
- printLocked(o, row, name, deps.out);
548
+ printLocked(o, row, name, d.lockDeps.out);
544
549
  const dropped = lockFactsOf(o.body, row, name, o.dropped).droppedFlags;
545
550
  try {
546
551
  return await attachToSeat(row, name, dropped, seat);
547
552
  }
548
553
  catch (e) {
549
554
  if (e instanceof DurableRefusal)
550
- return printDurableRefusal(e, false, deps);
555
+ return printDurableRefusal(e, false, d.lockDeps);
551
556
  throw e;
552
557
  }
553
558
  }
554
559
  // ---- create-and-lock (ruling B5a as amended) ----------------------------------------------------
560
+ /** The api's create-and-lock refusal before migration 06a (or after its rollback): falls back to tracked. */
561
+ export const PRE_06A_CREATE_LOCK_CODE = "create_lock_not_durable_only";
555
562
  /** The row a create-and-lock is about to make: what the idle-source lock reads (cwd, cc id). */
556
563
  export function bornRow(plan, name, agentId, cwd) {
557
564
  return {
@@ -565,19 +572,23 @@ export function bornRow(plan, name, agentId, cwd) {
565
572
  };
566
573
  }
567
574
  /**
568
- * B5a (as amended): `--new` under `always` on a durable-only agent creates the session locked in
569
- * one request (the create route with the lock verb's body; the api commits both in one
570
- * transaction), then attaches to the seat like any lock instead of spawning. No tracked row, no
571
- * attended claude, so no turn ever runs on a plain key. The lock is the lock verb's own
572
- * (lockResolvedRow: cwd rule, executor id, spec); only its POST goes to the create route. A
573
- * refusal is the durable_only_agent refusal and exit 1 (C02-R24), never a tracked spawn.
575
+ * Ruling A (B5a as amended, widened to every agent): a NEW session under `always` / ask-yes is
576
+ * created locked in one request (the create route with the lock verb's body; the api commits both
577
+ * in one transaction), then attaches to the seat like any lock instead of spawning. No tracked row,
578
+ * no attended claude, so no turn runs before the lock. The lock is the lock verb's own
579
+ * (lockResolvedRow: cwd rule, executor id, spec); only its POST goes to the create route. A refusal
580
+ * leaves no row and exits 1: on a durable-only agent with the durable_only_agent refusal (C02-R24),
581
+ * on any other agent with the lock refusal and its fix; the launcher never spawns tracked instead,
582
+ * with one exception (ruling 2026-10-31, option 1): an api without migration 06a, or with it rolled
583
+ * back, answers create_lock_not_durable_only for an agent that is not durable-only. That answer
584
+ * resolves to null, and the launch registers tracked and spawns exactly as before ruling A.
574
585
  *
575
586
  * No claude runs before the lock, so the spec cannot be read from a live original as the lock after
576
587
  * the first turn does: the launch's own `--model` / `--effort` stand in for it (a passthrough
577
588
  * `-- --model` / `--effort` first, as it comes last on claude's argv; else the flag, else
578
589
  * launcher.toml), with its `--trim-mcp`, so the seat runs as the launch chose, never the default.
579
590
  */
580
- export async function createLockedInsteadOfSpawn(plan, name, agentId, launcherKey, launch, d = {
591
+ export async function createLockedInsteadOfSpawn(plan, name, agentId, launcherKey, launch, durableOnly, d = {
581
592
  ...liveLockInsteadDeps(launcherKey),
582
593
  api: liveDurableApi(),
583
594
  }) {
@@ -588,6 +599,10 @@ export async function createLockedInsteadOfSpawn(plan, name, agentId, launcherKe
588
599
  const lockRow = async (r, n, deps) => {
589
600
  const o = await d.lockRow(r, n, {
590
601
  ...deps,
602
+ // Nothing holds the name: the row is not registered until the create lands, and a live row
603
+ // under the name refuses the launch first (planDurableLaunch, session_name_in_use). So a
604
+ // refusal's fix line never sends the user to `mnemom agent done`, which would fail (C11-R27).
605
+ nameHeld: false,
591
606
  specDefaults: (row) => ({
592
607
  ...deps.specDefaults(row),
593
608
  ...(model ? { model } : {}),
@@ -608,28 +623,11 @@ export async function createLockedInsteadOfSpawn(plan, name, agentId, launcherKe
608
623
  r.id = sid;
609
624
  return o;
610
625
  };
611
- return lockInsteadOfSpawn(row, name, launcherKey, true, { ...d, lockRow });
612
- }
613
- /**
614
- * Dogfood B5: the live lock after the first turn of a new session (lib/durable/first-turn.ts). The
615
- * transcript is read under `${CLAUDE_CONFIG_DIR:-~/.claude}/projects`; the lock is the lock verb's
616
- * (cwd rule, transcript slug check, executor id) under the running-session handover, so a commit
617
- * freezes the attended claude, waits for the seat's hello and ends it, and the launcher's exit path
618
- * attaches to the seat (C02-R26 step 7). A refusal is printed with its fix line; claude goes on.
619
- */
620
- export function liveLockAfterFirstTurn(row, name, launcherKey, childAlive) {
621
- const dirs = liveTranscriptDirs(process.env);
622
- const seat = liveSeatDeps(launcherKey);
623
- return lockAfterFirstTurn({
624
- name,
625
- readTranscript: transcriptReader(ccProjectsDir(process.env), dirs, row.cc_session_id, (p) => readFileSync(p, "utf8")),
626
- childAlive,
627
- lock: () => lockResolvedRow(row, name, {}, {
628
- ...liveLockDeps(),
629
- handover: liveHandoverDeps(liveDurableApi()),
630
- transcriptDirs: dirs,
631
- }),
632
- printRefusal: (r) => printDurableRefusal(r, false, seat),
633
- sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
634
- });
626
+ const o = await lockRow(row, name, d.lockDeps);
627
+ if (o.ok)
628
+ return attachLocked(o, row, name, d);
629
+ // nothing printed: the caller registers tracked and spawns as before ruling A
630
+ if (o.refusal.code === PRE_06A_CREATE_LOCK_CODE)
631
+ return null;
632
+ return printLockRefusal(o.refusal, durableOnly, launcherKey, name, d.lockDeps);
635
633
  }
@@ -98,6 +98,12 @@ export interface LauncherConfig {
98
98
  stallTurns?: number;
99
99
  };
100
100
  }
101
+ /**
102
+ * The launcher key `durable` a launch uses: a valid MNEMOM_AGENT_DURABLE wins over launcher.toml
103
+ * (so `MNEMOM_AGENT_DURABLE=off` turns a configured `always` off for one launch), else the
104
+ * launcher.toml value, else `off`. Adopt reads the key the same way.
105
+ */
106
+ export declare function durableLauncherKey(env: Readonly<Record<string, string | undefined>>, configured: LauncherKey | undefined): LauncherKey;
101
107
  /**
102
108
  * A settable key: its dotted config name, the getter/setter into a LauncherConfig,
103
109
  * and a one-line description for `config list`/help. Kept as data so get/set/list
@@ -34,6 +34,17 @@ function coerceDurable(raw) {
34
34
  return v;
35
35
  throw new Error(`durable must be one of ${LAUNCHER_KEYS.join(", ")} (got '${raw}').`);
36
36
  }
37
+ /**
38
+ * The launcher key `durable` a launch uses: a valid MNEMOM_AGENT_DURABLE wins over launcher.toml
39
+ * (so `MNEMOM_AGENT_DURABLE=off` turns a configured `always` off for one launch), else the
40
+ * launcher.toml value, else `off`. Adopt reads the key the same way.
41
+ */
42
+ export function durableLauncherKey(env, configured) {
43
+ const e = env.MNEMOM_AGENT_DURABLE?.trim().toLowerCase();
44
+ if (e !== undefined && LAUNCHER_KEYS.includes(e))
45
+ return e;
46
+ return configured ?? "off";
47
+ }
37
48
  function coerceLaunch(raw) {
38
49
  const v = raw.trim();
39
50
  if (LAUNCH_SHAPES.includes(v))
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The exec_append body: exactly these keys, as frozen S1 has it (`refs`, and the boot-bound
3
+ * `nonce`). Every executor append (the daemon's executor.spawned and executor.turn_continued, and the
4
+ * thin runner's executor.spawned) sends this shape. It lives in lib/runner with no imports so the
5
+ * runner can load it inside its allow-listed closure (runner-no-prompts); host/s1-names re-exports it.
6
+ */
7
+ export interface ExecAppendBody {
8
+ readonly kind: string;
9
+ readonly refs: Readonly<Record<string, unknown>>;
10
+ readonly idempotency_key: string;
11
+ readonly expected_epoch: number;
12
+ readonly nonce: string;
13
+ }
14
+ /** The executor.spawned idempotency key, one per (session, epoch, spawn). */
15
+ export declare const spawnIdempotencyKey: (sessionId: string, epoch: number, spawnId: string) => string;
@@ -0,0 +1,2 @@
1
+ /** The executor.spawned idempotency key, one per (session, epoch, spawn). */
2
+ export const spawnIdempotencyKey = (sessionId, epoch, spawnId) => `spawn:${sessionId}:${epoch}:${spawnId}`;
@@ -8,6 +8,7 @@
8
8
  * Nothing here decides an outcome: each call returns its status, body and error code, and the job
9
9
  * (job.ts) maps them to 34.12's reasons. Never throws: a network failure is status 0.
10
10
  */
11
+ import type { ExecAppendBody } from "./exec-append.js";
11
12
  export interface HttpResult {
12
13
  readonly status: number;
13
14
  readonly body: unknown;
@@ -40,7 +41,7 @@ export interface RunnerApi {
40
41
  nonce: string;
41
42
  new_key?: true;
42
43
  }): Promise<HttpResult>;
43
- append(sessionId: string, body: Record<string, unknown>): Promise<HttpResult>;
44
+ append(sessionId: string, body: ExecAppendBody): Promise<HttpResult>;
44
45
  hello(sessionId: string, body: {
45
46
  epoch: number;
46
47
  nonce: string;
@@ -0,0 +1,10 @@
1
+ /**
2
+ * The enrolled executor's own id, from its executor-state.json (S1 24.14; Durable lead, Doors
3
+ * W-L13). S1's pool claim answer names no executor_id (POOL_CLAIM_FIELDS), so the runner binds the
4
+ * served bundle to its own: the bundle's claim.executor_id must still equal it.
5
+ *
6
+ * The file counts only when it is S1's closed shape and its executor_key is the key this run uses,
7
+ * so an id is never borrowed from another executor's state.
8
+ */
9
+ /** executor_id from the file's text, or null when it is absent, malformed or another key's. */
10
+ export declare function executorIdFromState(text: string | null, executorKey: string): string | null;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The enrolled executor's own id, from its executor-state.json (S1 24.14; Durable lead, Doors
3
+ * W-L13). S1's pool claim answer names no executor_id (POOL_CLAIM_FIELDS), so the runner binds the
4
+ * served bundle to its own: the bundle's claim.executor_id must still equal it.
5
+ *
6
+ * The file counts only when it is S1's closed shape and its executor_key is the key this run uses,
7
+ * so an id is never borrowed from another executor's state.
8
+ */
9
+ import { executorStateOk } from "../../vendor/durable-contract/index.js";
10
+ /** executor_id from the file's text, or null when it is absent, malformed or another key's. */
11
+ export function executorIdFromState(text, executorKey) {
12
+ if (text === null)
13
+ return null;
14
+ let v;
15
+ try {
16
+ v = JSON.parse(text);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ if (!executorStateOk(v))
22
+ return null;
23
+ return v.executor_key === executorKey ? v.executor_id : null;
24
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Where `mnemom agent run` reaches GitHub.
3
+ *
4
+ * github.com for git and api.github.com for REST and GraphQL, always, except on a local Durable lane
5
+ * in test mode: there the fixture repos live on the lane's fake GitHub, the same one the api uses
6
+ * (DURABLE_TEST_GITHUB_API_BASE), which serves git and REST from one loopback origin. The test-mode
7
+ * module decides whether that base is honoured; outside test mode a set value is ignored with a
8
+ * warning and the runner stays on github.com. The runner's GitHub token only goes to these bases.
9
+ */
10
+ export interface GithubBases {
11
+ /** git remotes and PR URLs: `${web}/${owner}/${repo}`. */
12
+ web: string;
13
+ /** REST and GraphQL: `${api}/repos/...`, `${api}/graphql`. */
14
+ api: string;
15
+ }
16
+ export declare const GITHUB_BASES: Readonly<GithubBases>;
17
+ export declare const GITHUB_BASE_IGNORED = "mnemom agent run: DURABLE_TEST_GITHUB_API_BASE is ignored outside test mode; using github.com";
18
+ /** The bases to use, and a warning when a lane base was set but not honoured. */
19
+ export declare function resolveGithubBases(env: NodeJS.ProcessEnv): {
20
+ bases: GithubBases;
21
+ warning: string | null;
22
+ };
23
+ /** The PR URL pattern for a web base: `${web}/<owner>/<repo>/pull/<n>`. */
24
+ export declare function prUrlPattern(web: string): RegExp;
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Where `mnemom agent run` reaches GitHub.
3
+ *
4
+ * github.com for git and api.github.com for REST and GraphQL, always, except on a local Durable lane
5
+ * in test mode: there the fixture repos live on the lane's fake GitHub, the same one the api uses
6
+ * (DURABLE_TEST_GITHUB_API_BASE), which serves git and REST from one loopback origin. The test-mode
7
+ * module decides whether that base is honoured; outside test mode a set value is ignored with a
8
+ * warning and the runner stays on github.com. The runner's GitHub token only goes to these bases.
9
+ */
10
+ import { testGithubBase } from "../../durable/test-mode.mjs";
11
+ export const GITHUB_BASES = Object.freeze({
12
+ web: "https://github.com",
13
+ api: "https://api.github.com",
14
+ });
15
+ export const GITHUB_BASE_IGNORED = "mnemom agent run: DURABLE_TEST_GITHUB_API_BASE is ignored outside test mode; using github.com";
16
+ /** The bases to use, and a warning when a lane base was set but not honoured. */
17
+ export function resolveGithubBases(env) {
18
+ const t = testGithubBase(env);
19
+ if (t.base === null)
20
+ return { bases: GITHUB_BASES, warning: t.ignored ? GITHUB_BASE_IGNORED : null };
21
+ return { bases: { web: t.base, api: t.base }, warning: null };
22
+ }
23
+ /** The PR URL pattern for a web base: `${web}/<owner>/<repo>/pull/<n>`. */
24
+ export function prUrlPattern(web) {
25
+ const esc = web.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
26
+ return new RegExp(`^${esc}/([^/]+/[^/]+)/pull/(\\d+)$`);
27
+ }
@@ -25,6 +25,11 @@ export interface Workspace {
25
25
  }
26
26
  export interface JobDeps {
27
27
  api: RunnerApi;
28
+ /**
29
+ * This runner's executor id: MNEMOM_EXECUTOR_ID, else the OIDC exchange answer's, else the
30
+ * host's executor-state.json when it holds the key in use; null: the claim's.
31
+ */
32
+ executorId: string | null;
28
33
  /** One nonce per runner process, reused on every call (A3 fences on it). */
29
34
  nonce: string;
30
35
  newSpawnId(): string;
@@ -38,6 +43,8 @@ export interface JobDeps {
38
43
  runPhase(inv: PhaseInvocation, ws: Workspace, modelUrl: string, signal: AbortSignal): Promise<number>;
39
44
  /** Push the job branch and open its PR; resolves the PR URL. */
40
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>;
41
48
  enableAutoMerge(prUrl: string): Promise<void>;
42
49
  cleanup(ws: Workspace): Promise<void>;
43
50
  }
@@ -63,8 +70,8 @@ export declare function authRefusal(r: HttpResult): RefusedReason | null;
63
70
  */
64
71
  export declare function runJob(sessionId: string, opts: JobOptions, deps: JobDeps): Promise<JobResult>;
65
72
  /**
66
- * Report a failed or refused job after claim (34.12 (6), (10)). The fail reason vocabulary is the
67
- * Durable lead's to rule (A3 accepts only transcript_lost today); a refused report is logged and
68
- * never changes the outcome, because the lease lapses on its own.
73
+ * Report a failed job after claim (34.12 (6), (10)); a refusal releases the lease instead. The
74
+ * fail reason vocabulary is the Durable lead's to rule (A3 accepts only transcript_lost today); a
75
+ * fail the api refuses is logged and never changes the outcome, because the lease lapses on its own.
69
76
  */
70
- export declare function reportFail(deps: JobDeps, sessionId: string, epoch: number, kind: "runner_failed" | "runner_refused", reason: string): Promise<void>;
77
+ export declare function reportFail(deps: JobDeps, sessionId: string, epoch: number, kind: "runner_failed", reason: string): Promise<void>;