@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,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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type LockResult = {
|
|
2
|
+
ok: true;
|
|
3
|
+
fd: number;
|
|
4
|
+
} | {
|
|
5
|
+
ok: false;
|
|
6
|
+
reason: "held" | "error";
|
|
7
|
+
detail: string;
|
|
8
|
+
};
|
|
9
|
+
/** The helper commands, tried in order. Each locks fd 3 and exits 0 (taken), 3 (held), else error. */
|
|
10
|
+
export declare const FLOCK_HELPERS: readonly (readonly string[])[];
|
|
11
|
+
export interface FlockDeps {
|
|
12
|
+
openSync: (p: string, flags: string, mode: number) => number;
|
|
13
|
+
closeSync: (fd: number) => void;
|
|
14
|
+
mkdirSync: (p: string, opts: {
|
|
15
|
+
recursive: true;
|
|
16
|
+
mode: number;
|
|
17
|
+
}) => unknown;
|
|
18
|
+
run: (argv: readonly string[], fd: number) => number | null;
|
|
19
|
+
exists: (p: string) => boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare const defaultFlockDeps: FlockDeps;
|
|
22
|
+
/**
|
|
23
|
+
* Take an exclusive non-blocking flock on `lockPath` (created 0600 in a 0700 directory). On success
|
|
24
|
+
* the returned fd must stay open for the life of the lock; closing it (or exiting) releases it.
|
|
25
|
+
*/
|
|
26
|
+
export declare function tryLockFile(lockPath: string, deps?: FlockDeps): LockResult;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// flock(2) LOCK_EX|LOCK_NB for the durable seat proxy's per-conversation lock (S1 proxyLockPath,
|
|
2
|
+
// BACKBONE amendment 51, C13-R4).
|
|
3
|
+
//
|
|
4
|
+
// Node has no flock binding, so the lock is taken by a short-lived helper on an fd this process
|
|
5
|
+
// opened and passes down: flock locks belong to the open file description, so the lock stays held
|
|
6
|
+
// by this process's fd after the helper exits, is visible to `lsof -p <this pid>`, and is released
|
|
7
|
+
// when this process exits for any reason (kill -9 included). The helper is the system perl
|
|
8
|
+
// (Fcntl), present on every macOS; python3 is the fallback.
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
/** The helper commands, tried in order. Each locks fd 3 and exits 0 (taken), 3 (held), else error. */
|
|
13
|
+
export const FLOCK_HELPERS = [
|
|
14
|
+
[
|
|
15
|
+
"/usr/bin/perl",
|
|
16
|
+
"-MFcntl=:flock",
|
|
17
|
+
"-e",
|
|
18
|
+
'open(my $fh, "+<&=", 3) or exit 9; flock($fh, LOCK_EX|LOCK_NB) ? exit 0 : exit 3',
|
|
19
|
+
],
|
|
20
|
+
[
|
|
21
|
+
"/usr/bin/python3",
|
|
22
|
+
"-I",
|
|
23
|
+
"-c",
|
|
24
|
+
"import fcntl,sys\ntry:\n fcntl.flock(3, fcntl.LOCK_EX|fcntl.LOCK_NB)\nexcept BlockingIOError:\n sys.exit(3)\nexcept Exception:\n sys.exit(9)",
|
|
25
|
+
],
|
|
26
|
+
];
|
|
27
|
+
export const defaultFlockDeps = {
|
|
28
|
+
openSync: (p, flags, mode) => fs.openSync(p, flags, mode),
|
|
29
|
+
closeSync: (fd) => fs.closeSync(fd),
|
|
30
|
+
mkdirSync: (p, opts) => fs.mkdirSync(p, opts),
|
|
31
|
+
run: (argv, fd) => spawnSync(argv[0], argv.slice(1), {
|
|
32
|
+
stdio: ["ignore", "ignore", "ignore", fd],
|
|
33
|
+
timeout: 10_000,
|
|
34
|
+
}).status,
|
|
35
|
+
exists: (p) => fs.existsSync(p),
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Take an exclusive non-blocking flock on `lockPath` (created 0600 in a 0700 directory). On success
|
|
39
|
+
* the returned fd must stay open for the life of the lock; closing it (or exiting) releases it.
|
|
40
|
+
*/
|
|
41
|
+
export function tryLockFile(lockPath, deps = defaultFlockDeps) {
|
|
42
|
+
let fd;
|
|
43
|
+
try {
|
|
44
|
+
deps.mkdirSync(path.dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
45
|
+
fd = deps.openSync(lockPath, "a", 0o600);
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
return { ok: false, reason: "error", detail: e.message };
|
|
49
|
+
}
|
|
50
|
+
// The fd is handed back only when the lock is taken; every other way out (held, no helper, a
|
|
51
|
+
// helper or the existence probe throwing) closes it here.
|
|
52
|
+
let taken = false;
|
|
53
|
+
try {
|
|
54
|
+
for (const helper of FLOCK_HELPERS) {
|
|
55
|
+
if (!deps.exists(helper[0]))
|
|
56
|
+
continue;
|
|
57
|
+
const status = deps.run(helper, fd);
|
|
58
|
+
if (status === 0) {
|
|
59
|
+
taken = true;
|
|
60
|
+
return { ok: true, fd };
|
|
61
|
+
}
|
|
62
|
+
if (status === 3)
|
|
63
|
+
return { ok: false, reason: "held", detail: "lock held" };
|
|
64
|
+
}
|
|
65
|
+
return { ok: false, reason: "error", detail: "no flock helper could take the lock" };
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
return { ok: false, reason: "error", detail: e.message };
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
if (!taken) {
|
|
72
|
+
try {
|
|
73
|
+
deps.closeSync(fd);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
/* already closed */
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -83,6 +83,11 @@ export interface LauncherArgvSplit {
|
|
|
83
83
|
launcherArgv: string[];
|
|
84
84
|
passthrough: string[];
|
|
85
85
|
yoloInjected: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* The launcher's interactive Remote Control prefix (`--remote-control <name> [--name <name>]`,
|
|
88
|
+
* agent.ts launch): named as a dropped --remote-control, its session name never a positional.
|
|
89
|
+
*/
|
|
90
|
+
rcInjected: boolean;
|
|
86
91
|
/** The --session-id the launcher pinned, if any. */
|
|
87
92
|
sessionId: string | null;
|
|
88
93
|
/**
|
|
@@ -100,7 +105,8 @@ export interface LauncherArgvSplit {
|
|
|
100
105
|
}
|
|
101
106
|
/**
|
|
102
107
|
* Split an original claude argv (after argv[0]) the way `mnemom agent` built it (buildCliArgs):
|
|
103
|
-
* [--model v] [--effort v] [yolo] [--session-id id]
|
|
108
|
+
* [--remote-control n [--name n]] [--model v] [--effort v] [yolo] [--session-id id]
|
|
109
|
+
* [--plugin-dir dir] <passthrough>
|
|
104
110
|
* [--strict-mcp-config] [--mcp-config <v>]. `passthroughHint` (the tokens after the parent
|
|
105
111
|
* launcher's `--`, read from its own argv) pins the split exactly when it occurs where the
|
|
106
112
|
* layout allows; an empty hint (the parent was read and had no `--`) makes the shape read exact;
|
|
@@ -159,15 +159,21 @@ function isSubsequenceAt(hay, needle, at) {
|
|
|
159
159
|
}
|
|
160
160
|
/**
|
|
161
161
|
* Split an original claude argv (after argv[0]) the way `mnemom agent` built it (buildCliArgs):
|
|
162
|
-
* [--model v] [--effort v] [yolo] [--session-id id]
|
|
162
|
+
* [--remote-control n [--name n]] [--model v] [--effort v] [yolo] [--session-id id]
|
|
163
|
+
* [--plugin-dir dir] <passthrough>
|
|
163
164
|
* [--strict-mcp-config] [--mcp-config <v>]. `passthroughHint` (the tokens after the parent
|
|
164
165
|
* launcher's `--`, read from its own argv) pins the split exactly when it occurs where the
|
|
165
166
|
* layout allows; an empty hint (the parent was read and had no `--`) makes the shape read exact;
|
|
166
167
|
* without a hint (null or absent) the prefix and suffix are read by shape and reported ambiguous.
|
|
167
168
|
*/
|
|
168
169
|
export function splitLauncherArgv(argv, passthroughHint) {
|
|
169
|
-
//
|
|
170
|
+
// The RC launch puts its own `--remote-control <name> --name <name>` first.
|
|
170
171
|
let i = 0;
|
|
172
|
+
const rcName = argv[1];
|
|
173
|
+
const rcInjected = argv[0] === REMOTE_CONTROL_FLAG && rcName !== undefined && readToken(rcName).flag === null;
|
|
174
|
+
if (rcInjected)
|
|
175
|
+
i = argv[2] === "--name" && argv[3] === rcName ? 4 : 2;
|
|
176
|
+
// Prefix: launcher-injected flags in their fixed order.
|
|
171
177
|
const prefix = {};
|
|
172
178
|
for (const [flag, arity] of LAUNCHER_PREFIX) {
|
|
173
179
|
if (argv[i] !== flag)
|
|
@@ -230,6 +236,7 @@ export function splitLauncherArgv(argv, passthroughHint) {
|
|
|
230
236
|
launcherArgv,
|
|
231
237
|
passthrough,
|
|
232
238
|
yoloInjected: prefix[YOLO_FLAG] === true,
|
|
239
|
+
rcInjected,
|
|
233
240
|
sessionId: typeof prefix["--session-id"] === "string" ? prefix["--session-id"] : null,
|
|
234
241
|
exact,
|
|
235
242
|
ambiguous,
|
|
@@ -245,7 +252,7 @@ export function runningSourceInput(originalArgv, cwd, opts = {}) {
|
|
|
245
252
|
userArgv: [...split.ambiguous, ...split.passthrough],
|
|
246
253
|
yoloInjected: split.yoloInjected,
|
|
247
254
|
// Names are deduplicated, so a --remote-control the user also passed is named once.
|
|
248
|
-
remoteControl: opts.remoteControl === true,
|
|
255
|
+
remoteControl: opts.remoteControl === true || split.rcInjected,
|
|
249
256
|
cwd,
|
|
250
257
|
};
|
|
251
258
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type ExperimentalPrefs } from "../lib/experimental.js";
|
|
2
|
+
/** True when argv (after the node + script pair) names the seat proxy. */
|
|
3
|
+
export declare function isSeatProxyArgv(argv: readonly string[]): boolean;
|
|
4
|
+
export interface SeatProxyEntryDeps {
|
|
5
|
+
stored: () => ExperimentalPrefs | undefined;
|
|
6
|
+
stderr: (line: string) => void;
|
|
7
|
+
exit: (code: number) => void;
|
|
8
|
+
run: (argv: readonly string[]) => Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
/** Gate, then run the proxy with the arguments after `seat-proxy`. */
|
|
11
|
+
export declare function runSeatProxyEntry(argv: readonly string[], deps: SeatProxyEntryDeps): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Entry dispatch for the hidden `mnemom agent host seat-proxy` (C3). It bypasses commander (the
|
|
2
|
+
// `agent` launcher's options would reject the proxy's flags, as with `agent slack`), the update
|
|
3
|
+
// hook and the experimental migration, so the proxy's stdout carries only its READY line.
|
|
4
|
+
//
|
|
5
|
+
// Gate (BACKBONE Flags/CLI): the durable experimental feature, read from config.json only, like the
|
|
6
|
+
// host daemon that spawns the proxy (resolveExperimental with an empty env; MNEMOM_EXPERIMENTAL is
|
|
7
|
+
// never read here). Off: exit 2 with the gate message, no HTTP, no file under HOME, no process.
|
|
8
|
+
import { gatedMessage, resolveExperimental } from "../lib/experimental.js";
|
|
9
|
+
/** True when argv (after the node + script pair) names the seat proxy. */
|
|
10
|
+
export function isSeatProxyArgv(argv) {
|
|
11
|
+
return argv[0] === "agent" && argv[1] === "host" && argv[2] === "seat-proxy";
|
|
12
|
+
}
|
|
13
|
+
/** Gate, then run the proxy with the arguments after `seat-proxy`. */
|
|
14
|
+
export function runSeatProxyEntry(argv, deps) {
|
|
15
|
+
const on = resolveExperimental({ stored: deps.stored(), env: {} }).enabled.includes("durable");
|
|
16
|
+
if (!on) {
|
|
17
|
+
deps.stderr(`${gatedMessage("`mnemom agent host`", "durable")}\n`);
|
|
18
|
+
deps.exit(2);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
deps.run(argv.slice(3)).catch(() => {
|
|
22
|
+
deps.stderr("mnemom agent host seat-proxy: internal error\n");
|
|
23
|
+
deps.exit(1);
|
|
24
|
+
});
|
|
25
|
+
}
|