@mnemom/mnemom 0.17.34-next.2 → 0.17.34-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent-run.d.ts +14 -6
- package/dist/commands/agent-run.js +44 -27
- package/dist/commands/agent-share-test-issuer.d.ts +67 -0
- package/dist/commands/agent-share-test-issuer.js +139 -0
- package/dist/commands/agent-share.d.ts +7 -1
- package/dist/commands/agent-share.js +14 -4
- package/dist/commands/agent.d.ts +7 -5
- package/dist/commands/agent.js +64 -36
- package/dist/commands/durable-lock.d.ts +8 -1
- package/dist/commands/durable-lock.js +9 -4
- package/dist/commands/scan.d.ts +12 -1
- package/dist/commands/scan.js +22 -3
- package/dist/commands/wrap.js +1 -1
- package/dist/durable/adopt-live.js +5 -8
- package/dist/durable/adopt.d.ts +8 -0
- package/dist/durable/adopt.js +27 -6
- package/dist/durable/cwd-rule.d.ts +9 -1
- package/dist/durable/cwd-rule.js +18 -0
- package/dist/durable/launch-spec.d.ts +2 -0
- package/dist/durable/launch-spec.js +1 -1
- package/dist/durable/test-mode.mjs +25 -1
- package/dist/host/client.d.ts +3 -1
- package/dist/host/daemon.d.ts +7 -3
- package/dist/host/daemon.js +13 -3
- package/dist/host/owner.d.ts +3 -0
- package/dist/host/owner.js +2 -0
- package/dist/host/run.d.ts +2 -3
- package/dist/host/run.js +5 -20
- package/dist/host/s1-names.d.ts +1 -0
- package/dist/host/s1-names.js +1 -0
- package/dist/host/verbs.d.ts +2 -0
- package/dist/host/verbs.js +11 -0
- package/dist/index.js +13 -9
- package/dist/lib/agent.d.ts +5 -3
- package/dist/lib/agent.js +6 -3
- package/dist/lib/claude-bin.d.ts +6 -0
- package/dist/lib/claude-bin.js +26 -0
- package/dist/lib/commit-report.d.ts +5 -4
- package/dist/lib/commit-report.js +6 -9
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.js +28 -0
- package/dist/lib/durable/launch.d.ts +28 -11
- package/dist/lib/durable/launch.js +33 -15
- package/dist/lib/durable/live.d.ts +15 -19
- package/dist/lib/durable/live.js +41 -43
- package/dist/lib/launcher-config.d.ts +6 -0
- package/dist/lib/launcher-config.js +11 -0
- package/dist/lib/runner/exec-append.d.ts +15 -0
- package/dist/lib/runner/exec-append.js +2 -0
- package/dist/lib/runner/executor-client.d.ts +2 -1
- package/dist/lib/runner/executor-state.d.ts +10 -0
- package/dist/lib/runner/executor-state.js +24 -0
- package/dist/lib/runner/github-base.d.ts +24 -0
- package/dist/lib/runner/github-base.js +27 -0
- package/dist/lib/runner/job.d.ts +9 -4
- package/dist/lib/runner/job.js +43 -15
- package/dist/lib/runner/oidc.d.ts +3 -2
- package/dist/lib/runner/oidc.js +7 -4
- package/dist/lib/runner/options.d.ts +6 -0
- package/dist/lib/runner/options.js +19 -1
- package/dist/lib/runner/phases.d.ts +7 -4
- package/dist/lib/runner/phases.js +8 -4
- package/dist/lib/runner/proxy.d.ts +5 -1
- package/dist/lib/runner/proxy.js +7 -1
- package/dist/lib/scan/cost-per-pr.d.ts +93 -0
- package/dist/lib/scan/cost-per-pr.js +101 -0
- package/dist/lib/scan/record.d.ts +27 -1
- package/dist/lib/scan/record.js +114 -16
- package/dist/lib/scan/report-email.d.ts +25 -5
- package/dist/lib/scan/report-email.js +23 -2
- package/dist/lib/scan/scan.d.ts +3 -0
- package/dist/lib/scan/scan.js +13 -1
- package/package.json +1 -1
- package/dist/lib/durable/first-turn.d.ts +0 -50
- 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:
|
|
338
|
-
*
|
|
339
|
-
*
|
|
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
|
-
|
|
343
|
-
|
|
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
|
-
// ----
|
|
374
|
+
// ---- a new session locks before claude starts (ruling A) --------------------------------------
|
|
360
375
|
/**
|
|
361
|
-
* A lock on a NEW Claude Code session (no transcript yet)
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
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
|
|
381
|
+
export function locksAtCreate(plan) {
|
|
367
382
|
return plan.lock && plan.newCcSessionId !== null;
|
|
368
383
|
}
|
|
369
|
-
/**
|
|
370
|
-
|
|
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:
|
|
160
|
-
|
|
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
|
|
167
|
-
* one request (the create route with the lock verb's body; the api commits both
|
|
168
|
-
* transaction), then attaches to the seat like any lock instead of spawning. No tracked row,
|
|
169
|
-
* attended claude, so no turn
|
|
170
|
-
* (lockResolvedRow: cwd rule, executor id, spec); only its POST goes to the create route. A
|
|
171
|
-
*
|
|
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>;
|
package/dist/lib/durable/live.js
CHANGED
|
@@ -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
|
|
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
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
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,
|
|
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,
|
|
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
|
|
569
|
-
* one request (the create route with the lock verb's body; the api commits both
|
|
570
|
-
* transaction), then attaches to the seat like any lock instead of spawning. No tracked row,
|
|
571
|
-
* attended claude, so no turn
|
|
572
|
-
* (lockResolvedRow: cwd rule, executor id, spec); only its POST goes to the create route. A
|
|
573
|
-
*
|
|
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
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
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;
|
|
@@ -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:
|
|
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
|
+
}
|
package/dist/lib/runner/job.d.ts
CHANGED
|
@@ -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;
|
|
@@ -63,8 +68,8 @@ export declare function authRefusal(r: HttpResult): RefusedReason | null;
|
|
|
63
68
|
*/
|
|
64
69
|
export declare function runJob(sessionId: string, opts: JobOptions, deps: JobDeps): Promise<JobResult>;
|
|
65
70
|
/**
|
|
66
|
-
* Report a failed
|
|
67
|
-
* Durable lead's to rule (A3 accepts only transcript_lost today); a
|
|
68
|
-
* never changes the outcome, because the lease lapses on its own.
|
|
71
|
+
* Report a failed job after claim (34.12 (6), (10)); a refusal releases the lease instead. The
|
|
72
|
+
* fail reason vocabulary is the Durable lead's to rule (A3 accepts only transcript_lost today); a
|
|
73
|
+
* fail the api refuses is logged and never changes the outcome, because the lease lapses on its own.
|
|
69
74
|
*/
|
|
70
|
-
export declare function reportFail(deps: JobDeps, sessionId: string, epoch: number, kind: "runner_failed"
|
|
75
|
+
export declare function reportFail(deps: JobDeps, sessionId: string, epoch: number, kind: "runner_failed", reason: string): Promise<void>;
|
package/dist/lib/runner/job.js
CHANGED
|
@@ -11,7 +11,8 @@
|
|
|
11
11
|
* Every side effect is in `JobDeps`, so each arm is tested; `liveJobDeps` (commands/agent-run.ts)
|
|
12
12
|
* is the thin driver.
|
|
13
13
|
*/
|
|
14
|
-
import { RUNNER_OPEN_MODE_TOLERATED, claimNeedsKeyRetry, } from "../../vendor/durable-contract/index.js";
|
|
14
|
+
import { EXECUTOR_WRITABLE_KINDS, RUNNER_OPEN_MODE_TOLERATED, claimNeedsKeyRetry, } from "../../vendor/durable-contract/index.js";
|
|
15
|
+
import { spawnIdempotencyKey } from "./exec-append.js";
|
|
15
16
|
import { canonicalJson } from "../canonical-json.js";
|
|
16
17
|
import { verifyServedPhaseBundle } from "./bundle.js";
|
|
17
18
|
import { sealedContractHeader } from "./contract-header.js";
|
|
@@ -133,6 +134,8 @@ async function claimSession(deps, sessionId) {
|
|
|
133
134
|
},
|
|
134
135
|
};
|
|
135
136
|
}
|
|
137
|
+
/** A header-safe agent id; the gateway compares it with the lease's session. */
|
|
138
|
+
const AGENT_ID_RE = /^[A-Za-z0-9_.:-]{1,128}$/;
|
|
136
139
|
function parseJob(body) {
|
|
137
140
|
if (!body || typeof body !== "object")
|
|
138
141
|
return null;
|
|
@@ -147,7 +150,11 @@ function parseJob(body) {
|
|
|
147
150
|
return null;
|
|
148
151
|
if (sha === null || version === null || typeof b.contract !== "string")
|
|
149
152
|
return null;
|
|
150
|
-
|
|
153
|
+
// No agent id, no job: a model request without x-mnemom-agent-id would be refused anyway.
|
|
154
|
+
const agentId = str(b.agent_id);
|
|
155
|
+
if (agentId === null || !AGENT_ID_RE.test(agentId))
|
|
156
|
+
return null;
|
|
157
|
+
return { repo, agent_id: agentId, contract_sha256: sha, version, contract: b.contract };
|
|
151
158
|
}
|
|
152
159
|
/**
|
|
153
160
|
* Run one candidate session to an outcome. Never throws for a Mnemom error: every one is an
|
|
@@ -189,15 +196,19 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
189
196
|
};
|
|
190
197
|
const refuse = async (reason, detail) => {
|
|
191
198
|
deps.log(`refused (${reason}): ${detail}`);
|
|
192
|
-
|
|
199
|
+
// A refusal hands the lease back: S1's fail reasons are closed, and the api refuses
|
|
200
|
+
// runner_refused as durable_reason_invalid, which left the session leased until it lapsed.
|
|
201
|
+
await release(deps, sessionId, claim.epoch);
|
|
193
202
|
return { kind: "outcome", outcome: refusedOutcome(facts, reason) };
|
|
194
203
|
};
|
|
195
204
|
try {
|
|
196
205
|
// Register the spawn, then hello: the session moves to running and the lease is ours.
|
|
197
206
|
const ap = await deps.api.append(sessionId, {
|
|
198
|
-
kind:
|
|
199
|
-
|
|
207
|
+
kind: EXECUTOR_WRITABLE_KINDS[0], // executor.spawned
|
|
208
|
+
refs: { spawn_id: spawnId },
|
|
209
|
+
idempotency_key: spawnIdempotencyKey(sessionId, claim.epoch, spawnId),
|
|
200
210
|
expected_epoch: claim.epoch,
|
|
211
|
+
nonce: deps.nonce,
|
|
201
212
|
});
|
|
202
213
|
if (!ok2xx(ap))
|
|
203
214
|
return await refuse(routeRefusal(ap), `register spawn: ${ap.code ?? ap.status}`);
|
|
@@ -279,7 +290,11 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
279
290
|
await reportFail(deps, sessionId, claim.epoch, "runner_failed", reason);
|
|
280
291
|
return { kind: "outcome", outcome: failedOutcome(facts, reason) };
|
|
281
292
|
}
|
|
282
|
-
const state = {
|
|
293
|
+
const state = {
|
|
294
|
+
contractValue: header.value,
|
|
295
|
+
leaseKey: claim.leaseKey,
|
|
296
|
+
agentId: job.agent_id,
|
|
297
|
+
};
|
|
283
298
|
proxy = await deps.startProxy(state);
|
|
284
299
|
let opensPr = false;
|
|
285
300
|
for (const phase of phases) {
|
|
@@ -309,7 +324,7 @@ export async function runJob(sessionId, opts, deps) {
|
|
|
309
324
|
if (proxy.contractMismatch && !amended) {
|
|
310
325
|
// 34.11: the charter moved. Continue only on a higher sealed version with both the job
|
|
311
326
|
// and a fresh bundle bound to it; then the phase runs again under the new seal.
|
|
312
|
-
const next = await amendedSeal(deps, sessionId, claim, bundle);
|
|
327
|
+
const next = await amendedSeal(deps, sessionId, claim, bundle, job.agent_id);
|
|
313
328
|
if (!next)
|
|
314
329
|
return await refuse("contract_mismatch_unrecoverable", "the amended seal did not check out");
|
|
315
330
|
state.contractValue = next.value;
|
|
@@ -381,19 +396,29 @@ async function fetchBundle(deps, sessionId, claim) {
|
|
|
381
396
|
detail: `jwks: ${jwksRes.networkError ?? jwksRes.status}`,
|
|
382
397
|
};
|
|
383
398
|
}
|
|
384
|
-
if (
|
|
385
|
-
|
|
399
|
+
if (deps.executorId !== null &&
|
|
400
|
+
claim.executorId !== null &&
|
|
401
|
+
deps.executorId !== claim.executorId) {
|
|
402
|
+
return {
|
|
403
|
+
kind: "refused",
|
|
404
|
+
reason: "bundle_binding_mismatch",
|
|
405
|
+
detail: "the claim answer names another executor_id than this runner's",
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
const executorId = deps.executorId ?? claim.executorId;
|
|
409
|
+
if (executorId === null) {
|
|
410
|
+
// Neither the runner nor the claim answer names the executor id, so the bundle cannot be bound.
|
|
386
411
|
return {
|
|
387
412
|
kind: "refused",
|
|
388
413
|
reason: "bundle_binding_mismatch",
|
|
389
|
-
detail:
|
|
414
|
+
detail: `no executor_id: set MNEMOM_EXECUTOR_ID, the claim answer named none`,
|
|
390
415
|
};
|
|
391
416
|
}
|
|
392
417
|
const v = verifyServedPhaseBundle(res.text, jwksRes.body, {
|
|
393
418
|
org_id: orgOf(res.body),
|
|
394
419
|
session_id: sessionId,
|
|
395
420
|
epoch: claim.epoch,
|
|
396
|
-
executor_id:
|
|
421
|
+
executor_id: executorId,
|
|
397
422
|
contract_sha256: claim.contractSha256,
|
|
398
423
|
contract_version: claim.contractVersion,
|
|
399
424
|
}, { now: deps.now });
|
|
@@ -410,11 +435,14 @@ function orgOf(body) {
|
|
|
410
435
|
const o = body?.org_id;
|
|
411
436
|
return typeof o === "string" ? o : "";
|
|
412
437
|
}
|
|
413
|
-
async function amendedSeal(deps, sessionId, claim, running) {
|
|
438
|
+
async function amendedSeal(deps, sessionId, claim, running, agentId) {
|
|
414
439
|
const jr = await deps.api.job(sessionId);
|
|
415
440
|
const job = jr.status === 200 ? parseJob(jr.body) : null;
|
|
416
441
|
if (!job || job.version <= claim.contractVersion)
|
|
417
442
|
return null;
|
|
443
|
+
// An amend never moves the session to another agent.
|
|
444
|
+
if (job.agent_id !== agentId)
|
|
445
|
+
return null;
|
|
418
446
|
const fresh = await fetchBundle(deps, sessionId, {
|
|
419
447
|
...claim,
|
|
420
448
|
contractSha256: job.contract_sha256,
|
|
@@ -438,9 +466,9 @@ function sameWork(a, b) {
|
|
|
438
466
|
canonicalJson({ workflow: b.workflow, phases: b.phases }));
|
|
439
467
|
}
|
|
440
468
|
/**
|
|
441
|
-
* Report a failed
|
|
442
|
-
* Durable lead's to rule (A3 accepts only transcript_lost today); a
|
|
443
|
-
* never changes the outcome, because the lease lapses on its own.
|
|
469
|
+
* Report a failed job after claim (34.12 (6), (10)); a refusal releases the lease instead. The
|
|
470
|
+
* fail reason vocabulary is the Durable lead's to rule (A3 accepts only transcript_lost today); a
|
|
471
|
+
* fail the api refuses is logged and never changes the outcome, because the lease lapses on its own.
|
|
444
472
|
*/
|
|
445
473
|
export async function reportFail(deps, sessionId, epoch, kind, reason) {
|
|
446
474
|
const r = await deps.api.fail(sessionId, {
|
|
@@ -8,14 +8,15 @@
|
|
|
8
8
|
* is oidc_<code> for S1's OIDC_EXCHANGE_REFUSALS, anything else oidc_unavailable. The token never
|
|
9
9
|
* reaches argv, a log line or an outcome.
|
|
10
10
|
*
|
|
11
|
-
* S1 names the route but not its answer's
|
|
12
|
-
* with the Durable lead until S1 freezes it (no api handler exists yet to read it from).
|
|
11
|
+
* S1 names the route but not its answer's fields; `executor_key` (and `executor_id`, which binds
|
|
12
|
+
* the bundle) is this runner's reading, open with the Durable lead until S1 freezes it (no api handler exists yet to read it from).
|
|
13
13
|
*/
|
|
14
14
|
import { type FetchFn } from "./executor-client.js";
|
|
15
15
|
import type { RefusedReason } from "./outcome.js";
|
|
16
16
|
export type OidcExchange = {
|
|
17
17
|
ok: true;
|
|
18
18
|
executorKey: string;
|
|
19
|
+
executorId: string | null;
|
|
19
20
|
} | {
|
|
20
21
|
ok: false;
|
|
21
22
|
reason: RefusedReason;
|