@mnemom/mnemom 0.17.34-next.1 → 0.17.34-next.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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-run.d.ts +14 -6
- package/dist/commands/agent-run.js +44 -27
- 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-test-issuer.d.ts +67 -0
- package/dist/commands/agent-share-test-issuer.js +139 -0
- package/dist/commands/agent-share.d.ts +7 -1
- package/dist/commands/agent-share.js +14 -4
- package/dist/commands/agent.d.ts +18 -5
- package/dist/commands/agent.js +186 -59
- package/dist/commands/durable-doctor.d.ts +50 -0
- package/dist/commands/durable-doctor.js +186 -0
- package/dist/commands/durable-lock.d.ts +261 -0
- package/dist/commands/durable-lock.js +600 -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/scan.d.ts +12 -1
- package/dist/commands/scan.js +22 -3
- package/dist/commands/wrap.js +1 -1
- package/dist/durable/adopt-live.js +5 -8
- package/dist/durable/adopt.d.ts +8 -0
- package/dist/durable/adopt.js +27 -6
- package/dist/durable/cwd-rule.d.ts +9 -1
- package/dist/durable/cwd-rule.js +18 -0
- package/dist/durable/flock.d.ts +26 -0
- package/dist/durable/flock.js +80 -0
- package/dist/durable/launch-spec.d.ts +9 -1
- package/dist/durable/launch-spec.js +11 -4
- 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 +105 -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 +125 -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 +221 -0
- package/dist/host/daemon.js +616 -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 +58 -0
- package/dist/host/owner.js +123 -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 +65 -0
- package/dist/host/run.js +299 -0
- package/dist/host/runtime.d.ts +8 -0
- package/dist/host/runtime.js +22 -0
- package/dist/host/s1-names.d.ts +23 -0
- package/dist/host/s1-names.js +22 -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 +52 -0
- package/dist/host/verbs.js +282 -0
- package/dist/index.d.ts +8 -6
- package/dist/index.js +209 -43
- package/dist/lib/agent.d.ts +13 -29
- package/dist/lib/agent.js +17 -29
- package/dist/lib/claude-bin.d.ts +6 -0
- package/dist/lib/claude-bin.js +26 -0
- package/dist/lib/claude-plugin.d.ts +11 -2
- package/dist/lib/claude-plugin.js +10 -1
- package/dist/lib/commit-report.d.ts +5 -4
- package/dist/lib/commit-report.js +6 -9
- package/dist/lib/config.d.ts +8 -1
- package/dist/lib/config.js +28 -0
- package/dist/lib/durable/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/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 +199 -0
- package/dist/lib/durable/launch.js +461 -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 +190 -0
- package/dist/lib/durable/live.js +633 -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 +13 -0
- package/dist/lib/launcher-config.js +32 -0
- package/dist/lib/runner/exec-append.d.ts +15 -0
- package/dist/lib/runner/exec-append.js +2 -0
- package/dist/lib/runner/executor-client.d.ts +2 -1
- package/dist/lib/runner/executor-state.d.ts +10 -0
- package/dist/lib/runner/executor-state.js +24 -0
- package/dist/lib/runner/github-base.d.ts +24 -0
- package/dist/lib/runner/github-base.js +27 -0
- package/dist/lib/runner/job.d.ts +9 -4
- package/dist/lib/runner/job.js +43 -15
- package/dist/lib/runner/oidc.d.ts +3 -2
- package/dist/lib/runner/oidc.js +7 -4
- package/dist/lib/runner/options.d.ts +6 -0
- package/dist/lib/runner/options.js +19 -1
- package/dist/lib/runner/phases.d.ts +7 -4
- package/dist/lib/runner/phases.js +8 -4
- package/dist/lib/runner/proxy.d.ts +5 -1
- package/dist/lib/runner/proxy.js +7 -1
- package/dist/lib/scan/cost-per-pr.d.ts +93 -0
- package/dist/lib/scan/cost-per-pr.js +101 -0
- package/dist/lib/scan/record.d.ts +27 -1
- package/dist/lib/scan/record.js +114 -16
- package/dist/lib/scan/report-email.d.ts +26 -6
- package/dist/lib/scan/report-email.js +32 -3
- package/dist/lib/scan/scan.d.ts +3 -0
- package/dist/lib/scan/scan.js +13 -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,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The flat durable owner verbs (BACKBONE C1; S1 owner-verbs.ts, ROUTES.own_*; C11, C16):
|
|
3
|
+
*
|
|
4
|
+
* mnemom agent pause|resume|done|kill <name> [--json]
|
|
5
|
+
* mnemom agent amend <name> [--budget <n>] [--turns <n>] [--json]
|
|
6
|
+
* mnemom agent sessions unlock <name>
|
|
7
|
+
* mnemom agent containment pause|resume <agent>
|
|
8
|
+
*
|
|
9
|
+
* The API decides (S1 owner-verb table, first match wins); the CLI resolves the name through the
|
|
10
|
+
* registry, sends the verb, and renders the answer: a refusal with its S1 fix line and machine
|
|
11
|
+
* code, a 200 no-op with its state, a 202 device step (user_code + verification_uri_complete,
|
|
12
|
+
* polled at the server interval with the 202's device_code and client_id; never a token printed
|
|
13
|
+
* or kept), or the applied change. `done`/`kill` on a tracked session also stop the local tracked
|
|
14
|
+
* claude after the 200 (final F04; SIGTERM, SIGKILL after TRACKED_STOP_GRACE_S).
|
|
15
|
+
*
|
|
16
|
+
* Every dependency is injected (`DurableVerbDeps`), so each branch is unit-tested.
|
|
17
|
+
*/
|
|
18
|
+
import { PER_SESSION_BUDGET_LIMIT_USD, TRACKED_STOP_GRACE_S, } from "../vendor/shared/durable-contract/constants.js";
|
|
19
|
+
import { renderFixLine, } from "../vendor/shared/durable-contract/fixlines.js";
|
|
20
|
+
import { errorCodeOf, errorDetailsOf, errorMessageOf, RegistryAnswerError, RegistryUnavailable, } from "../lib/durable/api.js";
|
|
21
|
+
import { DurableRefusal, printDurableRefusal } from "../lib/durable/refusal.js";
|
|
22
|
+
import { isLocked, liveRow, stateLabel } from "../lib/durable/view.js";
|
|
23
|
+
import { CODE } from "../lib/durable/codes.js";
|
|
24
|
+
const VERB_ROUTE = {
|
|
25
|
+
pause: "own_pause",
|
|
26
|
+
resume: "own_resume",
|
|
27
|
+
done: "own_complete",
|
|
28
|
+
kill: "own_kill",
|
|
29
|
+
amend: "own_amend",
|
|
30
|
+
unlock: "own_unlock",
|
|
31
|
+
};
|
|
32
|
+
const APPLIED_TEXT = {
|
|
33
|
+
pause: "paused",
|
|
34
|
+
resume: "resumed; the host daemon picks it up",
|
|
35
|
+
done: "done",
|
|
36
|
+
kill: "killed",
|
|
37
|
+
amend: "amended",
|
|
38
|
+
unlock: "unlocked",
|
|
39
|
+
};
|
|
40
|
+
/** The session a verb acts on: the non-terminal row holding the name, else the newest row. */
|
|
41
|
+
export async function resolveTarget(api, name) {
|
|
42
|
+
let rows;
|
|
43
|
+
try {
|
|
44
|
+
rows = await api.listSessions({ name });
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
if (e instanceof RegistryAnswerError)
|
|
48
|
+
throw refusalFromAnswer(e.answer, name, "off");
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
const row = liveRow(rows) ?? rows[0] ?? null;
|
|
52
|
+
if (row === null) {
|
|
53
|
+
throw new DurableRefusal({
|
|
54
|
+
code: "agent_not_found",
|
|
55
|
+
message: `no session named ${name}`,
|
|
56
|
+
fix: null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return row;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A refusal from an API answer: the API's message and code, its rendered fix (details.fix, as the
|
|
63
|
+
* API already applied the fix-line rule), else the CLI's catalogue fallback for that code.
|
|
64
|
+
*/
|
|
65
|
+
export function refusalFromAnswer(a, name, launcherKey, row) {
|
|
66
|
+
const code = errorCodeOf(a.body) ?? `http_${a.status}`;
|
|
67
|
+
const details = errorDetailsOf(a.body);
|
|
68
|
+
const apiFix = typeof details.fix === "string" && details.fix !== "" ? details.fix : null;
|
|
69
|
+
const fix = apiFix ?? fallbackFix(code, details, name, launcherKey, row);
|
|
70
|
+
const apiMessage = errorMessageOf(a.body);
|
|
71
|
+
// The refusal line always names the session the verb was for (C02-R05: the user sees what failed).
|
|
72
|
+
const message = apiMessage === null
|
|
73
|
+
? `${name}: refused (${a.status})`
|
|
74
|
+
: apiMessage.includes(name)
|
|
75
|
+
? apiMessage
|
|
76
|
+
: `${name}: ${apiMessage}`;
|
|
77
|
+
return new DurableRefusal({ code, message: sanitizeLine(message), fix, apiBody: a.body });
|
|
78
|
+
}
|
|
79
|
+
/** A one-line message: no control characters reach the terminal. */
|
|
80
|
+
function sanitizeLine(s) {
|
|
81
|
+
return (s
|
|
82
|
+
// eslint-disable-next-line no-control-regex
|
|
83
|
+
.replace(/[\u0000-\u001f\u007f]+/g, " ")
|
|
84
|
+
.replace(/ {2,}/g, " ")
|
|
85
|
+
.trim());
|
|
86
|
+
}
|
|
87
|
+
function render(key, params, launcherKey, held) {
|
|
88
|
+
return renderFixLine(key, { launcherKey, nameHeld: held, params });
|
|
89
|
+
}
|
|
90
|
+
/** The catalogue fix the CLI prints when the API body carries none. */
|
|
91
|
+
export function fallbackFix(code, details, name, launcherKey, row) {
|
|
92
|
+
const agent = row?.agent_name ?? row?.agent_id ?? "<agent>";
|
|
93
|
+
switch (code) {
|
|
94
|
+
case CODE.durable_session_ended:
|
|
95
|
+
return details.state_reason === "agent_killed"
|
|
96
|
+
? render("agent_killed", { name }, launcherKey, false)
|
|
97
|
+
: render("session_ended", { name }, launcherKey, false);
|
|
98
|
+
case CODE.durable_agent_contained:
|
|
99
|
+
return render("agent_contained", { agent }, launcherKey, true);
|
|
100
|
+
case CODE.durable_not_owner:
|
|
101
|
+
return typeof details.owner === "string"
|
|
102
|
+
? render("not_owner_info", { owner: details.owner }, launcherKey, true)
|
|
103
|
+
: null;
|
|
104
|
+
case CODE.durable_session_login_required:
|
|
105
|
+
return render("device_step_login", {}, launcherKey, true);
|
|
106
|
+
case CODE.durable_unsupported:
|
|
107
|
+
if (details.feature === "launch_spec_change") {
|
|
108
|
+
// The flags come from the answer; without them there is no line to print.
|
|
109
|
+
const flags = Array.isArray(details.flags)
|
|
110
|
+
? details.flags.filter((f) => typeof f === "string").join(" ")
|
|
111
|
+
: typeof details.flags === "string"
|
|
112
|
+
? details.flags
|
|
113
|
+
: "";
|
|
114
|
+
return flags.trim() === ""
|
|
115
|
+
? null
|
|
116
|
+
: render("launch_spec_change", { name, flags: flags.trim() }, launcherKey, true);
|
|
117
|
+
}
|
|
118
|
+
if (details.feature === "above_limit_raise")
|
|
119
|
+
return render("above_limit_raise", { name, limit: String(PER_SESSION_BUDGET_LIMIT_USD) }, launcherKey, true);
|
|
120
|
+
return null;
|
|
121
|
+
default:
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
function isRecord(v) {
|
|
126
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
127
|
+
}
|
|
128
|
+
export function deviceStepOf(body) {
|
|
129
|
+
if (!isRecord(body))
|
|
130
|
+
return null;
|
|
131
|
+
const b = (isRecord(body.device_step) ? body.device_step : body);
|
|
132
|
+
const s = (k) => (typeof b[k] === "string" && b[k] !== "" ? b[k] : null);
|
|
133
|
+
const user = s("user_code");
|
|
134
|
+
const uri = s("verification_uri_complete");
|
|
135
|
+
const dc = s("device_code");
|
|
136
|
+
const cid = s("client_id");
|
|
137
|
+
if (!user || !uri || !dc || !cid)
|
|
138
|
+
return null;
|
|
139
|
+
const interval = typeof b.interval === "number" && b.interval > 0 ? b.interval : 5;
|
|
140
|
+
return {
|
|
141
|
+
user_code: user,
|
|
142
|
+
verification_uri_complete: uri,
|
|
143
|
+
device_code: dc,
|
|
144
|
+
interval,
|
|
145
|
+
client_id: cid,
|
|
146
|
+
...(s("approval_id") ? { approval_id: s("approval_id") } : {}),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
/** How long the CLI keeps polling a device step before giving up (the approval TTL). */
|
|
150
|
+
export const DEVICE_STEP_POLL_MAX_MS = 15 * 60 * 1000;
|
|
151
|
+
/**
|
|
152
|
+
* Print the device step and poll until it resolves. 0 when applied; throws a DurableRefusal when
|
|
153
|
+
* denied, expired or refused. Never prints the device_code or any token.
|
|
154
|
+
*/
|
|
155
|
+
export async function runDeviceStep(step, note, deps) {
|
|
156
|
+
deps.err(`Approve this change: open ${step.verification_uri_complete}`);
|
|
157
|
+
deps.err(`and confirm the code ${step.user_code}.`);
|
|
158
|
+
if (note)
|
|
159
|
+
deps.err(note);
|
|
160
|
+
let interval = step.interval * 1000;
|
|
161
|
+
const deadline = deps.now() + DEVICE_STEP_POLL_MAX_MS;
|
|
162
|
+
for (;;) {
|
|
163
|
+
await deps.sleep(interval);
|
|
164
|
+
const a = await deps.api.devicePoll(step.device_code, step.client_id);
|
|
165
|
+
const code = errorCodeOf(a.body);
|
|
166
|
+
if (a.status < 300 && code === null) {
|
|
167
|
+
deps.err("approved and applied");
|
|
168
|
+
return 0;
|
|
169
|
+
}
|
|
170
|
+
if (code === "authorization_pending" || code === "slow_down") {
|
|
171
|
+
if (code === "slow_down")
|
|
172
|
+
interval += 5000;
|
|
173
|
+
if (deps.now() + interval > deadline) {
|
|
174
|
+
throw new DurableRefusal({
|
|
175
|
+
code: "expired_token",
|
|
176
|
+
message: "the approval expired",
|
|
177
|
+
fix: null,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
const why = code === "access_denied"
|
|
183
|
+
? "the approval was denied"
|
|
184
|
+
: code === "expired_token"
|
|
185
|
+
? "the approval expired"
|
|
186
|
+
: (errorMessageOf(a.body) ?? "the approval was refused");
|
|
187
|
+
throw new DurableRefusal({ code: code ?? `http_${a.status}`, message: why, fix: null });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/** Run one verb end to end and return its exit status (refusals rendered here). */
|
|
191
|
+
export async function runVerb(verb, name, body, opts, deps) {
|
|
192
|
+
const launcherKey = deps.launcherKey();
|
|
193
|
+
try {
|
|
194
|
+
const row = await resolveTarget(deps.api, name);
|
|
195
|
+
const a = await deps.api.ownerVerb(VERB_ROUTE[verb], row.conversation_id, row.agent_id, body);
|
|
196
|
+
if (a.status === 202) {
|
|
197
|
+
const step = deviceStepOf(a.body);
|
|
198
|
+
if (step === null) {
|
|
199
|
+
throw new DurableRefusal({
|
|
200
|
+
code: "device_step_malformed",
|
|
201
|
+
message: "the API answered 202 without a usable device step",
|
|
202
|
+
fix: null,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
const contained = row.state === "paused" && row.state_reason === "agent_contained";
|
|
206
|
+
const note = contained
|
|
207
|
+
? "an org admin other than you must approve this unpause (a solo org approves it itself)"
|
|
208
|
+
: null;
|
|
209
|
+
const rc = await runDeviceStep(step, note, deps);
|
|
210
|
+
if (opts.json)
|
|
211
|
+
deps.out(JSON.stringify({ name, verb, applied: true }));
|
|
212
|
+
return rc;
|
|
213
|
+
}
|
|
214
|
+
if (a.status >= 200 && a.status < 300) {
|
|
215
|
+
if (verb === "done" || verb === "kill") {
|
|
216
|
+
// An ending verb fails closed: only an answer that says the session ended (or a re-read
|
|
217
|
+
// that shows it) counts, and only then is a tracked claude stopped. Never SIGKILL on an
|
|
218
|
+
// ambiguous 2xx (a null body, a proxy-swallowed body, schema drift).
|
|
219
|
+
if (!(isRecord(a.body) && a.body.changed === false)) {
|
|
220
|
+
await confirmEnded(verb, row, a, deps.api, name);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const changed = !(isRecord(a.body) && a.body.changed === false);
|
|
224
|
+
// --json: stdout carries the one JSON document; the human lines go to stderr.
|
|
225
|
+
const say = opts.json ? deps.err : deps.out;
|
|
226
|
+
if (!changed) {
|
|
227
|
+
say(`${name}: no change (${stateLabel(row)})`);
|
|
228
|
+
if (verb === "resume")
|
|
229
|
+
say(`fix: ${render("resume_live", { name }, launcherKey, true)}`);
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
say(`${name}: ${APPLIED_TEXT[verb]}`);
|
|
233
|
+
if ((verb === "done" || verb === "kill") && !isLocked(row) && deps.stopTrackedClaude) {
|
|
234
|
+
await deps.stopTrackedClaude(row, TRACKED_STOP_GRACE_S);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (opts.json)
|
|
238
|
+
deps.out(JSON.stringify(a.body ?? { changed }));
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
throw refusalFromAnswer(a, name, launcherKey, row);
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
return handleVerbError(e, opts.json === true, deps);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
/** The state an applied done / kill writes (S1 owner-verbs: complete -> completed, kill -> killed). */
|
|
248
|
+
const ENDED_STATE = { done: "completed", kill: "killed" };
|
|
249
|
+
/**
|
|
250
|
+
* Confirm an applied done / kill: the answer's own `{changed: true, state}` when it names the ended
|
|
251
|
+
* state, else one re-read of the session by conversation id. Throws a refusal (nothing stopped
|
|
252
|
+
* locally) when neither shows the session ended.
|
|
253
|
+
*/
|
|
254
|
+
export async function confirmEnded(verb, row, a, api, name) {
|
|
255
|
+
const want = ENDED_STATE[verb];
|
|
256
|
+
if (isRecord(a.body) && a.body.changed === true && a.body.state === want)
|
|
257
|
+
return;
|
|
258
|
+
const seen = await api.listSessions({ name }).then((rows) => rows.find((r) => r.conversation_id === row.conversation_id) ?? null, () => null);
|
|
259
|
+
if (seen !== null && seen.state === want)
|
|
260
|
+
return;
|
|
261
|
+
throw new DurableRefusal({
|
|
262
|
+
code: "verb_unconfirmed",
|
|
263
|
+
message: `${name}: the API answered ${a.status} but did not confirm the session ${want === "completed" ? "completed" : "was killed"}; nothing was stopped locally`,
|
|
264
|
+
fix: null,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
export function handleVerbError(e, json, deps) {
|
|
268
|
+
if (e instanceof DurableRefusal)
|
|
269
|
+
return printDurableRefusal(e, json, deps);
|
|
270
|
+
if (e instanceof RegistryUnavailable) {
|
|
271
|
+
return printDurableRefusal(new DurableRefusal({
|
|
272
|
+
code: "session_registry_unavailable",
|
|
273
|
+
message: `the session registry is unreachable (${e.message}); nothing was changed`,
|
|
274
|
+
fix: null,
|
|
275
|
+
}), json, deps);
|
|
276
|
+
}
|
|
277
|
+
throw e;
|
|
278
|
+
}
|
|
279
|
+
/** Parse a positive amount (amend --budget); `integer` for a count (amend --turns). */
|
|
280
|
+
export function parsePositive(flag, raw, integer = false) {
|
|
281
|
+
if (raw === undefined)
|
|
282
|
+
return undefined;
|
|
283
|
+
const n = Number(raw);
|
|
284
|
+
if (!Number.isFinite(n) || n <= 0 || (integer && !Number.isInteger(n))) {
|
|
285
|
+
throw new DurableRefusal({
|
|
286
|
+
code: "invalid_argument",
|
|
287
|
+
message: `${flag} must be a positive ${integer ? "whole number" : "number"} (got '${raw}')`,
|
|
288
|
+
fix: null,
|
|
289
|
+
exit: 2,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
return n;
|
|
293
|
+
}
|
|
294
|
+
export async function amendCommand(name, opts, deps) {
|
|
295
|
+
let budget;
|
|
296
|
+
let turns;
|
|
297
|
+
try {
|
|
298
|
+
budget = parsePositive("--budget", opts.budget);
|
|
299
|
+
turns = parsePositive("--turns", opts.turns, true);
|
|
300
|
+
}
|
|
301
|
+
catch (e) {
|
|
302
|
+
return handleVerbError(e, opts.json === true, deps);
|
|
303
|
+
}
|
|
304
|
+
if (budget === undefined && turns === undefined) {
|
|
305
|
+
deps.err("mnemom agent amend: give --budget <n> and/or --turns <n>");
|
|
306
|
+
return 2;
|
|
307
|
+
}
|
|
308
|
+
const body = {};
|
|
309
|
+
if (budget !== undefined)
|
|
310
|
+
body.budget_usd = budget;
|
|
311
|
+
if (turns !== undefined)
|
|
312
|
+
body.max_turns = turns;
|
|
313
|
+
return runVerb("amend", name, body, opts, deps);
|
|
314
|
+
}
|
|
315
|
+
export async function unlockCommand(name, deps) {
|
|
316
|
+
return runVerb("unlock", name, {}, {}, deps);
|
|
317
|
+
}
|
|
318
|
+
/**
|
|
319
|
+
* `mnemom agent containment pause|resume <agent>` (BACKBONE C1, amendment 35; C16-R30): a thin
|
|
320
|
+
* verb over POST /v1/orgs/{org}/agents/{id}/pause|resume. The org comes from the agent record,
|
|
321
|
+
* never the active org. It needs a session login: on an API-key-only credential, or the api's
|
|
322
|
+
* 403 org_scope_requires_session, it exits non-zero with the containment_login line and never
|
|
323
|
+
* sends the API key or prints the raw 403.
|
|
324
|
+
*/
|
|
325
|
+
export async function containmentCommand(verb, agentRef, deps) {
|
|
326
|
+
const launcherKey = deps.launcherKey();
|
|
327
|
+
const loginRefusal = () => new DurableRefusal({
|
|
328
|
+
// Never the raw 403 code (C11-R26): the machine code is the S1 fix key.
|
|
329
|
+
code: "containment_login",
|
|
330
|
+
message: `mnemom agent containment ${verb}: org containment acts as a signed-in org admin`,
|
|
331
|
+
fix: render("containment_login", {}, launcherKey, false),
|
|
332
|
+
});
|
|
333
|
+
try {
|
|
334
|
+
if ((await deps.api.credentialType()) !== "jwt")
|
|
335
|
+
throw loginRefusal();
|
|
336
|
+
const { orgId, agentId } = await resolveContainmentAgent(agentRef, deps, launcherKey);
|
|
337
|
+
const a = await deps.api.containment(orgId, agentId, verb);
|
|
338
|
+
if (a.status === 403 && errorCodeOf(a.body) === "org_scope_requires_session")
|
|
339
|
+
throw loginRefusal();
|
|
340
|
+
if (a.status >= 200 && a.status < 300) {
|
|
341
|
+
deps.out(`agent ${agentRef}: ${verb === "pause" ? "paused (contained)" : "active again"}`);
|
|
342
|
+
return 0;
|
|
343
|
+
}
|
|
344
|
+
throw refusalFromAnswer(a, agentRef, launcherKey);
|
|
345
|
+
}
|
|
346
|
+
catch (e) {
|
|
347
|
+
return handleVerbError(e, false, deps);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** An agent id as the CLI recognises one (lib/api.ts resolveAgentId). */
|
|
351
|
+
export const AGENT_ID_RE = /^(smolt-[0-9a-f]{8}|mnm-[0-9a-f-]{36})$/;
|
|
352
|
+
/**
|
|
353
|
+
* The containment target and its org, from the agent record (never the active org): an id is read
|
|
354
|
+
* with GET /v1/agents/{id}; a name is looked up in the caller's org fleets and must match exactly one.
|
|
355
|
+
*/
|
|
356
|
+
export async function resolveContainmentAgent(agentRef, deps, launcherKey) {
|
|
357
|
+
if (AGENT_ID_RE.test(agentRef)) {
|
|
358
|
+
const ag = await deps.api.getAgent(agentRef);
|
|
359
|
+
if (ag.status !== 200 || !isRecord(ag.body))
|
|
360
|
+
throw refusalFromAnswer(ag, agentRef, launcherKey);
|
|
361
|
+
const rec = (isRecord(ag.body.agent) ? ag.body.agent : ag.body);
|
|
362
|
+
const orgId = typeof rec.org_id === "string" ? rec.org_id : null;
|
|
363
|
+
if (orgId === null) {
|
|
364
|
+
throw new DurableRefusal({
|
|
365
|
+
code: "agent_not_found",
|
|
366
|
+
message: `agent ${agentRef} has no org on record`,
|
|
367
|
+
fix: null,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return { orgId, agentId: typeof rec.id === "string" ? rec.id : agentRef };
|
|
371
|
+
}
|
|
372
|
+
const found = await deps.api.agentsByName(agentRef);
|
|
373
|
+
if (found.length === 0) {
|
|
374
|
+
throw new DurableRefusal({
|
|
375
|
+
code: "agent_not_found",
|
|
376
|
+
message: `no agent named ${agentRef} in your orgs`,
|
|
377
|
+
fix: null,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
if (found.length > 1) {
|
|
381
|
+
throw new DurableRefusal({
|
|
382
|
+
code: "agent_ambiguous",
|
|
383
|
+
message: `${found.length} agents are named ${agentRef} in your orgs (${found.map((a) => a.id).join(", ")}); name one by its id`,
|
|
384
|
+
fix: null,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const [only] = found;
|
|
388
|
+
return { orgId: only.org_id, agentId: only.id };
|
|
389
|
+
}
|
|
390
|
+
/** The thin verbs (pause/resume/done/kill) with no body. */
|
|
391
|
+
export async function ownerVerbCommand(verb, name, opts, deps) {
|
|
392
|
+
return runVerb(verb, name, {}, opts, deps);
|
|
393
|
+
}
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -5,9 +5,11 @@
|
|
|
5
5
|
* arm is unit-testable without Commander (same pattern as commands/usage.ts).
|
|
6
6
|
*/
|
|
7
7
|
import { type GitHubCredential } from "../lib/scan/credential.js";
|
|
8
|
-
import { type ScanDeps } from "../lib/scan/scan.js";
|
|
8
|
+
import { type ScanDeps, type ScanResult } from "../lib/scan/scan.js";
|
|
9
|
+
import { selectSpend } from "../lib/scan/spend/spend-tiers.js";
|
|
9
10
|
import { type AnthropicUsageDeps } from "../lib/scan/spend/anthropic-usage.js";
|
|
10
11
|
import { type RecordDeps, type RecordOutcome } from "../lib/scan/record.js";
|
|
12
|
+
import { type CostSpend } from "../lib/scan/cost-per-pr.js";
|
|
11
13
|
import { type ReportEmailDeps } from "../lib/scan/report-email.js";
|
|
12
14
|
import { type ReportRecipients } from "../lib/scan/share.js";
|
|
13
15
|
export interface ScanCommandOptions {
|
|
@@ -64,6 +66,15 @@ export declare function resolveRecipients(o: {
|
|
|
64
66
|
}): Promise<ReportRecipients>;
|
|
65
67
|
/** The CTO's figure in USD, or null for none. Throws fixed text only; the input is never echoed. */
|
|
66
68
|
export declare function resolveSelfReportedSpend(o: ScanCommandOptions): Promise<number | null>;
|
|
69
|
+
/**
|
|
70
|
+
* The one spend the cost-per-PR figures split, matching the tier the scan chose. Null leaves them out.
|
|
71
|
+
* Tier 1 sends the measured Anthropic total; tier 2 NEVER does (a figure split from the stated monthly
|
|
72
|
+
* spend would let that number be recovered, and the tier 2 line promises it is "not sent"); tier 3 (or
|
|
73
|
+
* tiers off) the modelled range. Sources are never mixed.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveCostSpend(o: ScanCommandOptions, result: ScanResult, spend: ReturnType<typeof selectSpend> | null, measured: {
|
|
76
|
+
usd: number;
|
|
77
|
+
} | null): CostSpend | null;
|
|
67
78
|
export declare const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
|
|
68
79
|
export declare const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
|
|
69
80
|
/**
|
package/dist/commands/scan.js
CHANGED
|
@@ -13,6 +13,7 @@ import { ADMIN_KEY_ENV, AnthropicUsageError, fetchMeasuredUsage, isAdminKeyShape
|
|
|
13
13
|
import { getAuthInfo } from "../lib/auth.js";
|
|
14
14
|
import { listMyOrgs } from "../lib/api.js";
|
|
15
15
|
import { fetchRecordTotals, isOrgId, } from "../lib/scan/record.js";
|
|
16
|
+
import { modelledSpend } from "../lib/scan/cost-per-pr.js";
|
|
16
17
|
import { RECORD_NO_ORG_NOTE, renderRecordSection } from "../lib/scan/report.js";
|
|
17
18
|
import { buildEmailBody, buildEmailReport, DRY_RUN_MSG, UNAVAILABLE_MSG, failureMessage, ReportEmailError, sendReportEmail, } from "../lib/scan/report-email.js";
|
|
18
19
|
import { buildSharePayload, parseShareLevel, promptShareLevel, } from "../lib/scan/share.js";
|
|
@@ -67,6 +68,23 @@ export async function resolveSelfReportedSpend(o) {
|
|
|
67
68
|
throw new Error(SPEND_INVALID_MSG);
|
|
68
69
|
return n;
|
|
69
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The one spend the cost-per-PR figures split, matching the tier the scan chose. Null leaves them out.
|
|
73
|
+
* Tier 1 sends the measured Anthropic total; tier 2 NEVER does (a figure split from the stated monthly
|
|
74
|
+
* spend would let that number be recovered, and the tier 2 line promises it is "not sent"); tier 3 (or
|
|
75
|
+
* tiers off) the modelled range. Sources are never mixed.
|
|
76
|
+
*/
|
|
77
|
+
export function resolveCostSpend(o, result, spend, measured) {
|
|
78
|
+
if (spend === null)
|
|
79
|
+
return modelledSpend(result.spend);
|
|
80
|
+
if (spend.tier === 1) {
|
|
81
|
+
const usd = measured?.usd ?? o.anthropicUsageUsd;
|
|
82
|
+
return typeof usd === "number" ? { source: "anthropic_usage", usd } : null;
|
|
83
|
+
}
|
|
84
|
+
if (spend.tier === 2)
|
|
85
|
+
return null;
|
|
86
|
+
return spend.tier === 3 ? modelledSpend(result.spend) : null;
|
|
87
|
+
}
|
|
70
88
|
export const NO_ADMIN_KEY_TERMINAL_MSG = "Set ANTHROPIC_ADMIN_KEY (there is no terminal to ask on).";
|
|
71
89
|
export const TIER1_FALLBACK_NOTE = "Tier 1 skipped: falling back to the next tier.";
|
|
72
90
|
/**
|
|
@@ -194,7 +212,7 @@ export async function scanCommand(o) {
|
|
|
194
212
|
const level = parseShareLevel(o.share) ?? (await promptShareLevel());
|
|
195
213
|
// Every successful scan emails the report to the person who ran it, at every share level, no opt-out.
|
|
196
214
|
// Mnemom keeps nothing from a share=none send (the server sends the email and does nothing else).
|
|
197
|
-
const emailFailure = await emailReport(o, emailOrg, result, level, period, spend);
|
|
215
|
+
const emailFailure = await emailReport(o, emailOrg, result, level, spend, period, resolveCostSpend(o, result, spend, measured));
|
|
198
216
|
if (level === "none") {
|
|
199
217
|
console.log("Share level: nothing. No share payload is built; Mnemom keeps nothing (the report email is only sent to you).");
|
|
200
218
|
if (emailFailure)
|
|
@@ -211,16 +229,17 @@ export async function scanCommand(o) {
|
|
|
211
229
|
* Emails the report. Loud on every failure: prints the reason name, never the server's words, and
|
|
212
230
|
* returns it so the caller exits non-zero after the local report is shown.
|
|
213
231
|
*/
|
|
214
|
-
async function emailReport(o, org, result, level, period,
|
|
232
|
+
async function emailReport(o, org, result, level, spend, period, costSpend) {
|
|
215
233
|
const fail = (f) => {
|
|
216
234
|
console.log(failureMessage(f));
|
|
217
235
|
return f;
|
|
218
236
|
};
|
|
219
237
|
const report = buildEmailReport(result, {
|
|
220
238
|
orgId: org,
|
|
221
|
-
period,
|
|
222
239
|
now: o.now ?? Date.now,
|
|
223
240
|
sendSpendRange: spend ? spend.shareable && spend.tier === 3 : true,
|
|
241
|
+
period,
|
|
242
|
+
costSpend,
|
|
224
243
|
});
|
|
225
244
|
const body = buildEmailBody(report, level, result.coverage.complete, o.ctoEmail?.trim());
|
|
226
245
|
const outcome = await sendReportEmail(org, body, o.emailDeps);
|
package/dist/commands/wrap.js
CHANGED
|
@@ -11,7 +11,7 @@ import { executorStateOk, executorStatePath, LAUNCHER_KEYS, } from "../vendor/du
|
|
|
11
11
|
import { getApiUrl, MNEMOM_DIR } from "../lib/config.js";
|
|
12
12
|
import { resolveAuth } from "../lib/auth.js";
|
|
13
13
|
import { readSessionRecords } from "../lib/agent-ledger.js";
|
|
14
|
-
import { LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
|
|
14
|
+
import { durableLauncherKey, LAUNCHER_CONFIG_PATH } from "../lib/launcher-config.js";
|
|
15
15
|
import { mnemomFetch } from "../lib/version-gate.js";
|
|
16
16
|
import { httpAdoptApi } from "./adopt-api.js";
|
|
17
17
|
import { liveCwdDeps } from "./cwd-rule.js";
|
|
@@ -37,21 +37,18 @@ export function ledgerFactsFrom(records, cid) {
|
|
|
37
37
|
}
|
|
38
38
|
/** The launcher key `durable` (MNEMOM_AGENT_DURABLE, then launcher.toml), default off. */
|
|
39
39
|
export function launcherKeyFrom(env, tomlText) {
|
|
40
|
-
|
|
41
|
-
const e = env.MNEMOM_AGENT_DURABLE?.trim().toLowerCase();
|
|
42
|
-
if (ok(e))
|
|
43
|
-
return e;
|
|
40
|
+
let configured;
|
|
44
41
|
if (tomlText !== null) {
|
|
45
42
|
try {
|
|
46
43
|
const v = parseToml(tomlText).durable;
|
|
47
|
-
if (
|
|
48
|
-
|
|
44
|
+
if (typeof v === "string" && LAUNCHER_KEYS.includes(v))
|
|
45
|
+
configured = v;
|
|
49
46
|
}
|
|
50
47
|
catch {
|
|
51
48
|
// an unparseable launcher.toml is the launcher's error to report, not adopt's
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
|
-
return
|
|
51
|
+
return durableLauncherKey(env, configured);
|
|
55
52
|
}
|
|
56
53
|
/** The executor state file, or null when absent or not the frozen S1 shape. */
|
|
57
54
|
export function executorStateFrom(text) {
|
package/dist/durable/adopt.d.ts
CHANGED
|
@@ -172,12 +172,20 @@ export interface AdoptPlan {
|
|
|
172
172
|
export declare function ruleCwd(live: LiveClaude, realpath: (p: string) => string | null): string | null;
|
|
173
173
|
/** The session row of a GET answer: the body itself, or its `session`; agent_id also from `agent.id`. */
|
|
174
174
|
export declare function rowOf(a: ApiAnswer): SessionRow | null;
|
|
175
|
+
/**
|
|
176
|
+
* A session's fix lines name it by its registry row's own name (the row discovery matched by
|
|
177
|
+
* conversation id); only then does the name hold a row, so only then may a line carry the done step.
|
|
178
|
+
* The environment's name is the launcher's display name (agent-scenario), which no verb resolves.
|
|
179
|
+
*/
|
|
180
|
+
export declare function nameHeldOf(s: Pick<PlannedSession, "name" | "_row">): boolean;
|
|
175
181
|
/** What a refusal's fix line may be filled from. */
|
|
176
182
|
export interface RefusalContext {
|
|
177
183
|
name: string;
|
|
178
184
|
launcherKey: LauncherKey;
|
|
179
185
|
/** The group's agent (for agent_contained when the refusal names none). */
|
|
180
186
|
agent?: string;
|
|
187
|
+
/** Whether the name holds a registry row (nameHeldOf): only then does the line carry the done step. */
|
|
188
|
+
nameHeld: boolean;
|
|
181
189
|
}
|
|
182
190
|
/**
|
|
183
191
|
* The S1 fix line for a lock refusal answered by the check or the lock route, or null when the
|
package/dist/durable/adopt.js
CHANGED
|
@@ -76,6 +76,14 @@ export function rowOf(a) {
|
|
|
76
76
|
return { ...inner, agent_id: inner.agent.id };
|
|
77
77
|
return inner;
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* A session's fix lines name it by its registry row's own name (the row discovery matched by
|
|
81
|
+
* conversation id); only then does the name hold a row, so only then may a line carry the done step.
|
|
82
|
+
* The environment's name is the launcher's display name (agent-scenario), which no verb resolves.
|
|
83
|
+
*/
|
|
84
|
+
export function nameHeldOf(s) {
|
|
85
|
+
return typeof s._row?.name === "string" && s._row.name !== "" && s._row.name === s.name;
|
|
86
|
+
}
|
|
79
87
|
async function planSession(live, deps, launcherKey) {
|
|
80
88
|
const facts = deps.ledger(live.conversationId);
|
|
81
89
|
const base = {
|
|
@@ -117,6 +125,8 @@ async function planSession(live, deps, launcherKey) {
|
|
|
117
125
|
if (!row)
|
|
118
126
|
return unadoptable("row_unreadable");
|
|
119
127
|
Object.assign(base, { _row: row });
|
|
128
|
+
if (typeof row.name === "string" && row.name !== "")
|
|
129
|
+
base.name = row.name;
|
|
120
130
|
const agentId = row.agent_id ?? facts.agentId;
|
|
121
131
|
if (row.durable_mode === "locked") {
|
|
122
132
|
await compareLocked(base, live, row, deps, launcherKey);
|
|
@@ -137,7 +147,7 @@ async function planSession(live, deps, launcherKey) {
|
|
|
137
147
|
base.cwd_kind = verdict.cwd_kind;
|
|
138
148
|
base.reason = `cwd_${verdict.cwd_kind}`;
|
|
139
149
|
// A tracked row holds the name, so the fix carries the done clause (S1 fix-line rule).
|
|
140
|
-
base.fix = cwdFixLine(verdict, { name:
|
|
150
|
+
base.fix = cwdFixLine(verdict, { name: base.name, launcherKey, nameHeld: nameHeldOf(base) });
|
|
141
151
|
base.note = cwdNote(verdict.cwd_kind, base.fix);
|
|
142
152
|
return { s: base, agentId, agentLabel: facts.agentLabel };
|
|
143
153
|
}
|
|
@@ -199,7 +209,8 @@ async function compareLocked(base, live, row, deps, launcherKey) {
|
|
|
199
209
|
// read (gaps/c4-relock-sole-unlock.md): it is never offered from here.
|
|
200
210
|
// launchSpecChangeFixLines' tuple type guarantees the launch_spec_change line is first.
|
|
201
211
|
base.fix = launchSpecChangeFixLines({
|
|
202
|
-
name:
|
|
212
|
+
name: base.name,
|
|
213
|
+
nameHeld: nameHeldOf(base),
|
|
203
214
|
flags: specUserArgv(built.spec).join(" "),
|
|
204
215
|
launcherKey,
|
|
205
216
|
soleLockedSession: false,
|
|
@@ -234,7 +245,7 @@ export function refusalFix(code, details, ctx) {
|
|
|
234
245
|
params.repo = repos.join(", ");
|
|
235
246
|
if (placeholdersOf(key).some((p) => params[p] === undefined))
|
|
236
247
|
return null;
|
|
237
|
-
return renderFixLine(key, { launcherKey: ctx.launcherKey, nameHeld:
|
|
248
|
+
return renderFixLine(key, { launcherKey: ctx.launcherKey, nameHeld: ctx.nameHeld, params });
|
|
238
249
|
}
|
|
239
250
|
const LOCK_REFUSAL_FIX_CODES = Object.freeze({
|
|
240
251
|
durable_lock_not_entitled: true,
|
|
@@ -299,7 +310,7 @@ export async function planAdopt(deps, opts = {}) {
|
|
|
299
310
|
const checkout = root ?? ownCheckout ?? null;
|
|
300
311
|
if (checkout === null)
|
|
301
312
|
continue;
|
|
302
|
-
s.fix = cwdFixLine({ ok: false, cwd_kind: "not_git", params: { checkout } }, { name: s.name, launcherKey, nameHeld:
|
|
313
|
+
s.fix = cwdFixLine({ ok: false, cwd_kind: "not_git", params: { checkout } }, { name: s.name, launcherKey, nameHeld: nameHeldOf(s) });
|
|
303
314
|
s.note = cwdNote("not_git", s.fix);
|
|
304
315
|
}
|
|
305
316
|
const adoptable = g.sessions.filter((s) => s.verdict === "adopt");
|
|
@@ -343,7 +354,12 @@ export async function planAdopt(deps, opts = {}) {
|
|
|
343
354
|
else if (a.kind === "error") {
|
|
344
355
|
g.code = a.code ?? `http_${a.status}`;
|
|
345
356
|
g.reason = a.message;
|
|
346
|
-
g.fix = refusalFix(a.code, a.details, {
|
|
357
|
+
g.fix = refusalFix(a.code, a.details, {
|
|
358
|
+
name: s.name,
|
|
359
|
+
launcherKey,
|
|
360
|
+
agent: g.agent,
|
|
361
|
+
nameHeld: nameHeldOf(s),
|
|
362
|
+
});
|
|
347
363
|
}
|
|
348
364
|
else {
|
|
349
365
|
g.code = "durable_precondition_unavailable";
|
|
@@ -513,7 +529,12 @@ export async function handover(s, g, exec, deps, facts, launcherKey) {
|
|
|
513
529
|
name: s.name,
|
|
514
530
|
committed: false,
|
|
515
531
|
code: a.code ?? `http_${a.status}`,
|
|
516
|
-
fix: refusalFix(a.code, a.details, {
|
|
532
|
+
fix: refusalFix(a.code, a.details, {
|
|
533
|
+
name: s.name,
|
|
534
|
+
launcherKey,
|
|
535
|
+
agent: g.agent,
|
|
536
|
+
nameHeld: nameHeldOf(s),
|
|
537
|
+
}),
|
|
517
538
|
};
|
|
518
539
|
}
|
|
519
540
|
// No attempt was decisive and a request may still be in flight: only a re-read that shows the
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* The decision is pure over injected facts; liveCwdDeps is the thin driver.
|
|
19
19
|
*/
|
|
20
|
-
import { type CwdKind, type LauncherKey } from "../vendor/durable-contract/index.js";
|
|
20
|
+
import { type CwdCheckCause, type CwdKind, type LauncherKey } from "../vendor/durable-contract/index.js";
|
|
21
21
|
export interface CwdDeps {
|
|
22
22
|
/** realpath(p), or null when it does not resolve. */
|
|
23
23
|
realpath(p: string): string | null;
|
|
@@ -48,6 +48,14 @@ export declare function projectSlug(path: string): string;
|
|
|
48
48
|
* not --resume it).
|
|
49
49
|
*/
|
|
50
50
|
export declare function classifyCwd(cwd: string, deps: CwdDeps, ccSessionId?: string | null): CwdVerdict;
|
|
51
|
+
/**
|
|
52
|
+
* The daemon's spawn-time cwd check (attestation check `cwd`, C14-R04; mnemom-test #1002): the
|
|
53
|
+
* committed cwd's realpath must equal `git rev-parse --show-toplevel` there. Null when it holds;
|
|
54
|
+
* otherwise the S1 CWD_CHECK_CAUSES value: `missing` when there is no committed cwd or it does not
|
|
55
|
+
* resolve, `not_toplevel` when it resolves but is not a work tree's top level (a subdirectory, or
|
|
56
|
+
* not in a git work tree at all).
|
|
57
|
+
*/
|
|
58
|
+
export declare function spawnCwdCause(cwd: string | null, deps: Pick<CwdDeps, "realpath" | "gitToplevel">): CwdCheckCause | null;
|
|
51
59
|
/**
|
|
52
60
|
* The S1 fix line for a cwd refusal (fix-line rule: the done clause when a non-terminal row holds
|
|
53
61
|
* the name, the config suffix when the launcher key is not `always`), or null when a path the
|