@mnemom/mnemom 0.17.33 → 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-share-render.d.ts +1 -1
- package/dist/commands/agent-share-render.js +1 -1
- package/dist/commands/agent.d.ts +26 -0
- package/dist/commands/agent.js +228 -53
- 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/commands/protection.js +1 -1
- package/dist/commands/scan.d.ts +97 -0
- package/dist/commands/scan.js +238 -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 +232 -35
- package/dist/lib/agent.d.ts +8 -26
- package/dist/lib/agent.js +11 -26
- package/dist/lib/api.d.ts +13 -0
- package/dist/lib/api.js +8 -0
- 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/caps.d.ts +35 -0
- package/dist/lib/scan/caps.js +67 -0
- package/dist/lib/scan/credential.d.ts +37 -0
- package/dist/lib/scan/credential.js +104 -0
- package/dist/lib/scan/failure.d.ts +10 -0
- package/dist/lib/scan/failure.js +43 -0
- package/dist/lib/scan/pr-source.d.ts +57 -0
- package/dist/lib/scan/pr-source.js +130 -0
- package/dist/lib/scan/record.d.ts +49 -0
- package/dist/lib/scan/record.js +85 -0
- package/dist/lib/scan/report-email.d.ts +92 -0
- package/dist/lib/scan/report-email.js +144 -0
- package/dist/lib/scan/report.d.ts +13 -0
- package/dist/lib/scan/report.js +30 -0
- package/dist/lib/scan/scan.d.ts +64 -0
- package/dist/lib/scan/scan.js +182 -0
- package/dist/lib/scan/share.d.ts +38 -0
- package/dist/lib/scan/share.js +47 -0
- package/dist/lib/scan/spend/ai-tool-detect.d.ts +6 -0
- package/dist/lib/scan/spend/ai-tool-detect.js +18 -0
- package/dist/lib/scan/spend/anthropic-usage.d.ts +57 -0
- package/dist/lib/scan/spend/anthropic-usage.js +244 -0
- package/dist/lib/scan/spend/cost-corpus.v1.d.ts +56 -0
- package/dist/lib/scan/spend/cost-corpus.v1.js +64 -0
- package/dist/lib/scan/spend/prices.d.ts +33 -0
- package/dist/lib/scan/spend/prices.js +57 -0
- package/dist/lib/scan/spend/spend-estimate.d.ts +30 -0
- package/dist/lib/scan/spend/spend-estimate.js +70 -0
- package/dist/lib/scan/spend/spend-tiers.d.ts +25 -0
- package/dist/lib/scan/spend/spend-tiers.js +42 -0
- package/dist/lib/scan/spend/token-rate.d.ts +30 -0
- package/dist/lib/scan/spend/token-rate.js +54 -0
- package/dist/lib/session-key.d.ts +41 -0
- package/dist/lib/session-key.js +124 -0
- package/dist/lib/subscription-login.d.ts +15 -0
- package/dist/lib/subscription-login.js +28 -1
- package/dist/rc-key-401.mjs +75 -0
- package/dist/rc-proxy.mjs +221 -25
- 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 +4 -3
- 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,443 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable launcher (BACKBONE C1; C01-R01, R02, R04, R14, R15, R16, R17, R18, R19, R21, R22;
|
|
3
|
+
* C02-R20, R23, R24). Pure decisions plus one orchestrator over injected deps; the launcher in
|
|
4
|
+
* commands/agent.ts calls `planDurableLaunch` once it knows the org and the served agent, and
|
|
5
|
+
* `finishDurableLaunch` once the conversation id is fixed (registration, then lock-or-spawn).
|
|
6
|
+
*
|
|
7
|
+
* Only runs with the `durable` gate on. With it off nothing here is reached (C17).
|
|
8
|
+
*/
|
|
9
|
+
import { ATTACH_WAIT_S } from "../../vendor/shared/durable-contract/constants.js";
|
|
10
|
+
import { DURABLE_ONLY_PRECONDITION, LOCK_REFUSAL_FIX, lockRefusalFixKey, renderDurableOnly, renderNonTtyLocked, } from "../../vendor/shared/durable-contract/fixlines.js";
|
|
11
|
+
import { sessionNameRefusal } from "../../vendor/shared/durable-contract/names.js";
|
|
12
|
+
import { errorCodeOf, RegistryAnswerError, RegistryUnavailable, } from "./api.js";
|
|
13
|
+
import { shellWord } from "../../commands/durable-lock.js";
|
|
14
|
+
import { DurableRefusal, fixLine } from "./refusal.js";
|
|
15
|
+
import { continueDecision, effectiveOf, liveRow } from "./view.js";
|
|
16
|
+
import { CODE } from "./codes.js";
|
|
17
|
+
/** The env var a launched session carries (lib/agent.ts SESSION_NAME_ENV). */
|
|
18
|
+
export const NESTED_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
19
|
+
/**
|
|
20
|
+
* Refusals the CLI decides before any registry call (C01-R15, R19; C02-R20). Null = go on.
|
|
21
|
+
* - a nested launch (MNEMOM_AGENT_SESSION_NAME set) -> nested_launch_refused
|
|
22
|
+
* - --server while the launch may be durable (key not off) -> refused, naming --server
|
|
23
|
+
* - a name outside the S1 grammar -> invalid_session_name
|
|
24
|
+
*/
|
|
25
|
+
export function durablePreflight(i) {
|
|
26
|
+
const nested = i.env[NESTED_ENV];
|
|
27
|
+
if (typeof nested === "string" && nested !== "") {
|
|
28
|
+
return new DurableRefusal({
|
|
29
|
+
code: "nested_launch_refused",
|
|
30
|
+
message: `mnemom agent: a launch from inside a session ('${sanitize(nested)}') is refused; run it from a terminal outside that session`,
|
|
31
|
+
fix: null,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
if (i.server && i.launcherKey !== "off") {
|
|
35
|
+
return new DurableRefusal({
|
|
36
|
+
code: "server_launch_refused",
|
|
37
|
+
message: "mnemom agent: --server is not available for durable sessions; launch without --server (or set `mnemom agent config set durable off`)",
|
|
38
|
+
fix: null,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (i.name !== undefined) {
|
|
42
|
+
const why = sessionNameRefusal(i.name);
|
|
43
|
+
if (why !== null) {
|
|
44
|
+
return new DurableRefusal({
|
|
45
|
+
code: "invalid_session_name",
|
|
46
|
+
message: `mnemom agent: '${sanitize(i.name)}' cannot be a session name (${why})`,
|
|
47
|
+
fix: null,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function sanitize(s) {
|
|
54
|
+
// eslint-disable-next-line no-control-regex
|
|
55
|
+
return s.replace(/[\u0000-\u001f\u007f--]/g, "?").slice(0, 80);
|
|
56
|
+
}
|
|
57
|
+
// ---- passthrough on a registry hit (C01-R22) ---------------------------------------------------
|
|
58
|
+
export function flagValue(argv, flags) {
|
|
59
|
+
for (let i = 0; i < argv.length; i++) {
|
|
60
|
+
const a = argv[i];
|
|
61
|
+
for (const f of flags) {
|
|
62
|
+
if (a === f) {
|
|
63
|
+
const v = argv[i + 1];
|
|
64
|
+
return v !== undefined && !v.startsWith("-") ? v : null;
|
|
65
|
+
}
|
|
66
|
+
if (a.startsWith(`${f}=`))
|
|
67
|
+
return a.slice(f.length + 1) || null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The passthrough a continue keeps, or a refusal. `--continue` and `--resume <same>` are plain
|
|
74
|
+
* continue (dropped; the launcher adds `--resume <cc>` itself); `--resume <other>`,
|
|
75
|
+
* `--session-id` and `--fork-session` refuse with session_resume_conflict; a `--goal` refuses with
|
|
76
|
+
* session_goal_immutable.
|
|
77
|
+
*/
|
|
78
|
+
export function continuePassthrough(passthrough, ccSessionId, goalGiven) {
|
|
79
|
+
const conflict = (what) => ({
|
|
80
|
+
ok: false,
|
|
81
|
+
refusal: new DurableRefusal({
|
|
82
|
+
code: "session_resume_conflict",
|
|
83
|
+
message: `mnemom agent: ${what} would move this session to another Claude Code session; continue it without it, or start fresh with --new`,
|
|
84
|
+
fix: null,
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
if (goalGiven) {
|
|
88
|
+
return {
|
|
89
|
+
ok: false,
|
|
90
|
+
refusal: new DurableRefusal({
|
|
91
|
+
code: "session_goal_immutable",
|
|
92
|
+
message: "mnemom agent: a continued session keeps its goal; drop --goal, or start fresh with --new",
|
|
93
|
+
fix: null,
|
|
94
|
+
}),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (flagValue(passthrough, ["--session-id"]) !== undefined)
|
|
98
|
+
return conflict("--session-id");
|
|
99
|
+
if (passthrough.includes("--fork-session"))
|
|
100
|
+
return conflict("--fork-session");
|
|
101
|
+
const resumed = flagValue(passthrough, ["--resume", "-r"]);
|
|
102
|
+
if (typeof resumed === "string" && resumed !== ccSessionId)
|
|
103
|
+
return conflict("--resume <other id>");
|
|
104
|
+
const out = [];
|
|
105
|
+
for (let i = 0; i < passthrough.length; i++) {
|
|
106
|
+
const a = passthrough[i];
|
|
107
|
+
if (a === "--continue" || a === "-c")
|
|
108
|
+
continue;
|
|
109
|
+
if (a === "--resume" || a === "-r") {
|
|
110
|
+
if (passthrough[i + 1] === resumed)
|
|
111
|
+
i++;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (a.startsWith("--resume="))
|
|
115
|
+
continue;
|
|
116
|
+
out.push(a);
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, passthrough: out };
|
|
119
|
+
}
|
|
120
|
+
/** A lock refusal code and its cwd_kind, as durable_only_agent's <precondition> reads it. */
|
|
121
|
+
export function lockRefusalOf(r) {
|
|
122
|
+
if (r === null || !(r.code in LOCK_REFUSAL_FIX))
|
|
123
|
+
return null;
|
|
124
|
+
const kind = r.details.cwd_kind;
|
|
125
|
+
// durable_cwd_not_root picks its clause by cwd_kind; without one there is no clause to print.
|
|
126
|
+
if (r.code === CODE.durable_cwd_not_root && typeof kind !== "string")
|
|
127
|
+
return null;
|
|
128
|
+
const refusal = {
|
|
129
|
+
code: r.code,
|
|
130
|
+
...(typeof kind === "string" ? { cwd_kind: kind } : {}),
|
|
131
|
+
};
|
|
132
|
+
// A clause whose path the refusal does not carry (not_git: no checkout) is not printed: every
|
|
133
|
+
// printed fix line is a working command (C11-R27), never a placeholder.
|
|
134
|
+
const key = lockRefusalFixKey(refusal);
|
|
135
|
+
const clause = key === null ? "" : DURABLE_ONLY_PRECONDITION[key];
|
|
136
|
+
const needs = [...clause.matchAll(/<([a-z_]+)>/g)].map((m) => m[1]);
|
|
137
|
+
return needs.every((p) => p === "name" || r.fixParams[p] !== undefined) ? refusal : null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The durable-only refusal (C02-R24; S1 durable_only_agent, byte-equal): before any spawn, naming
|
|
141
|
+
* the lock refusal when one happened. `fixParams` fills <repo>, <toplevel>, <checkout>, <realpath>.
|
|
142
|
+
*/
|
|
143
|
+
export function durableOnlyRefusal(launcherKey, name, lockRefusal, fixParams = {}) {
|
|
144
|
+
const refusal = lockRefusalOf(lockRefusal);
|
|
145
|
+
const reason = lockRefusal ? ` (lock refused: ${lockRefusal.code})` : "";
|
|
146
|
+
return new DurableRefusal({
|
|
147
|
+
code: CODE.durable_only_agent,
|
|
148
|
+
message: `mnemom agent: this agent runs durable sessions only, and '${name}' would not end locked${reason}; nothing was started`,
|
|
149
|
+
fix: renderDurableOnly(launcherKey, { name, refusal, fixParams }),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const registryDown = (detail) => new DurableRefusal({
|
|
153
|
+
code: "session_registry_unavailable",
|
|
154
|
+
message: `mnemom agent: the session registry is unreachable (${detail}); nothing was started`,
|
|
155
|
+
fix: null,
|
|
156
|
+
});
|
|
157
|
+
async function wantsLock(i, deps, name) {
|
|
158
|
+
if (i.launcherKey === "always")
|
|
159
|
+
return true;
|
|
160
|
+
if (i.launcherKey === "ask" && i.interactive)
|
|
161
|
+
return deps.askYesNo(`Make ${name} durable? [y/N]`, false);
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Whether the entitlement answer gives this caller the registry (C01-R26, R14): a 200 whose
|
|
166
|
+
* `durable.tracked` is exactly true. The field absent (API flag off or a schema mismatch) or
|
|
167
|
+
* `tracked` false (not entitled) is not.
|
|
168
|
+
*/
|
|
169
|
+
export function durableTracked(a) {
|
|
170
|
+
if (a.status !== 200 || a.body === null || typeof a.body !== "object")
|
|
171
|
+
return false;
|
|
172
|
+
const d = a.body.durable;
|
|
173
|
+
return d !== null && typeof d === "object" && d.tracked === true;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* The caller's rows under `name`, or "legacy" when there is no registry for this caller: the
|
|
177
|
+
* entitlement's `durable` field absent or not tracked (C01-R26, R14: zero registry calls), the
|
|
178
|
+
* entitlement refused (403/404: the launch preamble decides access), or the list route absent (404)
|
|
179
|
+
* or refused (403). Unreachable (transport, timeout, 5xx) throws session_registry_unavailable
|
|
180
|
+
* (C01-R18). A 401 is rethrown as a RegistryAnswerError (preReadRegistry defers it). The launcher
|
|
181
|
+
* reads this FIRST, before any other API call, so a down registry refuses within
|
|
182
|
+
* REGISTRY_TIMEOUT_MS.
|
|
183
|
+
*/
|
|
184
|
+
export async function registryRows(d, name) {
|
|
185
|
+
let ent;
|
|
186
|
+
try {
|
|
187
|
+
ent = await d.entitlement();
|
|
188
|
+
}
|
|
189
|
+
catch (e) {
|
|
190
|
+
if (e instanceof RegistryUnavailable)
|
|
191
|
+
throw registryDown(e.message);
|
|
192
|
+
throw e;
|
|
193
|
+
}
|
|
194
|
+
if (ent.status === 401)
|
|
195
|
+
throw new RegistryAnswerError(ent);
|
|
196
|
+
if (ent.status >= 500)
|
|
197
|
+
throw registryDown(`HTTP ${ent.status}`);
|
|
198
|
+
if (!durableTracked(ent))
|
|
199
|
+
return "legacy";
|
|
200
|
+
try {
|
|
201
|
+
return await d.listSessions({ name });
|
|
202
|
+
}
|
|
203
|
+
catch (e) {
|
|
204
|
+
if (e instanceof RegistryUnavailable)
|
|
205
|
+
throw registryDown(e.message);
|
|
206
|
+
if (e instanceof RegistryAnswerError) {
|
|
207
|
+
if (e.answer.status === 403 || e.answer.status === 404)
|
|
208
|
+
return "legacy";
|
|
209
|
+
if (e.answer.status >= 500)
|
|
210
|
+
throw registryDown(`HTTP ${e.answer.status}`);
|
|
211
|
+
}
|
|
212
|
+
throw e;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The launcher's early read (before sign-in): `registryRows`, except that a 401 (not signed in yet)
|
|
217
|
+
* defers the read to the plan, which runs after sign-in. Any other answer is final here.
|
|
218
|
+
*/
|
|
219
|
+
export async function preReadRegistry(d, name) {
|
|
220
|
+
try {
|
|
221
|
+
return await registryRows(d, name);
|
|
222
|
+
}
|
|
223
|
+
catch (e) {
|
|
224
|
+
if (e instanceof RegistryAnswerError && e.answer.status === 401)
|
|
225
|
+
return undefined;
|
|
226
|
+
throw e;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* C01-R21: `--conversation-id X` on a name live under another cid refuses (session_name_in_use, no
|
|
231
|
+
* spawn). A free name registers on X (the create route refuses another owner's cid without
|
|
232
|
+
* disclosing it), and the live row's own cid continues it.
|
|
233
|
+
*/
|
|
234
|
+
export function explicitCidRefusal(rows, explicitCid, name) {
|
|
235
|
+
if (explicitCid === null)
|
|
236
|
+
return null;
|
|
237
|
+
const live = liveRow(rows);
|
|
238
|
+
if (live === null || live.conversation_id === explicitCid)
|
|
239
|
+
return null;
|
|
240
|
+
return new DurableRefusal({
|
|
241
|
+
code: "session_name_in_use",
|
|
242
|
+
message: `mnemom agent: '${name}' is already a session on another conversation (${effectiveOf(live)}); drop --conversation-id to continue it, or pick another name`,
|
|
243
|
+
fix: null,
|
|
244
|
+
details: {},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export async function planDurableLaunch(i, deps) {
|
|
248
|
+
const rows = i.rows ?? (await registryRows(deps, i.name));
|
|
249
|
+
if (rows === "legacy")
|
|
250
|
+
return { kind: "legacy" };
|
|
251
|
+
const explicitCid = i.explicitConversationId ?? null;
|
|
252
|
+
const cidRefusal = explicitCidRefusal(rows, explicitCid, i.name);
|
|
253
|
+
if (cidRefusal !== null)
|
|
254
|
+
throw cidRefusal;
|
|
255
|
+
// C02-R24: a launch that would not end locked on a durable-only agent is refused before spawn.
|
|
256
|
+
const spawnPlan = async (p) => {
|
|
257
|
+
const durableOnly = await deps.agentDurableOnly();
|
|
258
|
+
if (durableOnly && !p.lock)
|
|
259
|
+
throw durableOnlyRefusal(i.launcherKey, i.name, null);
|
|
260
|
+
return { ...p, durableOnly };
|
|
261
|
+
};
|
|
262
|
+
const fresh = async () => {
|
|
263
|
+
const cc = deps.newId();
|
|
264
|
+
return spawnPlan({
|
|
265
|
+
kind: "spawn",
|
|
266
|
+
conversationId: explicitCid ?? deps.newId(),
|
|
267
|
+
resumeCcSessionId: null,
|
|
268
|
+
newCcSessionId: cc,
|
|
269
|
+
passthrough: [...i.passthrough],
|
|
270
|
+
register: true,
|
|
271
|
+
row: null,
|
|
272
|
+
lock: await wantsLock(i, deps, i.name),
|
|
273
|
+
});
|
|
274
|
+
};
|
|
275
|
+
if (i.newFlag) {
|
|
276
|
+
const live = liveRow(rows);
|
|
277
|
+
if (live !== null) {
|
|
278
|
+
throw new DurableRefusal({
|
|
279
|
+
code: "session_name_in_use",
|
|
280
|
+
message: `mnemom agent: '${i.name}' is already a session (${effectiveOf(live)}); end it with \`mnemom agent done ${i.name}\` or \`mnemom agent kill ${i.name}\`, or pick another name`,
|
|
281
|
+
fix: null,
|
|
282
|
+
details: {},
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
return fresh();
|
|
286
|
+
}
|
|
287
|
+
const d = continueDecision({
|
|
288
|
+
rows,
|
|
289
|
+
launcherKey: i.launcherKey,
|
|
290
|
+
home: i.home,
|
|
291
|
+
daemonLoaded: deps.daemonLoaded(),
|
|
292
|
+
agentKilled: false,
|
|
293
|
+
explicitAgentId: i.explicitAgentId,
|
|
294
|
+
});
|
|
295
|
+
switch (d.kind) {
|
|
296
|
+
case "fresh":
|
|
297
|
+
return fresh();
|
|
298
|
+
case "refuse":
|
|
299
|
+
throw new DurableRefusal({
|
|
300
|
+
code: d.code,
|
|
301
|
+
message: d.message,
|
|
302
|
+
fix: d.fix,
|
|
303
|
+
details: d.details,
|
|
304
|
+
});
|
|
305
|
+
case "attach":
|
|
306
|
+
return { kind: "attach", row: d.row, wait: d.wait };
|
|
307
|
+
case "tracked_continue": {
|
|
308
|
+
const row = d.row;
|
|
309
|
+
const cc = typeof row.cc_session_id === "string" ? row.cc_session_id : null;
|
|
310
|
+
const pt = continuePassthrough(i.passthrough, cc, i.goalGiven);
|
|
311
|
+
if (!pt.ok)
|
|
312
|
+
throw pt.refusal;
|
|
313
|
+
if (cc === null || !deps.transcriptExists(cc)) {
|
|
314
|
+
// C01-R02: mark it failed (owner route), then refuse with the --new line (name now free).
|
|
315
|
+
await deps.failTracked(row);
|
|
316
|
+
throw new DurableRefusal({
|
|
317
|
+
code: "session_transcript_missing",
|
|
318
|
+
message: `mnemom agent: Claude Code no longer has the transcript of '${i.name}'; the session is marked failed`,
|
|
319
|
+
fix: fixLine("session_transcript_missing", i.launcherKey, false, { name: i.name }),
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return spawnPlan({
|
|
323
|
+
kind: "spawn",
|
|
324
|
+
conversationId: row.conversation_id,
|
|
325
|
+
resumeCcSessionId: cc,
|
|
326
|
+
newCcSessionId: null,
|
|
327
|
+
passthrough: pt.passthrough,
|
|
328
|
+
register: false,
|
|
329
|
+
row,
|
|
330
|
+
lock: i.launcherKey === "always",
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
// ---- lock refused on the launcher path (C02-R23 c, e; C02-R24) ---------------------------------
|
|
336
|
+
/**
|
|
337
|
+
* What a launcher does after its lock attempt refused: a durable-only agent gets the
|
|
338
|
+
* durable_only_agent refusal and exit 1, never a tracked spawn (C02-R24, final F02); any other
|
|
339
|
+
* agent prints the lock refusal and its fix, then spawns tracked (exit null).
|
|
340
|
+
*/
|
|
341
|
+
export function afterLockRefusal(refusal, durableOnly, launcherKey, name) {
|
|
342
|
+
if (!durableOnly)
|
|
343
|
+
return { print: refusal, exit: null };
|
|
344
|
+
// cwd_untrusted is CLI-local (no S1 precondition clause): a durable-only launch prints it as itself,
|
|
345
|
+
// its fix shaped for the launch (rerun it), not for `mnemom agent lock`.
|
|
346
|
+
const root = refusal.details.cwd;
|
|
347
|
+
if (refusal.code === "cwd_untrusted" && typeof root === "string") {
|
|
348
|
+
const r = new DurableRefusal({
|
|
349
|
+
code: "cwd_untrusted",
|
|
350
|
+
message: `${name}: Claude Code has not been trusted in ${root}, so the durable seat would stop at its trust dialog; nothing was started`,
|
|
351
|
+
fix: `run \`cd ${shellWord(root)} && claude\` once, accept the trust dialog, then rerun \`mnemom agent ${name} --new\``,
|
|
352
|
+
details: { cwd: root },
|
|
353
|
+
});
|
|
354
|
+
return { print: r, exit: r.exit };
|
|
355
|
+
}
|
|
356
|
+
const r = durableOnlyRefusal(launcherKey, name, refusal, refusal.fixParams);
|
|
357
|
+
return { print: r, exit: r.exit };
|
|
358
|
+
}
|
|
359
|
+
// ---- lock after the first turn (dogfood B5) -----------------------------------------------------
|
|
360
|
+
/**
|
|
361
|
+
* A lock on a NEW Claude Code session (no transcript yet) waits for the session's first turn: the
|
|
362
|
+
* seat resumes the transcript, so a lock before it exists fails transcript_lost. The launcher
|
|
363
|
+
* spawns claude attended and locks once that turn has completed (lib/durable/first-turn.ts). A
|
|
364
|
+
* continued session already has its transcript and locks instead of spawning, as before.
|
|
365
|
+
*/
|
|
366
|
+
export function locksAfterFirstTurn(plan) {
|
|
367
|
+
return plan.lock && plan.newCcSessionId !== null;
|
|
368
|
+
}
|
|
369
|
+
/** The row the first-turn lock commits: the registered row, naming the CC session this launch pins. */
|
|
370
|
+
export function firstTurnRow(row, plan) {
|
|
371
|
+
return typeof row.cc_session_id === "string"
|
|
372
|
+
? row
|
|
373
|
+
: { ...row, cc_session_id: plan.newCcSessionId };
|
|
374
|
+
}
|
|
375
|
+
// ---- registration (C01-R05) ---------------------------------------------------------------------
|
|
376
|
+
/** The create body the CLI sends (A2 owns the route; see notes-c1.md). */
|
|
377
|
+
export function createBody(name, ccSessionId, cwd) {
|
|
378
|
+
return { name, cc_session_id: ccSessionId, cwd };
|
|
379
|
+
}
|
|
380
|
+
/** A create answer: the row, or a refusal (409 session_name_in_use and the rest). */
|
|
381
|
+
export function createOutcome(a, name) {
|
|
382
|
+
if (a.status === 200 || a.status === 201) {
|
|
383
|
+
const b = (a.body ?? {});
|
|
384
|
+
const row = (b.session ?? b);
|
|
385
|
+
return row;
|
|
386
|
+
}
|
|
387
|
+
const code = errorCodeOf(a.body) ?? `http_${a.status}`;
|
|
388
|
+
if (code === "session_name_in_use") {
|
|
389
|
+
return new DurableRefusal({
|
|
390
|
+
code,
|
|
391
|
+
message: `mnemom agent: '${name}' is already a session; end it with \`mnemom agent done ${name}\` or \`mnemom agent kill ${name}\`, or pick another name`,
|
|
392
|
+
fix: null,
|
|
393
|
+
apiBody: a.body,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
if (code === CODE.durable_only_agent) {
|
|
397
|
+
// Another session is locked on this agent (walk5): say what to do with it, not just "refused".
|
|
398
|
+
return new DurableRefusal({
|
|
399
|
+
code,
|
|
400
|
+
message: `mnemom agent: this agent already has a locked session, so '${name}' was not started; finish that one with \`mnemom agent done <name>\` or carry on in it with \`mnemom agent <name>\` (\`mnemom agent sessions\` lists it), or start '${name}' on another agent with \`--agent\``,
|
|
401
|
+
fix: null,
|
|
402
|
+
apiBody: a.body,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
return new DurableRefusal({
|
|
406
|
+
code,
|
|
407
|
+
message: `mnemom agent: the session registry refused '${name}' (${a.status})`,
|
|
408
|
+
fix: null,
|
|
409
|
+
apiBody: a.body,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/** The poll interval of the seat wait. */
|
|
413
|
+
export const SEAT_POLL_MS = 1000;
|
|
414
|
+
/**
|
|
415
|
+
* Attach to a locked session's seat. Non-TTY: the one S1 line and exit 0, nothing attached. TTY:
|
|
416
|
+
* wait up to ATTACH_WAIT_S for the seat, attach; on timeout exit non-zero with the seat-wait fix
|
|
417
|
+
* (the session stays locked).
|
|
418
|
+
*/
|
|
419
|
+
export async function attachToSeat(row, name, droppedFlags, deps) {
|
|
420
|
+
if (!deps.interactive) {
|
|
421
|
+
deps.out(renderNonTtyLocked(name, droppedFlags));
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
const deadline = deps.now() + ATTACH_WAIT_S * 1000;
|
|
425
|
+
for (;;) {
|
|
426
|
+
const seat = await deps.findSeat(row);
|
|
427
|
+
if (seat !== null) {
|
|
428
|
+
const status = deps.attach(seat);
|
|
429
|
+
// tmux attach exits non-zero only when it could not attach (a detach exits 0); say how back in.
|
|
430
|
+
if (status !== 0)
|
|
431
|
+
deps.err(`couldn't attach to ${name}; to get back to it run: mnemom agent ${name}`);
|
|
432
|
+
return status;
|
|
433
|
+
}
|
|
434
|
+
if (deps.now() >= deadline)
|
|
435
|
+
break;
|
|
436
|
+
await deps.sleep(SEAT_POLL_MS);
|
|
437
|
+
}
|
|
438
|
+
throw new DurableRefusal({
|
|
439
|
+
code: "seat_wait_timeout",
|
|
440
|
+
message: `${name} is locked; the host daemon will start it`,
|
|
441
|
+
fix: fixLine("seat_wait_timeout", deps.launcherKey, true, { name }),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** A mark older than this is a launch that died without clearing it. */
|
|
2
|
+
export declare const LAUNCHING_MAX_AGE_MS: number;
|
|
3
|
+
export interface LaunchingDeps {
|
|
4
|
+
readonly dir: string;
|
|
5
|
+
read(path: string): string | null;
|
|
6
|
+
write(path: string, body: string): void;
|
|
7
|
+
remove(path: string): void;
|
|
8
|
+
alive(pid: number): boolean;
|
|
9
|
+
now(): number;
|
|
10
|
+
}
|
|
11
|
+
export declare function markLaunching(name: string, pid: number, d: LaunchingDeps): void;
|
|
12
|
+
export declare function clearLaunching(name: string, d: LaunchingDeps): void;
|
|
13
|
+
/** True while a live launch of `name` started under LAUNCHING_MAX_AGE_MS ago holds the mark. */
|
|
14
|
+
export declare function launchingNow(name: string, d: LaunchingDeps): boolean;
|
|
15
|
+
export declare const liveLaunchingDeps: (mnemomDir: string) => LaunchingDeps;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The launch-start marker (dogfood walk5 leg A): a durable `mnemom agent <name>` launch marks the
|
|
3
|
+
* name as starting before it registers its session, and clears the mark once the session is
|
|
4
|
+
* registered or the launch exits. A lock that finds no session by that name while the mark is
|
|
5
|
+
* live answers "start a first turn, then lock" instead of agent_not_found, which reads like a
|
|
6
|
+
* typo in the name. The launch ledger cannot do this: its record is written after registration.
|
|
7
|
+
*
|
|
8
|
+
* Pure over injected deps; liveLaunchingDeps wires the real filesystem and process table.
|
|
9
|
+
*/
|
|
10
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
/** A mark older than this is a launch that died without clearing it. */
|
|
13
|
+
export const LAUNCHING_MAX_AGE_MS = 10 * 60 * 1000;
|
|
14
|
+
const markPath = (dir, name) => join(dir, `${encodeURIComponent(name)}.json`);
|
|
15
|
+
export function markLaunching(name, pid, d) {
|
|
16
|
+
d.write(markPath(d.dir, name), JSON.stringify({ pid, started_at: d.now() }));
|
|
17
|
+
}
|
|
18
|
+
export function clearLaunching(name, d) {
|
|
19
|
+
d.remove(markPath(d.dir, name));
|
|
20
|
+
}
|
|
21
|
+
/** True while a live launch of `name` started under LAUNCHING_MAX_AGE_MS ago holds the mark. */
|
|
22
|
+
export function launchingNow(name, d) {
|
|
23
|
+
const raw = d.read(markPath(d.dir, name));
|
|
24
|
+
if (raw === null)
|
|
25
|
+
return false;
|
|
26
|
+
let m;
|
|
27
|
+
try {
|
|
28
|
+
m = JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (typeof m !== "object" || m === null)
|
|
34
|
+
return false;
|
|
35
|
+
const { pid, started_at } = m;
|
|
36
|
+
if (typeof pid !== "number" || typeof started_at !== "number")
|
|
37
|
+
return false;
|
|
38
|
+
if (d.now() - started_at > LAUNCHING_MAX_AGE_MS)
|
|
39
|
+
return false;
|
|
40
|
+
return d.alive(pid);
|
|
41
|
+
}
|
|
42
|
+
export const liveLaunchingDeps = (mnemomDir) => {
|
|
43
|
+
const dir = join(mnemomDir, "durable", "launching");
|
|
44
|
+
return {
|
|
45
|
+
dir,
|
|
46
|
+
read: (p) => {
|
|
47
|
+
try {
|
|
48
|
+
return readFileSync(p, "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
write: (p, body) => {
|
|
55
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
56
|
+
writeFileSync(p, body, { mode: 0o600 });
|
|
57
|
+
},
|
|
58
|
+
remove: (p) => rmSync(p, { force: true }),
|
|
59
|
+
alive: (pid) => {
|
|
60
|
+
try {
|
|
61
|
+
process.kill(pid, 0);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
return e.code === "EPERM";
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
now: () => Date.now(),
|
|
69
|
+
};
|
|
70
|
+
};
|