@mnemom/mnemom 0.17.32-next.2 → 0.17.32-next.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent.js +174 -67
- package/dist/durable/adopt-api.d.ts +37 -0
- package/dist/durable/adopt-api.js +106 -0
- package/dist/durable/adopt-live.d.ts +17 -0
- package/dist/durable/adopt-live.js +152 -0
- package/dist/durable/adopt.d.ts +305 -0
- package/dist/durable/adopt.js +679 -0
- package/dist/durable/cwd-rule.d.ts +65 -0
- package/dist/durable/cwd-rule.js +111 -0
- package/dist/durable/discover.d.ts +68 -0
- package/dist/durable/discover.js +123 -0
- package/dist/durable/gate.d.ts +8 -0
- package/dist/durable/gate.js +13 -0
- package/dist/durable/launch-spec.d.ts +137 -0
- package/dist/durable/launch-spec.js +277 -0
- package/dist/durable/proc.d.ts +86 -0
- package/dist/durable/proc.js +279 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +52 -1
- package/dist/lib/agent.d.ts +7 -0
- package/dist/lib/agent.js +26 -0
- package/dist/lib/api.d.ts +32 -0
- package/dist/lib/api.js +16 -0
- package/dist/lib/experimental.d.ts +1 -1
- package/dist/lib/experimental.js +13 -0
- package/dist/lib/session-key.d.ts +68 -0
- package/dist/lib/session-key.js +101 -0
- package/dist/lib/statusline.d.ts +13 -0
- package/dist/lib/statusline.js +27 -3
- package/dist/lib/subscription-login.d.ts +1 -1
- package/dist/lib/subscription-login.js +4 -1
- package/dist/vendor/durable-contract/index.js +5663 -0
- package/package.json +5 -4
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The thin live driver for `sessions adopt`: real processes, files, HTTP and the terminal. The
|
|
3
|
+
* decisions live in adopt.ts (pure over AdoptDeps); this file only wires them to the machine.
|
|
4
|
+
*/
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
6
|
+
import { accessSync, constants, readFileSync, realpathSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { createInterface } from "node:readline";
|
|
9
|
+
import { parse as parseToml } from "smol-toml";
|
|
10
|
+
import { executorStateOk, executorStatePath, LAUNCHER_KEYS, } from "../vendor/durable-contract/index.js";
|
|
11
|
+
import { getApiUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
12
|
+
import { resolveAuth } from "../lib/auth.js";
|
|
13
|
+
import { readSessionRecords } from "../lib/agent-ledger.js";
|
|
14
|
+
import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
|
|
15
|
+
import { mnemomFetch } from "../lib/version-gate.js";
|
|
16
|
+
import { httpAdoptApi } from "./adopt-api.js";
|
|
17
|
+
import { liveCwdDeps } from "./cwd-rule.js";
|
|
18
|
+
import { findLiveClaudes, liveSnapshot } from "./discover.js";
|
|
19
|
+
import { liveProcDeps, processTableWithStarts, readProcIdentity, readProcStart } from "./proc.js";
|
|
20
|
+
/** The ledger facts for a conversation: its newest record. */
|
|
21
|
+
export function ledgerFactsFrom(records, cid) {
|
|
22
|
+
let r;
|
|
23
|
+
for (const x of records)
|
|
24
|
+
if (x.conversation_id === cid)
|
|
25
|
+
r = x;
|
|
26
|
+
if (!r)
|
|
27
|
+
return null;
|
|
28
|
+
const goalMode = r.goal_mode ?? (r.contract ? "on" : "off");
|
|
29
|
+
return {
|
|
30
|
+
agentId: r.agent_id,
|
|
31
|
+
agentLabel: r.agent_slug,
|
|
32
|
+
orgId: r.org_id,
|
|
33
|
+
goalMode,
|
|
34
|
+
requirements: r.contract?.requirements ? r.contract.requirements.length : null,
|
|
35
|
+
launchShape: r.launch_shape,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** The launcher key `durable` (MNEMOM_AGENT_DURABLE, then launcher.toml), default off. */
|
|
39
|
+
export function launcherKeyFrom(env, tomlText) {
|
|
40
|
+
const ok = (v) => typeof v === "string" && LAUNCHER_KEYS.includes(v);
|
|
41
|
+
const e = env.MNEMOM_AGENT_DURABLE?.trim().toLowerCase();
|
|
42
|
+
if (ok(e))
|
|
43
|
+
return e;
|
|
44
|
+
if (tomlText !== null) {
|
|
45
|
+
try {
|
|
46
|
+
const v = parseToml(tomlText).durable;
|
|
47
|
+
if (ok(v))
|
|
48
|
+
return v;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// an unparseable launcher.toml is the launcher's error to report, not adopt's
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return "off";
|
|
55
|
+
}
|
|
56
|
+
/** The executor state file, or null when absent or not the frozen S1 shape. */
|
|
57
|
+
export function executorStateFrom(text) {
|
|
58
|
+
if (text === null)
|
|
59
|
+
return null;
|
|
60
|
+
try {
|
|
61
|
+
const v = JSON.parse(text);
|
|
62
|
+
return executorStateOk(v) ? v : null;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const readOrNull = (p) => {
|
|
69
|
+
try {
|
|
70
|
+
return readFileSync(p, "utf8");
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
export async function authHeaders() {
|
|
77
|
+
const cred = await resolveAuth();
|
|
78
|
+
if (cred.type === "jwt")
|
|
79
|
+
return { Authorization: `Bearer ${cred.token.trim()}` };
|
|
80
|
+
if (cred.type === "api-key")
|
|
81
|
+
return { "X-Mnemom-Api-Key": cred.key.trim() };
|
|
82
|
+
return {};
|
|
83
|
+
}
|
|
84
|
+
/** A [Y/n] answer: anything not starting with n is yes. */
|
|
85
|
+
export function answerIsYes(a) {
|
|
86
|
+
return !/^\s*n/i.test(a);
|
|
87
|
+
}
|
|
88
|
+
function ask(question) {
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
91
|
+
rl.question(question, (a) => {
|
|
92
|
+
// Settle before close(): readline emits "close" synchronously, and the close handler
|
|
93
|
+
// below would otherwise settle the prompt as declined first.
|
|
94
|
+
resolve(answerIsYes(a));
|
|
95
|
+
rl.close();
|
|
96
|
+
});
|
|
97
|
+
rl.on("close", () => resolve(false));
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
export function liveAdoptDeps() {
|
|
101
|
+
const home = homedir();
|
|
102
|
+
return {
|
|
103
|
+
discover: () => findLiveClaudes(liveSnapshot(), process.pid),
|
|
104
|
+
ledger: (cid) => ledgerFactsFrom(readSessionRecords({ dir: MNEMOM_DIR }), cid),
|
|
105
|
+
api: httpAdoptApi({
|
|
106
|
+
base: getApiUrl(),
|
|
107
|
+
auth: authHeaders,
|
|
108
|
+
fetch: (u, i) => mnemomFetch(u, i),
|
|
109
|
+
idempotencyKey: randomUUID,
|
|
110
|
+
}),
|
|
111
|
+
cwd: (live) => {
|
|
112
|
+
const env = { ...process.env };
|
|
113
|
+
delete env.CLAUDE_CONFIG_DIR;
|
|
114
|
+
if (live.env.CLAUDE_CONFIG_DIR)
|
|
115
|
+
env.CLAUDE_CONFIG_DIR = live.env.CLAUDE_CONFIG_DIR;
|
|
116
|
+
return liveCwdDeps(env);
|
|
117
|
+
},
|
|
118
|
+
realpath: (p) => {
|
|
119
|
+
try {
|
|
120
|
+
return realpathSync(p);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
executorState: () => executorStateFrom(readOrNull(executorStatePath(home))),
|
|
127
|
+
// Not wired: S1 ExecutorState and no other frozen host-state shape records the pinned claude
|
|
128
|
+
// path (gaps/c4-pinned-binary-state.md). The check is then the daemon's, at spawn.
|
|
129
|
+
pinnedBinary: () => null,
|
|
130
|
+
binaryRunnable: (p) => {
|
|
131
|
+
try {
|
|
132
|
+
accessSync(p, constants.X_OK);
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
ownCheckout: () => liveCwdDeps().gitToplevel(process.cwd()),
|
|
140
|
+
launcherKey: () => launcherKeyFrom(process.env, readOrNull(LAUNCHER_CONFIG_PATH)),
|
|
141
|
+
processTable: () => processTableWithStarts(liveProcDeps),
|
|
142
|
+
identity: (pid) => readProcIdentity(pid, liveProcDeps),
|
|
143
|
+
startOf: (pid) => readProcStart(pid, liveProcDeps),
|
|
144
|
+
signal: (pid, sig) => process.kill(pid, sig),
|
|
145
|
+
confirm: (q) => (process.stdin.isTTY ? ask(q) : Promise.resolve(false)),
|
|
146
|
+
out: (l) => process.stdout.write(`${l}\n`),
|
|
147
|
+
err: (l) => process.stderr.write(`${l}\n`),
|
|
148
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
149
|
+
now: () => Date.now(),
|
|
150
|
+
selfPid: process.pid,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent sessions adopt` (BACKBONE C4; C14-R12..R25, R20, R21; C18-R10).
|
|
3
|
+
*
|
|
4
|
+
* Plan (no side effects; `--dry-run` stops here): the live tracked claudes on this machine (the
|
|
5
|
+
* step-2 discovery), grouped one table per agent. For each session: its row (A2 GET), the C4 cwd
|
|
6
|
+
* rule with the transcript-slug check, the spec built from the original's exact argv by the one
|
|
7
|
+
* builder, and the Note ('approvals with you; flags dropped: …', S1 grammar). A group holding a
|
|
8
|
+
* session that fails the cwd rule or is otherwise unadoptable is skipped whole (R15), unless
|
|
9
|
+
* --own-agent moves its adoptable sessions to their own agent. The group checks (R17 sharing,
|
|
10
|
+
* R18 lock preconditions) run through the non-mutating lock check before anything is stopped;
|
|
11
|
+
* a refusal prints its one-line fix and touches nothing. The check fails closed: a check route the
|
|
12
|
+
* API does not serve (404/405 with no durable code), a timeout or a dropped response refuses the
|
|
13
|
+
* group as durable_precondition_unavailable, so an original whose group checks did not run is
|
|
14
|
+
* never stopped.
|
|
15
|
+
*
|
|
16
|
+
* A live original whose row is already locked (C14-R11 re-run): its spec is built from its exact
|
|
17
|
+
* argv and compared with the committed charter's launch_spec by the S1 specHash. Equal is a no-op
|
|
18
|
+
* (no check, no lock request, no signal). Different is refused 422-style on stderr
|
|
19
|
+
* (durable_unsupported {feature: launch_spec_change} and the S1 launch_spec_change fix line) and
|
|
20
|
+
* the command exits 1, again with no request and no signal. A committed spec that cannot be read
|
|
21
|
+
* is refused the same way as durable_precondition_unavailable.
|
|
22
|
+
*
|
|
23
|
+
* Run (after one [Y/n] per adoptable group): per session the U01 handover order (R25): the
|
|
24
|
+
* pinned-binary check ran in the plan (wired only when a pinned path is known: S1 freezes no host
|
|
25
|
+
* state field for it, see adopt-live.ts), SIGSTOP of the original claude pid only (an original
|
|
26
|
+
* that exited meanwhile is reported and skipped), the lock request (retried under one
|
|
27
|
+
* Idempotency-Key until an answer is decisive, see lockAnswerKind), SIGCONT only on a 4xx
|
|
28
|
+
* refusal, then the seat's hello (the row reaches running) or ATTACH_WAIT_S, then SIGKILL of the
|
|
29
|
+
* original's whole tree minus this process. Every signal re-checks its pid first (the original's
|
|
30
|
+
* identity; each tree pid's start time against the snapshot) and a pid that no longer matches is
|
|
31
|
+
* skipped and named, never signalled. The handover fails closed: when no attempt is
|
|
32
|
+
* decisive a request may still be in flight, so a re-read that shows the row locked proceeds and
|
|
33
|
+
* anything else leaves the original stopped (never two running copies of one conversation);
|
|
34
|
+
* adopt then names the pid and the way out.
|
|
35
|
+
*
|
|
36
|
+
* Every code adopt prints is an S1 code and every fix line an S1 catalogue entry rendered once
|
|
37
|
+
* (renderFixLine); a line whose parameters are unknown is not printed (C11-R27).
|
|
38
|
+
*
|
|
39
|
+
* On a host home (an executor state file present) the summary prints the current S1 risk line
|
|
40
|
+
* verbatim; adopt writes no seen record and never reads the stored risk-line version (R20).
|
|
41
|
+
*/
|
|
42
|
+
import { type CwdKind, type ExecutorState, type FixKey, type LauncherKey, type LaunchSpec, type LockBody } from "../vendor/durable-contract/index.js";
|
|
43
|
+
import { type CwdDeps } from "./cwd-rule.js";
|
|
44
|
+
import { type LiveClaude } from "./discover.js";
|
|
45
|
+
import { type ProcIdentity, type ProcRow } from "./proc.js";
|
|
46
|
+
/** What the local launch ledger knows about a conversation. */
|
|
47
|
+
export interface LedgerFacts {
|
|
48
|
+
agentId: string | null;
|
|
49
|
+
agentLabel: string;
|
|
50
|
+
orgId: string;
|
|
51
|
+
goalMode: "off" | "on" | "implicit";
|
|
52
|
+
requirements: number | null;
|
|
53
|
+
launchShape: string;
|
|
54
|
+
}
|
|
55
|
+
/** The fields of the A2 session row adopt reads (absent fields are unknown, never assumed). */
|
|
56
|
+
export interface SessionRow {
|
|
57
|
+
agent_id?: string;
|
|
58
|
+
name?: string;
|
|
59
|
+
cc_session_id?: string | null;
|
|
60
|
+
durable_mode?: "tracked" | "locked";
|
|
61
|
+
state?: string;
|
|
62
|
+
effective_state?: string;
|
|
63
|
+
lease_seconds?: number;
|
|
64
|
+
/** The committed charter's public commitments (A2 publicView); launch_spec among them. */
|
|
65
|
+
charter?: {
|
|
66
|
+
launch_spec?: unknown;
|
|
67
|
+
} | null;
|
|
68
|
+
}
|
|
69
|
+
export type ApiAnswer = {
|
|
70
|
+
kind: "ok";
|
|
71
|
+
status: number;
|
|
72
|
+
body: unknown;
|
|
73
|
+
} | {
|
|
74
|
+
kind: "error";
|
|
75
|
+
status: number;
|
|
76
|
+
code: string | null;
|
|
77
|
+
details: unknown;
|
|
78
|
+
message: string;
|
|
79
|
+
} | {
|
|
80
|
+
kind: "timeout";
|
|
81
|
+
} | {
|
|
82
|
+
kind: "network";
|
|
83
|
+
message: string;
|
|
84
|
+
};
|
|
85
|
+
export interface AdoptApi {
|
|
86
|
+
getSession(cid: string, facts: LedgerFacts): Promise<ApiAnswer>;
|
|
87
|
+
/** The non-mutating lock check (group checks before any signal); same body as lock. */
|
|
88
|
+
lockCheck(cid: string, body: LockBody): Promise<ApiAnswer>;
|
|
89
|
+
/** One idempotencyKey per handover: every retry of one lock sends the same key. */
|
|
90
|
+
lock(cid: string, body: LockBody, idempotencyKey?: string): Promise<ApiAnswer>;
|
|
91
|
+
}
|
|
92
|
+
export interface AdoptDeps {
|
|
93
|
+
discover(): LiveClaude[];
|
|
94
|
+
ledger(cid: string): LedgerFacts | null;
|
|
95
|
+
api: AdoptApi;
|
|
96
|
+
/** The cwd-rule deps for one original (its own CLAUDE_CONFIG_DIR locates the transcript). */
|
|
97
|
+
cwd(live: LiveClaude): CwdDeps;
|
|
98
|
+
realpath(p: string): string | null;
|
|
99
|
+
executorState(): ExecutorState | null;
|
|
100
|
+
/**
|
|
101
|
+
* The pinned claude path recorded at host install, or null when this machine records none (the
|
|
102
|
+
* check is then the daemon's, at spawn; a missing pinned binary there takes the respawn path).
|
|
103
|
+
*/
|
|
104
|
+
pinnedBinary(): string | null;
|
|
105
|
+
binaryRunnable(path: string): boolean;
|
|
106
|
+
/** This command's own git checkout (top level), offered for a not_git fix line, or null. */
|
|
107
|
+
ownCheckout(): string | null;
|
|
108
|
+
launcherKey(): LauncherKey;
|
|
109
|
+
processTable(): ProcRow[];
|
|
110
|
+
/** A pid's identity read now (start time and argv[0]), or null when it is gone. */
|
|
111
|
+
identity(pid: number): ProcIdentity | null;
|
|
112
|
+
/** A pid's start time read now (as processTable's start reads it), or null when it is gone. */
|
|
113
|
+
startOf(pid: number): string | null;
|
|
114
|
+
signal(pid: number, sig: "SIGSTOP" | "SIGCONT" | "SIGKILL"): void;
|
|
115
|
+
confirm(question: string): Promise<boolean>;
|
|
116
|
+
out(line: string): void;
|
|
117
|
+
err(line: string): void;
|
|
118
|
+
sleep(ms: number): Promise<void>;
|
|
119
|
+
now(): number;
|
|
120
|
+
selfPid: number;
|
|
121
|
+
}
|
|
122
|
+
export interface AdoptOptions {
|
|
123
|
+
dryRun?: boolean;
|
|
124
|
+
json?: boolean;
|
|
125
|
+
ownAgent?: boolean;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* spec_change: an already-locked session whose live original's spec differs from the committed
|
|
129
|
+
* one, or could not be compared (refused on stderr, nothing touched, exit 1).
|
|
130
|
+
*/
|
|
131
|
+
export type SessionVerdict = "adopt" | "skip" | "unadoptable" | "durable" | "spec_change";
|
|
132
|
+
export interface PlannedSession {
|
|
133
|
+
name: string;
|
|
134
|
+
conversation_id: string;
|
|
135
|
+
/** The Claude Code session id the seat resumes (null when the row has none: unadoptable). */
|
|
136
|
+
cc_session_id: string | null;
|
|
137
|
+
pid: number;
|
|
138
|
+
cwd: string | null;
|
|
139
|
+
/** "root" when the cwd rule passed, the S1 cwd_kind when it failed, null when not reached. */
|
|
140
|
+
cwd_kind: "root" | CwdKind | null;
|
|
141
|
+
verdict: SessionVerdict;
|
|
142
|
+
/** Why the session cannot be adopted (unadoptable / durable), or null. */
|
|
143
|
+
reason: string | null;
|
|
144
|
+
goal: "sealed" | "implicit" | "none";
|
|
145
|
+
rules: string;
|
|
146
|
+
note: string;
|
|
147
|
+
spec: LaunchSpec | null;
|
|
148
|
+
spec_hash: string | null;
|
|
149
|
+
dropped: string[];
|
|
150
|
+
fix: string | null;
|
|
151
|
+
/** Internal: what the handover needs. */
|
|
152
|
+
readonly _live?: LiveClaude;
|
|
153
|
+
readonly _row?: SessionRow;
|
|
154
|
+
}
|
|
155
|
+
export interface PlannedGroup {
|
|
156
|
+
agent_id: string | null;
|
|
157
|
+
agent: string;
|
|
158
|
+
verdict: "adopt" | "skip" | "refuse" | "none";
|
|
159
|
+
/** Why the group is skipped or refused, or null. */
|
|
160
|
+
reason: string | null;
|
|
161
|
+
code: string | null;
|
|
162
|
+
fix: string | null;
|
|
163
|
+
own_agent: boolean;
|
|
164
|
+
sessions: PlannedSession[];
|
|
165
|
+
}
|
|
166
|
+
export interface AdoptPlan {
|
|
167
|
+
groups: PlannedGroup[];
|
|
168
|
+
/** The S1 risk line on a host home, else null. */
|
|
169
|
+
risk_line: string | null;
|
|
170
|
+
}
|
|
171
|
+
/** The cwd the rule checks: the claude's PWD when it resolves to the process cwd (a symlink kept), else the process cwd. */
|
|
172
|
+
export declare function ruleCwd(live: LiveClaude, realpath: (p: string) => string | null): string | null;
|
|
173
|
+
/** The session row of a GET answer: the body itself, or its `session`; agent_id also from `agent.id`. */
|
|
174
|
+
export declare function rowOf(a: ApiAnswer): SessionRow | null;
|
|
175
|
+
/** What a refusal's fix line may be filled from. */
|
|
176
|
+
export interface RefusalContext {
|
|
177
|
+
name: string;
|
|
178
|
+
launcherKey: LauncherKey;
|
|
179
|
+
/** The group's agent (for agent_contained when the refusal names none). */
|
|
180
|
+
agent?: string;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* The S1 fix line for a lock refusal answered by the check or the lock route, or null when the
|
|
184
|
+
* code has none or a parameter it needs is unknown (no placeholder is ever printed).
|
|
185
|
+
*/
|
|
186
|
+
export declare function refusalFix(code: string | null, details: unknown, ctx: RefusalContext): string | null;
|
|
187
|
+
/**
|
|
188
|
+
* The precheck route answered 404/405 with no durable code: the API does not serve it (see
|
|
189
|
+
* PRECHECK note in adopt-api.ts; gaps/c4-adopt-precheck-route.md). Adopt fails closed on it.
|
|
190
|
+
*/
|
|
191
|
+
export declare function isPrecheckMissing(a: ApiAnswer): boolean;
|
|
192
|
+
/** The lock body for a planned session (S1 LOCK_BODY_KEYS: spec, executor_id, own_agent). */
|
|
193
|
+
export declare function lockBodyFor(s: PlannedSession, executorId: string, ownAgent: boolean): LockBody;
|
|
194
|
+
/** Build the plan: discovery, per-session checks, grouping and the group checks. No side effects. */
|
|
195
|
+
export declare function planAdopt(deps: AdoptDeps, opts?: AdoptOptions): Promise<AdoptPlan>;
|
|
196
|
+
/** The --json document (internal fields removed). */
|
|
197
|
+
export declare function planJson(plan: AdoptPlan): unknown;
|
|
198
|
+
/** One table per agent group (R12): Session, Goal, Rules, Note; Note last and unpadded. */
|
|
199
|
+
export declare function renderGroup(g: PlannedGroup): string[];
|
|
200
|
+
export declare function renderPlan(plan: AdoptPlan): string[];
|
|
201
|
+
export type HandoverResult =
|
|
202
|
+
/** killed false: the original's pid named another process (or none) before SIGKILL; none sent. */
|
|
203
|
+
{
|
|
204
|
+
ok: true;
|
|
205
|
+
name: string;
|
|
206
|
+
hello: boolean;
|
|
207
|
+
killed: boolean;
|
|
208
|
+
} | {
|
|
209
|
+
ok: false;
|
|
210
|
+
name: string;
|
|
211
|
+
committed: false;
|
|
212
|
+
code: string;
|
|
213
|
+
fix: string | null;
|
|
214
|
+
}
|
|
215
|
+
/** The original exited before it could be stopped: no lock request was sent. */
|
|
216
|
+
| {
|
|
217
|
+
ok: false;
|
|
218
|
+
name: string;
|
|
219
|
+
committed: false;
|
|
220
|
+
gone: true;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* The original's pid no longer names the process discovery saw (it ended while the confirm
|
|
224
|
+
* prompt waited, and the pid may be reused): no lock request, and nothing left stopped.
|
|
225
|
+
*/
|
|
226
|
+
| {
|
|
227
|
+
ok: false;
|
|
228
|
+
name: string;
|
|
229
|
+
committed: false;
|
|
230
|
+
ended: true;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* No lock attempt was decisive and no re-read showed the row locked: the lock may still commit,
|
|
234
|
+
* so the original (pid) is left stopped rather than resumed beside a seat (fail closed).
|
|
235
|
+
*/
|
|
236
|
+
| {
|
|
237
|
+
ok: false;
|
|
238
|
+
name: string;
|
|
239
|
+
committed: null;
|
|
240
|
+
unconfirmed: true;
|
|
241
|
+
pid: number;
|
|
242
|
+
code: string;
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* The pid still names the process discovery saw: the same start time and argv[0]. A pid is reused
|
|
246
|
+
* once its process exits, so a bare pid is never signalled on its own (pid reuse guard).
|
|
247
|
+
*/
|
|
248
|
+
export declare function sameProcess(live: LiveClaude, now: ProcIdentity | null): boolean;
|
|
249
|
+
/** Re-reads of the row after the lock attempts ran out undecided, REREAD_GAP_MS apart. */
|
|
250
|
+
export declare const REREAD_ATTEMPTS = 3;
|
|
251
|
+
export declare const REREAD_GAP_MS = 1000;
|
|
252
|
+
/**
|
|
253
|
+
* What the server says about a lock whose answer never came. Only a row that carries its
|
|
254
|
+
* durable_mode counts as an answer; a failed read, or a row without the field, is no answer.
|
|
255
|
+
*/
|
|
256
|
+
export declare function commitOutcome(cid: string, deps: AdoptDeps, facts: LedgerFacts): Promise<"committed" | "not_committed" | "unknown">;
|
|
257
|
+
/** Lock attempts per handover (the first plus same-key retries) and the gaps between them. */
|
|
258
|
+
export declare const LOCK_RETRY_GAPS_MS: readonly [2000, 5000, 10000];
|
|
259
|
+
export declare const LOCK_ATTEMPTS: number;
|
|
260
|
+
/**
|
|
261
|
+
* How one lock answer reads:
|
|
262
|
+
* - committed: any 2xx. A same-key replay of the first answer, or, once the api's 30 s window
|
|
263
|
+
* has passed and the retry re-runs the lock, the already-locked noop ({changed:false}), which
|
|
264
|
+
* the api answers only when the session is locked with this exact launch spec.
|
|
265
|
+
* - refused: a 4xx refusal; the lock did not commit, so the original is resumed. Also
|
|
266
|
+
* durable_precondition_unavailable on the first attempt: the api answers it only before the
|
|
267
|
+
* commit write (the locked step runs before every probe), and no earlier request is in flight.
|
|
268
|
+
* - pending: 409 idempotency_key_in_flight, the first request is still running; never a refusal.
|
|
269
|
+
* - ambiguous: a timeout, a network error, any other 5xx (a 503 can follow a commit whose answer
|
|
270
|
+
* was lost), durable_precondition_unavailable on a retry (an earlier request may still commit),
|
|
271
|
+
* or 409 durable_retry (another lock on this session committed while this one ran).
|
|
272
|
+
*/
|
|
273
|
+
export type LockAnswerKind = "committed" | "refused" | "pending" | "ambiguous";
|
|
274
|
+
/** A lock answer read once: a refusal always carries the error answer it came from. */
|
|
275
|
+
export type LockDecision = {
|
|
276
|
+
kind: "refused";
|
|
277
|
+
a: Extract<ApiAnswer, {
|
|
278
|
+
kind: "error";
|
|
279
|
+
}>;
|
|
280
|
+
} | {
|
|
281
|
+
kind: "committed" | "pending" | "ambiguous";
|
|
282
|
+
a: ApiAnswer;
|
|
283
|
+
};
|
|
284
|
+
/** One lock answer, read: see lockAnswerKind. */
|
|
285
|
+
export declare function lockDecision(a: ApiAnswer, first: boolean): LockDecision;
|
|
286
|
+
export declare function lockAnswerKind(a: ApiAnswer, first: boolean): LockAnswerKind;
|
|
287
|
+
/**
|
|
288
|
+
* The lock, retried under one Idempotency-Key until an answer is decisive (committed or refused)
|
|
289
|
+
* or LOCK_ATTEMPTS run out; the last answer is returned either way.
|
|
290
|
+
*/
|
|
291
|
+
export declare function lockDecided(cid: string, body: LockBody, deps: AdoptDeps): Promise<LockDecision>;
|
|
292
|
+
/** An API call that never throws (a throw reads as a dropped response). */
|
|
293
|
+
export declare function settle(f: () => Promise<ApiAnswer>): Promise<ApiAnswer>;
|
|
294
|
+
/** One session's U01 handover (R25 steps 2-5); step 1 ran in the plan. */
|
|
295
|
+
export declare function handover(s: PlannedSession, g: PlannedGroup, exec: ExecutorState, deps: AdoptDeps, facts: LedgerFacts, launcherKey: LauncherKey): Promise<HandoverResult>;
|
|
296
|
+
/**
|
|
297
|
+
* Ruling 19: the plan already holds a refusal, a refused group (lock check, executor state, pinned
|
|
298
|
+
* binary) or a locked session's changed spec (C14-R11). A group skipped by the cwd rule, or with
|
|
299
|
+
* nothing to adopt, is not a refusal.
|
|
300
|
+
*/
|
|
301
|
+
export declare function planRefused(plan: AdoptPlan): boolean;
|
|
302
|
+
/** The whole verb. Returns the exit status: 1 when anything was refused (ruling 19), else 0. */
|
|
303
|
+
export declare function adoptCommand(deps: AdoptDeps, opts?: AdoptOptions): Promise<number>;
|
|
304
|
+
/** The fix catalogue entries adopt can print (exported for the catalogue lint test). */
|
|
305
|
+
export declare const ADOPT_FIX_KEYS: readonly FixKey[];
|