@mnemom/mnemom 0.17.34-next.1 → 0.17.34-next.2
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/base-url-proxy.mjs +18 -4
- package/dist/commands/agent-config.js +5 -1
- package/dist/commands/agent-doctor.d.ts +9 -0
- package/dist/commands/agent-doctor.js +4 -0
- package/dist/commands/agent-session.d.ts +1 -2
- package/dist/commands/agent-session.js +1 -2
- package/dist/commands/agent-setup.d.ts +6 -0
- package/dist/commands/agent-setup.js +12 -1
- package/dist/commands/agent.d.ts +11 -0
- package/dist/commands/agent.js +145 -46
- package/dist/commands/durable-doctor.d.ts +50 -0
- package/dist/commands/durable-doctor.js +186 -0
- package/dist/commands/durable-lock.d.ts +254 -0
- package/dist/commands/durable-lock.js +595 -0
- package/dist/commands/durable-sessions.d.ts +87 -0
- package/dist/commands/durable-sessions.js +303 -0
- package/dist/commands/durable-verbs.d.ts +116 -0
- package/dist/commands/durable-verbs.js +393 -0
- package/dist/durable/flock.d.ts +26 -0
- package/dist/durable/flock.js +80 -0
- package/dist/durable/launch-spec.d.ts +7 -1
- package/dist/durable/launch-spec.js +10 -3
- package/dist/durable/seat-proxy-entry.d.ts +11 -0
- package/dist/durable/seat-proxy-entry.js +25 -0
- package/dist/durable/seat-proxy.d.ts +107 -0
- package/dist/durable/seat-proxy.js +599 -0
- package/dist/durable/seat-socket.d.ts +46 -0
- package/dist/durable/seat-socket.js +179 -0
- package/dist/durable/seat-spec-validator.d.ts +28 -0
- package/dist/durable/seat-spec-validator.js +59 -0
- package/dist/durable/test-mode.mjs +81 -0
- package/dist/durable/turn-chain.d.ts +55 -0
- package/dist/durable/turn-chain.js +178 -0
- package/dist/host/auto-continue.d.ts +67 -0
- package/dist/host/auto-continue.js +118 -0
- package/dist/host/bootid.d.ts +13 -0
- package/dist/host/bootid.js +41 -0
- package/dist/host/client.d.ts +123 -0
- package/dist/host/client.js +171 -0
- package/dist/host/commands.d.ts +2 -0
- package/dist/host/commands.js +122 -0
- package/dist/host/control.d.ts +33 -0
- package/dist/host/control.js +144 -0
- package/dist/host/daemon.d.ts +217 -0
- package/dist/host/daemon.js +606 -0
- package/dist/host/gate.d.ts +46 -0
- package/dist/host/gate.js +82 -0
- package/dist/host/grant.d.ts +24 -0
- package/dist/host/grant.js +24 -0
- package/dist/host/instance.d.ts +35 -0
- package/dist/host/instance.js +62 -0
- package/dist/host/integrity.d.ts +35 -0
- package/dist/host/integrity.js +115 -0
- package/dist/host/launchd.d.ts +16 -0
- package/dist/host/launchd.js +45 -0
- package/dist/host/lock.d.ts +20 -0
- package/dist/host/lock.js +63 -0
- package/dist/host/owner.d.ts +55 -0
- package/dist/host/owner.js +121 -0
- package/dist/host/plist.d.ts +16 -0
- package/dist/host/plist.js +85 -0
- package/dist/host/procs.d.ts +27 -0
- package/dist/host/procs.js +105 -0
- package/dist/host/run.d.ts +66 -0
- package/dist/host/run.js +314 -0
- package/dist/host/runtime.d.ts +8 -0
- package/dist/host/runtime.js +22 -0
- package/dist/host/s1-names.d.ts +22 -0
- package/dist/host/s1-names.js +21 -0
- package/dist/host/seat-credential.d.ts +27 -0
- package/dist/host/seat-credential.js +41 -0
- package/dist/host/seat-launch-main.d.ts +1 -0
- package/dist/host/seat-launch-main.js +59 -0
- package/dist/host/seat-launch.d.ts +116 -0
- package/dist/host/seat-launch.js +302 -0
- package/dist/host/seat.d.ts +48 -0
- package/dist/host/seat.js +149 -0
- package/dist/host/state.d.ts +33 -0
- package/dist/host/state.js +117 -0
- package/dist/host/status.d.ts +101 -0
- package/dist/host/status.js +307 -0
- package/dist/host/tmux.d.ts +56 -0
- package/dist/host/tmux.js +182 -0
- package/dist/host/transcript.d.ts +30 -0
- package/dist/host/transcript.js +130 -0
- package/dist/host/verbs.d.ts +50 -0
- package/dist/host/verbs.js +271 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +197 -35
- package/dist/lib/agent.d.ts +8 -26
- package/dist/lib/agent.js +11 -26
- package/dist/lib/claude-plugin.d.ts +11 -2
- package/dist/lib/claude-plugin.js +10 -1
- package/dist/lib/durable/api.d.ts +136 -0
- package/dist/lib/durable/api.js +220 -0
- package/dist/lib/durable/codes.d.ts +8 -0
- package/dist/lib/durable/codes.js +2 -0
- package/dist/lib/durable/first-turn.d.ts +50 -0
- package/dist/lib/durable/first-turn.js +102 -0
- package/dist/lib/durable/gate.d.ts +48 -0
- package/dist/lib/durable/gate.js +122 -0
- package/dist/lib/durable/handover.d.ts +101 -0
- package/dist/lib/durable/handover.js +175 -0
- package/dist/lib/durable/launch.d.ts +182 -0
- package/dist/lib/durable/launch.js +443 -0
- package/dist/lib/durable/launching.d.ts +15 -0
- package/dist/lib/durable/launching.js +70 -0
- package/dist/lib/durable/live.d.ts +194 -0
- package/dist/lib/durable/live.js +635 -0
- package/dist/lib/durable/refusal.d.ts +57 -0
- package/dist/lib/durable/refusal.js +78 -0
- package/dist/lib/durable/view.d.ts +83 -0
- package/dist/lib/durable/view.js +244 -0
- package/dist/lib/experimental.d.ts +0 -9
- package/dist/lib/experimental.js +18 -17
- package/dist/lib/launcher-config.d.ts +7 -0
- package/dist/lib/launcher-config.js +21 -0
- package/dist/lib/scan/report-email.d.ts +1 -1
- package/dist/lib/scan/report-email.js +9 -1
- package/dist/lib/subscription-login.d.ts +15 -0
- package/dist/lib/subscription-login.js +28 -1
- package/dist/vendor/durable-contract/index.js +240 -18
- package/dist/vendor/shared/durable-contract/admission.d.ts +240 -0
- package/dist/vendor/shared/durable-contract/admission.js +533 -0
- package/dist/vendor/shared/durable-contract/arm.d.ts +51 -0
- package/dist/vendor/shared/durable-contract/arm.js +133 -0
- package/dist/vendor/shared/durable-contract/canonical.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/canonical.js +98 -0
- package/dist/vendor/shared/durable-contract/cell-id.d.ts +22 -0
- package/dist/vendor/shared/durable-contract/cell-id.js +38 -0
- package/dist/vendor/shared/durable-contract/cli-output.d.ts +342 -0
- package/dist/vendor/shared/durable-contract/cli-output.js +201 -0
- package/dist/vendor/shared/durable-contract/cli-surface.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/cli-surface.js +270 -0
- package/dist/vendor/shared/durable-contract/codes.d.ts +537 -0
- package/dist/vendor/shared/durable-contract/codes.js +475 -0
- package/dist/vendor/shared/durable-contract/constants.d.ts +153 -0
- package/dist/vendor/shared/durable-contract/constants.js +179 -0
- package/dist/vendor/shared/durable-contract/doors.d.ts +119 -0
- package/dist/vendor/shared/durable-contract/doors.js +249 -0
- package/dist/vendor/shared/durable-contract/envelope.d.ts +103 -0
- package/dist/vendor/shared/durable-contract/envelope.js +317 -0
- package/dist/vendor/shared/durable-contract/events.d.ts +147 -0
- package/dist/vendor/shared/durable-contract/events.js +630 -0
- package/dist/vendor/shared/durable-contract/executor-state.d.ts +41 -0
- package/dist/vendor/shared/durable-contract/executor-state.js +32 -0
- package/dist/vendor/shared/durable-contract/fixlines.d.ts +318 -0
- package/dist/vendor/shared/durable-contract/fixlines.js +483 -0
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +787 -0
- package/dist/vendor/shared/durable-contract/freeze-record.js +361 -0
- package/dist/vendor/shared/durable-contract/index.d.ts +36 -0
- package/dist/vendor/shared/durable-contract/index.js +37 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.d.ts +29 -0
- package/dist/vendor/shared/durable-contract/journal-approvals.js +60 -0
- package/dist/vendor/shared/durable-contract/keys.d.ts +24 -0
- package/dist/vendor/shared/durable-contract/keys.js +66 -0
- package/dist/vendor/shared/durable-contract/lock.d.ts +128 -0
- package/dist/vendor/shared/durable-contract/lock.js +151 -0
- package/dist/vendor/shared/durable-contract/names.d.ts +25 -0
- package/dist/vendor/shared/durable-contract/names.js +78 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.d.ts +175 -0
- package/dist/vendor/shared/durable-contract/owner-verbs.js +403 -0
- package/dist/vendor/shared/durable-contract/paths.d.ts +23 -0
- package/dist/vendor/shared/durable-contract/paths.js +35 -0
- package/dist/vendor/shared/durable-contract/pure-fn.d.ts +48 -0
- package/dist/vendor/shared/durable-contract/pure-fn.js +89 -0
- package/dist/vendor/shared/durable-contract/routes.d.ts +205 -0
- package/dist/vendor/shared/durable-contract/routes.js +315 -0
- package/dist/vendor/shared/durable-contract/schema.d.ts +126 -0
- package/dist/vendor/shared/durable-contract/schema.js +441 -0
- package/dist/vendor/shared/durable-contract/seal.d.ts +54 -0
- package/dist/vendor/shared/durable-contract/seal.js +99 -0
- package/dist/vendor/shared/durable-contract/secret-scope.d.ts +28 -0
- package/dist/vendor/shared/durable-contract/secret-scope.js +76 -0
- package/dist/vendor/shared/durable-contract/session-id.d.ts +21 -0
- package/dist/vendor/shared/durable-contract/session-id.js +58 -0
- package/dist/vendor/shared/durable-contract/spec.d.ts +162 -0
- package/dist/vendor/shared/durable-contract/spec.js +286 -0
- package/dist/vendor/shared/durable-contract/states.d.ts +96 -0
- package/dist/vendor/shared/durable-contract/states.js +218 -0
- package/dist/vendor/shared/durable-contract/telemetry.d.ts +94 -0
- package/dist/vendor/shared/durable-contract/telemetry.js +180 -0
- package/dist/vendor/shared/durable-contract/test-mode.d.ts +62 -0
- package/dist/vendor/shared/durable-contract/test-mode.js +140 -0
- package/dist/vendor/shared/durable-contract/turn.d.ts +57 -0
- package/dist/vendor/shared/durable-contract/turn.js +81 -0
- package/dist/vendor/shared/durable-contract/vectors.d.ts +191 -0
- package/dist/vendor/shared/durable-contract/vectors.js +208 -0
- package/package.json +3 -2
- package/plugins/integrity-manifest.json +35 -0
- package/plugins/mnemom/hooks/hooks.json +15 -1
- package/plugins/mnemom/hooks/register.tsx +60 -2
- package/plugins/mnemom/hooks/seat-hello.mjs +116 -0
- package/dist/commands/agent-implicit-notice.d.ts +0 -31
- package/dist/commands/agent-implicit-notice.js +0 -73
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The live wiring of the durable verbs: real fetch, the stored login (JWT) or the personal org
|
|
3
|
+
* MNEMOM_API_KEY, the active org, launcher.toml's `durable` key, the process table. Thin by design:
|
|
4
|
+
* every decision lives in the injected modules and is unit-tested there.
|
|
5
|
+
*/
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
8
|
+
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
import { getAccessToken, resolveAuth } from "../auth.js";
|
|
11
|
+
import { getActiveOrg } from "../cli-config.js";
|
|
12
|
+
import { getCodeEntitlement, listOrgAgents } from "../api.js";
|
|
13
|
+
import { getApiUrl, MNEMOM_DIR } from "../config.js";
|
|
14
|
+
import { applyConfigToEnv, loadLauncherConfig } from "../launcher-config.js";
|
|
15
|
+
import { launchPreferenceExplicit, remoteControlAllowed, resolveLaunchShape } from "../agent.js";
|
|
16
|
+
import { checkRcCapability, resolveCliBin } from "../../commands/agent.js";
|
|
17
|
+
import { conversationOf, envRecord, launcherPassthrough } from "../../durable/discover.js";
|
|
18
|
+
import { liveProcDeps, processTable, readProcArgs } from "../../durable/proc.js";
|
|
19
|
+
import { ledgerPath, readSessionRecords } from "../agent-ledger.js";
|
|
20
|
+
import { launchingNow, liveLaunchingDeps } from "./launching.js";
|
|
21
|
+
import { ATTACH_WAIT_S } from "../../vendor/shared/durable-contract/constants.js";
|
|
22
|
+
import { durableDoctorLines } from "../../commands/durable-doctor.js";
|
|
23
|
+
import { liveCwdProbe, liveTranscriptDirs, liveTrustProbe, lockFactsOf, lockResolvedRow, lockSpecDefaults, printLocked, rcWouldBeNamed, readLocalExecutorId, } from "../../commands/durable-lock.js";
|
|
24
|
+
import { DurableApi, RegistryUnavailable, } from "./api.js";
|
|
25
|
+
import { afterLockRefusal, attachToSeat, createBody, createOutcome, flagValue, } from "./launch.js";
|
|
26
|
+
import { helloLanded, ledgerCcSessionIdFrom, lockCommitted, parentAfterChildEnded, processTree, sessionRowOf, } from "./handover.js";
|
|
27
|
+
import { ccProjectsDir } from "../../host/transcript.js";
|
|
28
|
+
import { resolveInstance } from "../../host/instance.js";
|
|
29
|
+
import { ownerApi, parseExecutorList } from "../../host/owner.js";
|
|
30
|
+
import { seatTmuxSession } from "../../host/tmux.js";
|
|
31
|
+
import { lockAfterFirstTurn, transcriptReader } from "./first-turn.js";
|
|
32
|
+
import { DurableRefusal, printDurableRefusal } from "./refusal.js";
|
|
33
|
+
export function liveDurableApi() {
|
|
34
|
+
return new DurableApi({
|
|
35
|
+
fetch: (...a) => fetch(...a),
|
|
36
|
+
apiBase: () => getApiUrl(),
|
|
37
|
+
credential: async () => {
|
|
38
|
+
const c = await resolveAuth();
|
|
39
|
+
if (c.type === "jwt")
|
|
40
|
+
return { type: "jwt", token: c.token };
|
|
41
|
+
if (c.type === "api-key")
|
|
42
|
+
return { type: "api-key", key: c.key };
|
|
43
|
+
return { type: "none" };
|
|
44
|
+
},
|
|
45
|
+
activeOrgId: () => getActiveOrg()?.org_id,
|
|
46
|
+
agentsByName: async (name) => (await listOrgAgents())
|
|
47
|
+
.filter((a) => a.name === name)
|
|
48
|
+
.map((a) => ({ id: a.id, org_id: a.org_id })),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
/** launcher.toml `durable`, defaulting to off (only read while the gate is on); unreadable is off. */
|
|
52
|
+
export function liveLauncherKey(load = loadLauncherConfig) {
|
|
53
|
+
try {
|
|
54
|
+
return load().durable ?? "off";
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return "off";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** PIDs of local claude processes whose argv names `ccSessionId` (pure over a `ps` listing). */
|
|
61
|
+
export function claudePidsFor(psOutput, ccSessionId, selfPid) {
|
|
62
|
+
const out = [];
|
|
63
|
+
for (const line of psOutput.split("\n")) {
|
|
64
|
+
const m = /^\s*(\d+)\s+(.*)$/.exec(line);
|
|
65
|
+
if (!m)
|
|
66
|
+
continue;
|
|
67
|
+
const pid = Number(m[1]);
|
|
68
|
+
const args = m[2];
|
|
69
|
+
if (pid === selfPid || !/(^|\/)claude(\s|$)/.test(args.split(" ")[0] ?? ""))
|
|
70
|
+
continue;
|
|
71
|
+
if (args.split(/\s+/).includes(ccSessionId))
|
|
72
|
+
out.push(pid);
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** done/kill on a tracked session: SIGTERM its local claude, SIGKILL after the grace (final F04). */
|
|
77
|
+
async function stopTrackedClaude(row, graceS) {
|
|
78
|
+
const cc = typeof row.cc_session_id === "string" ? row.cc_session_id : null;
|
|
79
|
+
if (cc === null)
|
|
80
|
+
return;
|
|
81
|
+
const ps = spawnSync("ps", ["-axo", "pid=,args="], { encoding: "utf8" });
|
|
82
|
+
if (ps.status !== 0)
|
|
83
|
+
return;
|
|
84
|
+
const pids = claudePidsFor(ps.stdout, cc, process.pid);
|
|
85
|
+
const alive = (pid) => {
|
|
86
|
+
try {
|
|
87
|
+
process.kill(pid, 0);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
for (const pid of pids) {
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, "SIGTERM");
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
/* already gone */
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const deadline = Date.now() + graceS * 1000;
|
|
103
|
+
while (pids.some(alive) && Date.now() < deadline)
|
|
104
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
105
|
+
for (const pid of pids.filter(alive)) {
|
|
106
|
+
try {
|
|
107
|
+
process.kill(pid, "SIGKILL");
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
/* already gone */
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
export function liveVerbDeps() {
|
|
115
|
+
return {
|
|
116
|
+
api: liveDurableApi(),
|
|
117
|
+
launcherKey: () => liveLauncherKey(),
|
|
118
|
+
out: (l) => process.stdout.write(`${l}\n`),
|
|
119
|
+
err: (l) => console.error(l),
|
|
120
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
121
|
+
now: () => Date.now(),
|
|
122
|
+
stopTrackedClaude,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
export function liveSessionsReadDeps() {
|
|
126
|
+
return {
|
|
127
|
+
api: liveDurableApi(),
|
|
128
|
+
launcherKey: () => liveLauncherKey(),
|
|
129
|
+
home: liveHomeFacts,
|
|
130
|
+
out: (l) => process.stdout.write(`${l}\n`),
|
|
131
|
+
err: (l) => console.error(l),
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function liveLockDeps() {
|
|
135
|
+
const base = liveVerbDeps();
|
|
136
|
+
return {
|
|
137
|
+
...base,
|
|
138
|
+
api: base.api,
|
|
139
|
+
home: homedir(),
|
|
140
|
+
cwd: () => process.cwd(),
|
|
141
|
+
probe: liveCwdProbe,
|
|
142
|
+
trust: liveTrustProbe(process.env),
|
|
143
|
+
readExecutorId: (h) => readLocalExecutorId(h),
|
|
144
|
+
executorOrg: (id) => {
|
|
145
|
+
const owner = ownerApi({
|
|
146
|
+
apiUrl: getApiUrl(),
|
|
147
|
+
jwt: getAccessToken,
|
|
148
|
+
fetch: globalThis.fetch,
|
|
149
|
+
});
|
|
150
|
+
return readExecutorOrg(id, () => owner.list());
|
|
151
|
+
},
|
|
152
|
+
launching: (name) => launchingNow(name, liveLaunchingDeps(MNEMOM_DIR)),
|
|
153
|
+
specDefaults: (row) => {
|
|
154
|
+
let launcher = {};
|
|
155
|
+
try {
|
|
156
|
+
const c = loadLauncherConfig();
|
|
157
|
+
launcher = { model: c.model, effort: c.effort };
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
/* no launcher.toml: the launch's own record alone */
|
|
161
|
+
}
|
|
162
|
+
return lockSpecDefaults(row.conversation_id, readSessionRecords({ dir: MNEMOM_DIR }), launcher);
|
|
163
|
+
},
|
|
164
|
+
readOriginal: (pid) => liveReadOriginal(pid),
|
|
165
|
+
rcNamed: () => liveRcNamed(),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
/** The live original's exact argv, its parent launcher's passthrough and its shape (C14-R13). */
|
|
169
|
+
export function liveReadOriginal(pid, deps = liveProcDeps) {
|
|
170
|
+
const a = readProcArgs(pid, deps);
|
|
171
|
+
if (a === null || a.argv.length === 0)
|
|
172
|
+
return null;
|
|
173
|
+
const row = processTable(deps).find((r) => r.pid === pid);
|
|
174
|
+
const parent = row ? readProcArgs(row.ppid, deps) : null;
|
|
175
|
+
return {
|
|
176
|
+
argv: a.argv,
|
|
177
|
+
passthroughHint: launcherPassthrough(parent?.argv),
|
|
178
|
+
remoteControl: conversationOf(envRecord(a.env))?.rc === true,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
export const liveRcNamingIo = () => ({
|
|
182
|
+
env: process.env,
|
|
183
|
+
loadConfig: () => loadLauncherConfig(),
|
|
184
|
+
entitlement: () => getCodeEntitlement(getActiveOrg()?.org_id),
|
|
185
|
+
capable: (env) => checkRcCapability(resolveCliBin(undefined, env)).ok,
|
|
186
|
+
});
|
|
187
|
+
/** The idle source's RC naming, as `mnemom agent <name>` would resolve it (fail-closed: false). */
|
|
188
|
+
export async function liveRcNamed(io = liveRcNamingIo()) {
|
|
189
|
+
const env = { ...io.env };
|
|
190
|
+
try {
|
|
191
|
+
applyConfigToEnv(io.loadConfig(), env);
|
|
192
|
+
const shape = resolveLaunchShape({}, env);
|
|
193
|
+
return rcWouldBeNamed({
|
|
194
|
+
shape,
|
|
195
|
+
shapeExplicit: launchPreferenceExplicit({}, env),
|
|
196
|
+
rcAllowed: remoteControlAllowed(await io.entitlement()),
|
|
197
|
+
rcCapable: () => io.capable(env),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
/** `mnemom agent doctor`'s durable lines (C02-R19), read through the live registry client. */
|
|
205
|
+
export function liveDoctorDurableLines() {
|
|
206
|
+
return durableDoctorLines(liveDurableApi());
|
|
207
|
+
}
|
|
208
|
+
/** This machine's launch-ledger records, or null when the ledger is missing or unreadable. */
|
|
209
|
+
export function readLedgerRecords(exists, read) {
|
|
210
|
+
try {
|
|
211
|
+
return exists() ? read() : null;
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/** The live C02-R26 handover: the process table, real signals, and the registry row. */
|
|
218
|
+
export function liveHandoverDeps(api) {
|
|
219
|
+
const reread = async (row) => sessionRowOf(await api.getSession(row.conversation_id, row.agent_id));
|
|
220
|
+
return {
|
|
221
|
+
ledgerCcSessionId: (cid) => ledgerCcSessionIdFrom(readLedgerRecords(() => existsSync(ledgerPath(MNEMOM_DIR)), () => readSessionRecords({ dir: MNEMOM_DIR })), cid),
|
|
222
|
+
log: (line) => console.error(line),
|
|
223
|
+
findOriginal: (cc) => {
|
|
224
|
+
const ps = spawnSync("ps", ["-axo", "pid=,args="], { encoding: "utf8" });
|
|
225
|
+
if (ps.status !== 0)
|
|
226
|
+
return null;
|
|
227
|
+
const pids = claudePidsFor(ps.stdout, cc, process.pid);
|
|
228
|
+
return pids.length === 0 ? null : Math.min(...pids);
|
|
229
|
+
},
|
|
230
|
+
signal: (pid, sig) => {
|
|
231
|
+
try {
|
|
232
|
+
process.kill(pid, sig);
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
/* already gone */
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
helloSeen: async (row) => helloLanded(await reread(row)),
|
|
239
|
+
committed: async (row) => lockCommitted(await reread(row)),
|
|
240
|
+
killTree: (pid) => {
|
|
241
|
+
const ps = spawnSync("ps", ["-axo", "pid=,ppid="], { encoding: "utf8" });
|
|
242
|
+
const pids = processTree(ps.status === 0 ? ps.stdout : "", pid, process.pid);
|
|
243
|
+
for (const p of pids) {
|
|
244
|
+
try {
|
|
245
|
+
process.kill(p, "SIGKILL");
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
/* already gone */
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
253
|
+
now: () => Date.now(),
|
|
254
|
+
attachWaitS: ATTACH_WAIT_S,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* The lock verb's deps: the handover on, and EPIPE on stdout ignored (under `/mnemom lock` the
|
|
259
|
+
* parent may be gone by the time the lock line prints; C02-R26 step 7).
|
|
260
|
+
*/
|
|
261
|
+
export function liveLockVerbDeps() {
|
|
262
|
+
process.stdout.on("error", (e) => {
|
|
263
|
+
if (e.code !== "EPIPE")
|
|
264
|
+
throw e;
|
|
265
|
+
});
|
|
266
|
+
const base = liveLockDeps();
|
|
267
|
+
return {
|
|
268
|
+
...base,
|
|
269
|
+
handover: liveHandoverDeps(liveDurableApi()),
|
|
270
|
+
transcriptDirs: liveTranscriptDirs(process.env),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* C02-R26 step 7 for the launcher: its tracked child ended. When a handover locked the session,
|
|
275
|
+
* restore the terminal mode saved before the spawn (`stty -g`), print the lock line and attach;
|
|
276
|
+
* the exit status, or null to exit with the child's status as today.
|
|
277
|
+
*/
|
|
278
|
+
export async function liveParentAfterChildEnded(row, name, launcherKey, savedTty) {
|
|
279
|
+
const api = liveDurableApi();
|
|
280
|
+
const seat = liveSeatDeps(launcherKey);
|
|
281
|
+
return parentAfterChildEnded(row, name, {
|
|
282
|
+
reread: async (r) => sessionRowOf(await api.getSession(r.conversation_id, r.agent_id)),
|
|
283
|
+
restoreTty: () => {
|
|
284
|
+
if (savedTty)
|
|
285
|
+
spawnSync("stty", [savedTty], { stdio: ["inherit", "ignore", "ignore"] });
|
|
286
|
+
},
|
|
287
|
+
printLockLine: (r, n) => printLocked({ body: {}, changed: true }, r, n, seat.out),
|
|
288
|
+
attach: (r, n) => attachToSeat(r, n, [], seat),
|
|
289
|
+
printRefusal: (r) => printDurableRefusal(r, false, seat),
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
/** The terminal mode (`stty -g`) before claude starts, or null off a TTY. */
|
|
293
|
+
export function saveTtyMode() {
|
|
294
|
+
if (!process.stdin.isTTY)
|
|
295
|
+
return null;
|
|
296
|
+
const r = spawnSync("stty", ["-g"], { stdio: ["inherit", "pipe", "ignore"], encoding: "utf8" });
|
|
297
|
+
return r.status === 0 && typeof r.stdout === "string" ? r.stdout.trim() || null : null;
|
|
298
|
+
}
|
|
299
|
+
const runThisCli = (bin, args) => spawnSync(process.execPath, [bin, ...args], {
|
|
300
|
+
encoding: "utf8",
|
|
301
|
+
timeout: 10_000,
|
|
302
|
+
env: { ...process.env, MNEMOM_NO_UPDATE_CHECK: "1" },
|
|
303
|
+
});
|
|
304
|
+
/** `mnemom agent host status --json` (C2), or null when it is not available. */
|
|
305
|
+
export function readHostStatus(bin = process.argv[1], run = runThisCli) {
|
|
306
|
+
if (!bin)
|
|
307
|
+
return null;
|
|
308
|
+
const r = run(bin, ["agent", "host", "status", "--json"]);
|
|
309
|
+
if (r.status !== 0 || typeof r.stdout !== "string")
|
|
310
|
+
return null;
|
|
311
|
+
try {
|
|
312
|
+
const v = JSON.parse(r.stdout);
|
|
313
|
+
return v !== null && typeof v === "object" ? v : null;
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* agents.agent_durable_only from an agent record (bare or under `agent`); null when the record
|
|
321
|
+
* carries no boolean flag. Absent is never read as false: the api once never served the field
|
|
322
|
+
* (a PostgREST computed field missed by `select=*`, mnemom-api #4263), and false skipped C02-R24.
|
|
323
|
+
*/
|
|
324
|
+
export function agentDurableOnlyOf(body) {
|
|
325
|
+
if (body === null || typeof body !== "object")
|
|
326
|
+
return null;
|
|
327
|
+
const b = body;
|
|
328
|
+
const rec = b.agent !== null && typeof b.agent === "object" ? b.agent : b;
|
|
329
|
+
return typeof rec.agent_durable_only === "boolean" ? rec.agent_durable_only : null;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* agents.agent_durable_only of an agent (A2 serves it on the agent record). It fails closed
|
|
333
|
+
* (C02-R24): a record that cannot be read (transport, timeout, 5xx, any non-200) refuses with
|
|
334
|
+
* session_registry_unavailable before any spawn, so a durable-only agent never gets a tracked
|
|
335
|
+
* claude on an unreadable flag. A 200 without the field refuses the same way: absent is not false.
|
|
336
|
+
*/
|
|
337
|
+
export async function readAgentDurableOnly(api, agentId) {
|
|
338
|
+
const unreadable = (detail) => new DurableRefusal({
|
|
339
|
+
code: "session_registry_unavailable",
|
|
340
|
+
message: `mnemom agent: agent ${agentId}'s durable-only setting could not be read (${detail}); nothing was started`,
|
|
341
|
+
fix: null,
|
|
342
|
+
});
|
|
343
|
+
let a;
|
|
344
|
+
try {
|
|
345
|
+
a = await api.getAgent(agentId);
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
if (e instanceof RegistryUnavailable)
|
|
349
|
+
throw unreadable(e.message);
|
|
350
|
+
throw e;
|
|
351
|
+
}
|
|
352
|
+
if (a.status !== 200)
|
|
353
|
+
throw unreadable(`HTTP ${a.status}`);
|
|
354
|
+
const flag = agentDurableOnlyOf(a.body);
|
|
355
|
+
if (flag === null)
|
|
356
|
+
throw unreadable("the agent record carries no agent_durable_only");
|
|
357
|
+
return flag;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* This machine's label: the machine_label of its current executor in the owner executor list
|
|
361
|
+
* (exec_list), matched by the state file's executor id. Never the hostname: enrollment takes any
|
|
362
|
+
* --label. Null when not enrolled here, when the list can't be read, or when the row has no
|
|
363
|
+
* label; the unreachable-home rule then never applies (the home reads elsewhere).
|
|
364
|
+
*/
|
|
365
|
+
export async function readLocalMachineLabel(localExecutorId, list) {
|
|
366
|
+
if (localExecutorId === null)
|
|
367
|
+
return null;
|
|
368
|
+
let r;
|
|
369
|
+
try {
|
|
370
|
+
r = await list();
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
if (r.status !== 200)
|
|
376
|
+
return null;
|
|
377
|
+
const id = localExecutorId.toLowerCase();
|
|
378
|
+
return parseExecutorList(r.body)?.find((x) => x.id === id)?.label ?? null;
|
|
379
|
+
}
|
|
380
|
+
/**
|
|
381
|
+
* The org this machine's executor is enrolled in (exec_list, matched by id), or null when the list
|
|
382
|
+
* can't be read or names no org for it. Read only to explain a durable_no_executor lock refusal.
|
|
383
|
+
*/
|
|
384
|
+
export async function readExecutorOrg(executorId, list) {
|
|
385
|
+
let r;
|
|
386
|
+
try {
|
|
387
|
+
r = await list();
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
if (r.status !== 200)
|
|
393
|
+
return null;
|
|
394
|
+
const id = executorId.toLowerCase();
|
|
395
|
+
return parseExecutorList(r.body)?.find((x) => x.id === id)?.orgId ?? null;
|
|
396
|
+
}
|
|
397
|
+
export async function liveHomeFacts() {
|
|
398
|
+
const localExecutorId = readLocalExecutorId(homedir());
|
|
399
|
+
const owner = ownerApi({
|
|
400
|
+
apiUrl: getApiUrl(),
|
|
401
|
+
jwt: getAccessToken,
|
|
402
|
+
fetch: globalThis.fetch,
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
localExecutorId,
|
|
406
|
+
localMachineLabel: await readLocalMachineLabel(localExecutorId, () => owner.list()),
|
|
407
|
+
nowMs: Date.now(),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
/** The launcher's registry reads (entitlement, then the list by name) over one client. */
|
|
411
|
+
export function liveRegistryReads(api) {
|
|
412
|
+
return {
|
|
413
|
+
entitlement: () => api.entitlement(),
|
|
414
|
+
listSessions: (f) => api.listSessions({ name: f.name, agentId: undefined }),
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
export function livePlanDeps(servedAgentId, transcriptExists, askYesNo) {
|
|
418
|
+
const api = liveDurableApi();
|
|
419
|
+
return {
|
|
420
|
+
...liveRegistryReads(api),
|
|
421
|
+
failTracked: (row) => api.ownerVerb("own_fail", row.conversation_id, row.agent_id || servedAgentId, {
|
|
422
|
+
reason: "transcript_lost",
|
|
423
|
+
}),
|
|
424
|
+
transcriptExists,
|
|
425
|
+
daemonLoaded: () => daemonPresent(readHostStatus()),
|
|
426
|
+
askYesNo,
|
|
427
|
+
newId: () => randomUUID(),
|
|
428
|
+
agentDurableOnly: () => readAgentDurableOnly(api, servedAgentId),
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
/**
|
|
432
|
+
* A host daemon serves this machine when launchd has it loaded, or when one is running here
|
|
433
|
+
* outside launchd (`mnemom agent host run` in the foreground): status reports its pid only while
|
|
434
|
+
* that pid holds host.lock and wrote the runtime file.
|
|
435
|
+
*/
|
|
436
|
+
export function daemonPresent(status) {
|
|
437
|
+
if (!status)
|
|
438
|
+
return false;
|
|
439
|
+
return status.loaded === true || typeof status.daemon_pid === "number";
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* The seat of a session from host status (C2). Status names its host by the launchd label
|
|
443
|
+
* (`instance`); a status for another host is never attached to. The socket is this host's tmux
|
|
444
|
+
* socket, resolved from the environment as the daemon resolves it (host/instance.ts), never the
|
|
445
|
+
* label: the two differ on every host. The target is the seat's tmux session, named as the daemon
|
|
446
|
+
* names it from the session and spawn ids (host/tmux.ts seatTmuxSession).
|
|
447
|
+
*/
|
|
448
|
+
export function seatOf(status, row, instance) {
|
|
449
|
+
if (status === null || status.instance !== instance.label)
|
|
450
|
+
return null;
|
|
451
|
+
const seats = Array.isArray(status.seats) ? status.seats : [];
|
|
452
|
+
const id = row.id ?? null;
|
|
453
|
+
const seat = seats.find((s) => s !== null &&
|
|
454
|
+
typeof s === "object" &&
|
|
455
|
+
s.session_id === id &&
|
|
456
|
+
s.claude_pid !== null);
|
|
457
|
+
if (!seat || id === null || typeof seat.spawn_id !== "string")
|
|
458
|
+
return null;
|
|
459
|
+
return { socket: instance.tmuxSocket, target: `=${seatTmuxSession(id, seat.spawn_id)}` };
|
|
460
|
+
}
|
|
461
|
+
/** tmux's argv to attach a seat. */
|
|
462
|
+
export function seatAttachArgv(seat) {
|
|
463
|
+
return ["-L", seat.socket, "attach", "-t", seat.target];
|
|
464
|
+
}
|
|
465
|
+
/** The host instance this CLI talks to, or null when its env names an invalid one. */
|
|
466
|
+
export function envInstance(env = process.env) {
|
|
467
|
+
try {
|
|
468
|
+
return resolveInstance(env);
|
|
469
|
+
}
|
|
470
|
+
catch {
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/** The seat to attach to: none when the env names no valid instance (status is then never read). */
|
|
475
|
+
export function seatIn(row, instance, status) {
|
|
476
|
+
return instance ? seatOf(status(), row, instance) : null;
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Attach the terminal to a seat with tmux and return tmux's exit status; a tmux ended by a signal
|
|
480
|
+
* (no status) is a failed attach, 1.
|
|
481
|
+
*/
|
|
482
|
+
export function attachSeat(seat, run) {
|
|
483
|
+
return run("tmux", seatAttachArgv(seat), { stdio: "inherit" }).status ?? 1;
|
|
484
|
+
}
|
|
485
|
+
export function liveSeatDeps(launcherKey) {
|
|
486
|
+
return {
|
|
487
|
+
findSeat: async (row) => seatIn(row, envInstance(), () => readHostStatus()),
|
|
488
|
+
attach: (seat) => attachSeat(seat, spawnSync),
|
|
489
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
490
|
+
now: () => Date.now(),
|
|
491
|
+
out: (l) => process.stdout.write(`${l}\n`),
|
|
492
|
+
err: (l) => console.error(l),
|
|
493
|
+
interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
494
|
+
launcherKey,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/** Register a new session (POST create) or return the continued row; a refusal is thrown. */
|
|
498
|
+
export async function registerDurableSession(plan, name, agentId, deps = { api: liveDurableApi(), cwd: () => process.cwd() }) {
|
|
499
|
+
if (!plan.register && plan.row !== null)
|
|
500
|
+
return plan.row;
|
|
501
|
+
let a;
|
|
502
|
+
try {
|
|
503
|
+
a = await deps.api.createSession(plan.conversationId, agentId, createBody(name, plan.newCcSessionId, deps.cwd()));
|
|
504
|
+
}
|
|
505
|
+
catch (e) {
|
|
506
|
+
if (e instanceof RegistryUnavailable) {
|
|
507
|
+
throw new DurableRefusal({
|
|
508
|
+
code: "session_registry_unavailable",
|
|
509
|
+
message: `mnemom agent: the session registry is unreachable (${e.message}); nothing was started`,
|
|
510
|
+
fix: null,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
throw e;
|
|
514
|
+
}
|
|
515
|
+
const out = createOutcome(a, name);
|
|
516
|
+
if (out instanceof DurableRefusal)
|
|
517
|
+
throw out;
|
|
518
|
+
return {
|
|
519
|
+
...out,
|
|
520
|
+
conversation_id: out.conversation_id ?? plan.conversationId,
|
|
521
|
+
agent_id: out.agent_id ?? agentId,
|
|
522
|
+
name: out.name ?? name,
|
|
523
|
+
cwd: out.cwd ?? deps.cwd(),
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
function liveLockInsteadDeps(launcherKey) {
|
|
527
|
+
return {
|
|
528
|
+
lockRow: (row, name, deps) => lockResolvedRow(row, name, {}, deps),
|
|
529
|
+
lockDeps: liveLockDeps(),
|
|
530
|
+
seat: liveSeatDeps(launcherKey),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
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
|
+
}
|
|
541
|
+
const seat = d.seat;
|
|
542
|
+
if (seat.interactive)
|
|
543
|
+
printLocked(o, row, name, deps.out);
|
|
544
|
+
const dropped = lockFactsOf(o.body, row, name, o.dropped).droppedFlags;
|
|
545
|
+
try {
|
|
546
|
+
return await attachToSeat(row, name, dropped, seat);
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
if (e instanceof DurableRefusal)
|
|
550
|
+
return printDurableRefusal(e, false, deps);
|
|
551
|
+
throw e;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
// ---- create-and-lock (ruling B5a as amended) ----------------------------------------------------
|
|
555
|
+
/** The row a create-and-lock is about to make: what the idle-source lock reads (cwd, cc id). */
|
|
556
|
+
export function bornRow(plan, name, agentId, cwd) {
|
|
557
|
+
return {
|
|
558
|
+
conversation_id: plan.conversationId,
|
|
559
|
+
name,
|
|
560
|
+
agent_id: agentId,
|
|
561
|
+
durable_mode: "tracked",
|
|
562
|
+
state: "running",
|
|
563
|
+
cc_session_id: plan.newCcSessionId,
|
|
564
|
+
cwd,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/**
|
|
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.
|
|
574
|
+
*
|
|
575
|
+
* No claude runs before the lock, so the spec cannot be read from a live original as the lock after
|
|
576
|
+
* the first turn does: the launch's own `--model` / `--effort` stand in for it (a passthrough
|
|
577
|
+
* `-- --model` / `--effort` first, as it comes last on claude's argv; else the flag, else
|
|
578
|
+
* launcher.toml), with its `--trim-mcp`, so the seat runs as the launch chose, never the default.
|
|
579
|
+
*/
|
|
580
|
+
export async function createLockedInsteadOfSpawn(plan, name, agentId, launcherKey, launch, d = {
|
|
581
|
+
...liveLockInsteadDeps(launcherKey),
|
|
582
|
+
api: liveDurableApi(),
|
|
583
|
+
}) {
|
|
584
|
+
const cwd = d.lockDeps.cwd();
|
|
585
|
+
const row = bornRow(plan, name, agentId, cwd);
|
|
586
|
+
const model = flagValue(plan.passthrough, ["--model"]) || launch.model;
|
|
587
|
+
const effort = flagValue(plan.passthrough, ["--effort"]) || launch.effort;
|
|
588
|
+
const lockRow = async (r, n, deps) => {
|
|
589
|
+
const o = await d.lockRow(r, n, {
|
|
590
|
+
...deps,
|
|
591
|
+
specDefaults: (row) => ({
|
|
592
|
+
...deps.specDefaults(row),
|
|
593
|
+
...(model ? { model } : {}),
|
|
594
|
+
...(effort ? { effort } : {}),
|
|
595
|
+
...(launch.trimMcp === true ? { trimMcp: true } : {}),
|
|
596
|
+
}),
|
|
597
|
+
api: {
|
|
598
|
+
...deps.api,
|
|
599
|
+
lock: (cid, aid, body) => d.api.createSession(cid, aid, {
|
|
600
|
+
...createBody(n, plan.newCcSessionId, cwd),
|
|
601
|
+
lock: body,
|
|
602
|
+
}),
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
// the seat is found by the new row's id, which only the answer names
|
|
606
|
+
const sid = o.ok ? o.body?.session_id : undefined;
|
|
607
|
+
if (typeof sid === "string")
|
|
608
|
+
r.id = sid;
|
|
609
|
+
return o;
|
|
610
|
+
};
|
|
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
|
+
});
|
|
635
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable refusals on the CLI (wave-0 fix ruling 19; S1 cli-output.ts). One shape for every refused
|
|
3
|
+
* durable verb, with or without --json:
|
|
4
|
+
*
|
|
5
|
+
* stderr <refusal line>
|
|
6
|
+
* fix: <S1 catalogue line, rendered under the fix-line rule> (when there is one)
|
|
7
|
+
* code: <machine code>
|
|
8
|
+
* stdout with --json only: exactly one JSON document, the ruling-14 error envelope. An API or
|
|
9
|
+
* gateway refusal body is passed through unchanged; a refusal the CLI decides itself is
|
|
10
|
+
* built with S1 `cliRefusalJson` (details.fix carries the rendered line).
|
|
11
|
+
* exit CLI_REFUSAL_EXIT (1).
|
|
12
|
+
*
|
|
13
|
+
* The refusal line never contains "code:"/"error:" text, so the first machine-readable stderr token
|
|
14
|
+
* is always the code line.
|
|
15
|
+
*/
|
|
16
|
+
import { type FixKey, type LauncherKey } from "../../vendor/shared/durable-contract/fixlines.js";
|
|
17
|
+
export interface DurableRefusalInit {
|
|
18
|
+
/** The machine code (an S1 durable code, or an existing API code such as agent_not_found). */
|
|
19
|
+
code: string;
|
|
20
|
+
/** The human refusal line. */
|
|
21
|
+
message: string;
|
|
22
|
+
/** The rendered fix line, when the refusal has one. */
|
|
23
|
+
fix?: string | null;
|
|
24
|
+
/** Extra closed details for a CLI-built --json document (the code's CodeSpec.fields). */
|
|
25
|
+
details?: Record<string, unknown>;
|
|
26
|
+
/** The API or gateway body to pass through unchanged on --json. */
|
|
27
|
+
apiBody?: unknown;
|
|
28
|
+
/** Exit status; CLI_REFUSAL_EXIT unless a caller has a reason (claude's own status, say). */
|
|
29
|
+
exit?: number;
|
|
30
|
+
/** Fix-line params a later renderer reuses (<repo>, <toplevel>, …); never printed or serialized. */
|
|
31
|
+
fixParams?: Record<string, string>;
|
|
32
|
+
}
|
|
33
|
+
/** A refused durable verb. Commands throw it; `printDurableRefusal` renders it. */
|
|
34
|
+
export declare class DurableRefusal extends Error {
|
|
35
|
+
readonly code: string;
|
|
36
|
+
readonly fix: string | null;
|
|
37
|
+
readonly details: Record<string, unknown>;
|
|
38
|
+
readonly apiBody: unknown;
|
|
39
|
+
readonly exit: number;
|
|
40
|
+
readonly fixParams: Record<string, string>;
|
|
41
|
+
constructor(init: DurableRefusalInit);
|
|
42
|
+
}
|
|
43
|
+
/** Render one catalogue line (thin wrapper so callers never restate a fix text). */
|
|
44
|
+
export declare function fixLine(key: FixKey, launcherKey: LauncherKey, nameHeld: boolean, params: Record<string, string>): string;
|
|
45
|
+
/** The --json stdout document for a refusal. */
|
|
46
|
+
export declare function refusalJson(r: DurableRefusal): unknown;
|
|
47
|
+
export interface RefusalSink {
|
|
48
|
+
out: (line: string) => void;
|
|
49
|
+
err: (line: string) => void;
|
|
50
|
+
}
|
|
51
|
+
/** Print a refusal (stderr always; stdout JSON only with --json) and return its exit status. */
|
|
52
|
+
export declare function printDurableRefusal(r: DurableRefusal, json: boolean, sink: RefusalSink): number;
|
|
53
|
+
/**
|
|
54
|
+
* The agent action's catch (index.ts): a DurableRefusal is printed (refusal line, fix, code) and its
|
|
55
|
+
* exit status returned; anything else is null and goes to the generic CLI error report.
|
|
56
|
+
*/
|
|
57
|
+
export declare function durableRefusalExit(error: unknown, sink: RefusalSink): number | null;
|