@mnemom/mnemom 0.17.34-next.4 → 0.17.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent-config.js +3 -0
- package/dist/commands/agent-goal.d.ts +8 -3
- package/dist/commands/agent-goal.js +22 -8
- package/dist/commands/agent-run.d.ts +10 -2
- package/dist/commands/agent-run.js +19 -6
- package/dist/commands/agent-slack.js +32 -0
- package/dist/commands/durable-inbox.d.ts +51 -0
- package/dist/commands/durable-inbox.js +134 -0
- package/dist/commands/durable-questions.d.ts +64 -0
- package/dist/commands/durable-questions.js +161 -0
- package/dist/durable/seat-proxy.d.ts +12 -1
- package/dist/durable/seat-proxy.js +55 -9
- package/dist/durable/seat-socket.d.ts +20 -2
- package/dist/durable/seat-socket.js +58 -0
- package/dist/durable/turn-chain.d.ts +8 -0
- package/dist/durable/turn-chain.js +25 -4
- package/dist/host/client.d.ts +41 -0
- package/dist/host/client.js +7 -0
- package/dist/host/control.d.ts +7 -0
- package/dist/host/control.js +15 -0
- package/dist/host/daemon.d.ts +20 -0
- package/dist/host/daemon.js +74 -1
- package/dist/host/inbox-delivery.d.ts +174 -0
- package/dist/host/inbox-delivery.js +453 -0
- package/dist/host/run.d.ts +18 -0
- package/dist/host/run.js +61 -0
- package/dist/host/s1-names.d.ts +2 -0
- package/dist/host/s1-names.js +2 -0
- package/dist/host/tmux.d.ts +7 -0
- package/dist/host/tmux.js +19 -0
- package/dist/index.js +59 -1
- package/dist/lib/api.d.ts +5 -0
- package/dist/lib/api.js +16 -0
- package/dist/lib/durable/api.d.ts +11 -0
- package/dist/lib/durable/api.js +28 -3
- package/dist/lib/durable/gate.d.ts +1 -1
- package/dist/lib/durable/gate.js +8 -1
- package/dist/lib/durable/live.d.ts +5 -0
- package/dist/lib/durable/live.js +19 -0
- package/dist/lib/experimental.js +3 -0
- package/dist/lib/goal-api.d.ts +11 -4
- package/dist/lib/goal-api.js +4 -3
- package/dist/lib/launcher-config.d.ts +5 -0
- package/dist/lib/launcher-config.js +17 -2
- package/dist/lib/prompt.d.ts +4 -2
- package/dist/lib/prompt.js +15 -5
- package/dist/lib/runner/job.d.ts +2 -2
- package/dist/lib/runner/job.js +16 -3
- package/dist/lib/runner/pr-body.d.ts +17 -0
- package/dist/lib/runner/pr-body.js +42 -0
- package/dist/lib/runner/proxy.d.ts +18 -3
- package/dist/lib/runner/proxy.js +24 -5
- package/dist/lib/scan/record.d.ts +60 -9
- package/dist/lib/scan/record.js +157 -39
- package/dist/lib/scan/report.d.ts +6 -8
- package/dist/lib/scan/report.js +21 -7
- package/dist/lib/scan/share.d.ts +5 -1
- package/dist/lib/scan/share.js +15 -5
- package/dist/lib/slack-durable.d.ts +123 -0
- package/dist/lib/slack-durable.js +237 -0
- package/dist/lib/slack-router.d.ts +7 -0
- package/dist/lib/slack-router.js +50 -4
- package/dist/vendor/durable-contract/index.js +992 -34
- package/dist/vendor/shared/durable-contract/admission.d.ts +81 -2
- package/dist/vendor/shared/durable-contract/admission.js +319 -2
- package/dist/vendor/shared/durable-contract/cli-surface.js +21 -0
- package/dist/vendor/shared/durable-contract/codes.d.ts +60 -0
- package/dist/vendor/shared/durable-contract/codes.js +22 -0
- package/dist/vendor/shared/durable-contract/events.d.ts +4 -2
- package/dist/vendor/shared/durable-contract/events.js +55 -1
- package/dist/vendor/shared/durable-contract/freeze-record.d.ts +14 -0
- package/dist/vendor/shared/durable-contract/inbox.d.ts +299 -0
- package/dist/vendor/shared/durable-contract/inbox.js +462 -0
- package/dist/vendor/shared/durable-contract/index.d.ts +2 -0
- package/dist/vendor/shared/durable-contract/index.js +2 -0
- package/dist/vendor/shared/durable-contract/question.d.ts +218 -0
- package/dist/vendor/shared/durable-contract/question.js +373 -0
- package/dist/vendor/shared/durable-contract/routes.d.ts +44 -2
- package/dist/vendor/shared/durable-contract/routes.js +43 -1
- package/dist/vendor/shared/durable-contract/turn.d.ts +15 -1
- package/dist/vendor/shared/durable-contract/turn.js +33 -1
- package/dist/vendor/shared/durable-contract/vectors.d.ts +45 -0
- package/dist/vendor/shared/durable-contract/vectors.js +49 -0
- package/package.json +1 -1
- package/plugins/integrity-manifest.json +6 -2
- package/plugins/mnemom/hooks/hooks.json +12 -0
- package/plugins/mnemom/hooks/seat-ask.mjs +221 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable question verbs (S1 amendment 61, signed off by Wassim 2026-11-02; ROUTES.own_questions and
|
|
3
|
+
* ROUTES.own_answer):
|
|
4
|
+
*
|
|
5
|
+
* mnemom agent sessions questions <name> [--all] [--json]
|
|
6
|
+
* mnemom agent sessions answer <name> <qid|latest> [choice...] [--dismiss] [--json]
|
|
7
|
+
*
|
|
8
|
+
* A locked seat that calls AskUserQuestion records the question at the api (question.asked) instead
|
|
9
|
+
* of drawing a dialog nobody sees. `questions` lists them (the open ones by default), with the option
|
|
10
|
+
* the seat recommends marked; `answer` answers one (question.answered, via coding_agent) or dismisses
|
|
11
|
+
* it. Each choice is a 1-based option number (comma separated for a multi-select question), an
|
|
12
|
+
* option label, or free text; with several questions, one choice per question in order. The seat
|
|
13
|
+
* gets the answer either live (its hook is still waiting) or as its next inbox message.
|
|
14
|
+
*
|
|
15
|
+
* Every dependency is injected (`QuestionVerbDeps`), so each branch is unit-tested.
|
|
16
|
+
*/
|
|
17
|
+
import { answerFromChoice, QUESTION_ID_RE, questionAnswerResponseOk, questionListResponseOk, } from "../vendor/shared/durable-contract/question.js";
|
|
18
|
+
import { DurableRefusal } from "../lib/durable/refusal.js";
|
|
19
|
+
import { handleVerbError, refusalFromAnswer, resolveTarget, } from "./durable-verbs.js";
|
|
20
|
+
const usage = (message) => new DurableRefusal({ code: "invalid_argument", message, fix: null, exit: 2 });
|
|
21
|
+
async function listQuestions(deps, name) {
|
|
22
|
+
const row = await resolveTarget(deps.api, name);
|
|
23
|
+
const a = await deps.api.questions("own_questions", row.conversation_id, row.agent_id);
|
|
24
|
+
if (a.status !== 200 || !questionListResponseOk(a.body))
|
|
25
|
+
throw refusalFromAnswer(a, name, deps.launcherKey(), row);
|
|
26
|
+
return {
|
|
27
|
+
rows: a.body.questions,
|
|
28
|
+
cid: row.conversation_id,
|
|
29
|
+
agentId: row.agent_id,
|
|
30
|
+
raw: a.body,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether the owner can still answer `q`. The api reports a question the seat released as
|
|
35
|
+
* `handed_off` whether or not it was answered since (S1 question.ts: answered and handed_off can
|
|
36
|
+
* both hold), so a handed-off question is still waiting for the owner while it has no answered_at
|
|
37
|
+
* and has not expired. Its answer then reaches the seat as an inbox message (S1 §61 (c)/(e)).
|
|
38
|
+
*/
|
|
39
|
+
export function awaitingAnswer(q, now) {
|
|
40
|
+
if (q.state === "open")
|
|
41
|
+
return true;
|
|
42
|
+
return q.state === "handed_off" && q.answered_at === null && Date.parse(q.expires_at) > now;
|
|
43
|
+
}
|
|
44
|
+
/** The state the owner reads: a handed-off question still waiting for them says so. */
|
|
45
|
+
function stateLabel(q, now) {
|
|
46
|
+
if (q.state !== "handed_off")
|
|
47
|
+
return q.state;
|
|
48
|
+
if (q.answered_at !== null)
|
|
49
|
+
return q.answers !== null ? "answered (handed off)" : "dismissed (handed off)";
|
|
50
|
+
return awaitingAnswer(q, now) ? "open (handed off: the answer goes to its inbox)" : "expired";
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Text the seat's model wrote, made safe for the owner's terminal: every C0/C1 control character
|
|
54
|
+
* (ESC, CSI, OSC, BEL, CR, ...) and DEL is shown escaped (`\x1b`), so a question cannot move the
|
|
55
|
+
* cursor, rewrite or hide a line, or write the clipboard. S1 question.ts only refuses NUL.
|
|
56
|
+
*/
|
|
57
|
+
export function terminalSafe(s) {
|
|
58
|
+
// eslint-disable-next-line no-control-regex
|
|
59
|
+
return s.replace(/[\u0000-\u001f\u007f-\u009f]/g, (c) => {
|
|
60
|
+
const h = c.charCodeAt(0).toString(16).padStart(2, "0");
|
|
61
|
+
return `\\x${h}`;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
/** The human lines for one question row. */
|
|
65
|
+
export function renderQuestion(q, now = Date.now()) {
|
|
66
|
+
const t = terminalSafe;
|
|
67
|
+
const lines = [`question ${q.approval_id} ${stateLabel(q, now)} asked ${t(q.created_at)}`];
|
|
68
|
+
q.questions.forEach((item, i) => {
|
|
69
|
+
lines.push(` ${item.header ? `[${t(item.header)}] ` : ""}${t(item.question)}${item.multiSelect ? " (choose one or more)" : ""}`);
|
|
70
|
+
item.options.forEach((o, j) => {
|
|
71
|
+
const rec = q.recommended[i] === j ? " <- recommended" : "";
|
|
72
|
+
lines.push(` ${j + 1}. ${t(o.label)}${o.description ? ` - ${t(o.description)}` : ""}${rec}`);
|
|
73
|
+
});
|
|
74
|
+
const a = q.answers?.[item.question];
|
|
75
|
+
if (a !== undefined)
|
|
76
|
+
lines.push(` answer: ${t(a)}`);
|
|
77
|
+
});
|
|
78
|
+
if (q.answered_via)
|
|
79
|
+
lines.push(` answered via ${q.answered_via} at ${q.answered_at ?? "?"}`);
|
|
80
|
+
return lines;
|
|
81
|
+
}
|
|
82
|
+
export async function questionsCommand(name, opts, deps) {
|
|
83
|
+
const json = opts.json === true;
|
|
84
|
+
try {
|
|
85
|
+
const { rows, raw } = await listQuestions(deps, name);
|
|
86
|
+
const now = deps.now();
|
|
87
|
+
const shown = opts.all === true ? rows : rows.filter((r) => awaitingAnswer(r, now));
|
|
88
|
+
if (json) {
|
|
89
|
+
deps.out(JSON.stringify(opts.all === true ? raw : { questions: shown }));
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
if (shown.length === 0) {
|
|
93
|
+
deps.out(`${name}: no ${opts.all === true ? "" : "open "}questions`);
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
for (const q of shown)
|
|
97
|
+
for (const l of renderQuestion(q, now))
|
|
98
|
+
deps.out(l);
|
|
99
|
+
deps.out(`answer with: mnemom agent sessions answer ${name} <question id|latest> <choice>...`);
|
|
100
|
+
return 0;
|
|
101
|
+
}
|
|
102
|
+
catch (e) {
|
|
103
|
+
return handleVerbError(e, json, deps);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** The answers map for one question row from the owner's choices (one per question, in order). */
|
|
107
|
+
export function answersFromChoices(q, choices) {
|
|
108
|
+
if (choices.length !== q.questions.length)
|
|
109
|
+
throw usage(`question ${q.approval_id} has ${q.questions.length} question(s): give one choice for each, in order`);
|
|
110
|
+
const out = {};
|
|
111
|
+
q.questions.forEach((item, i) => {
|
|
112
|
+
const c = choices[i] ?? "";
|
|
113
|
+
if (c.trim() === "")
|
|
114
|
+
throw usage("an empty choice is not an answer");
|
|
115
|
+
out[item.question] = answerFromChoice(item, c);
|
|
116
|
+
});
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
export async function answerCommand(name, qid, choices, opts, deps) {
|
|
120
|
+
const json = opts.json === true;
|
|
121
|
+
const say = json ? deps.err : deps.out;
|
|
122
|
+
try {
|
|
123
|
+
const dismiss = opts.dismiss === true;
|
|
124
|
+
if (dismiss && choices.length > 0)
|
|
125
|
+
throw usage("--dismiss takes no choices");
|
|
126
|
+
if (!dismiss && choices.length === 0)
|
|
127
|
+
throw usage(`mnemom agent sessions answer ${name} <question id|latest> <choice>... (or --dismiss)`);
|
|
128
|
+
if (qid !== "latest" && !QUESTION_ID_RE.test(qid))
|
|
129
|
+
throw usage(`'${qid}' is not a question id (see mnemom agent sessions questions ${name})`);
|
|
130
|
+
const { rows, cid, agentId } = await listQuestions(deps, name);
|
|
131
|
+
const now = deps.now();
|
|
132
|
+
const open = rows.filter((r) => awaitingAnswer(r, now));
|
|
133
|
+
const q = qid === "latest" ? open[0] : rows.find((r) => r.approval_id === qid);
|
|
134
|
+
if (!q)
|
|
135
|
+
throw usage(qid === "latest" ? `${name} has no open question` : `${name} has no question ${qid}`);
|
|
136
|
+
if (!awaitingAnswer(q, now))
|
|
137
|
+
throw usage(`question ${q.approval_id} is already ${stateLabel(q, now)}`);
|
|
138
|
+
const body = dismiss
|
|
139
|
+
? { approval_id: q.approval_id, via: "coding_agent", dismiss: true }
|
|
140
|
+
: {
|
|
141
|
+
approval_id: q.approval_id,
|
|
142
|
+
via: "coding_agent",
|
|
143
|
+
answers: answersFromChoices(q, choices),
|
|
144
|
+
};
|
|
145
|
+
const a = await deps.api.questions("own_answer", cid, agentId, body);
|
|
146
|
+
if (a.status !== 200 || !questionAnswerResponseOk(a.body))
|
|
147
|
+
throw refusalFromAnswer(a, name, deps.launcherKey());
|
|
148
|
+
const r = a.body;
|
|
149
|
+
say(r.outcome === "dismissed"
|
|
150
|
+
? `${name}: question ${q.approval_id} dismissed`
|
|
151
|
+
: r.delivery === "live"
|
|
152
|
+
? `${name}: answered; the waiting seat takes it now`
|
|
153
|
+
: `${name}: answered; it reaches the session as its next message (inbox ${r.inbox_id})`);
|
|
154
|
+
if (json)
|
|
155
|
+
deps.out(JSON.stringify(a.body));
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
catch (e) {
|
|
159
|
+
return handleVerbError(e, json, deps);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -51,7 +51,10 @@ export declare function armVerdict(pathname: string, rawHeaders: readonly string
|
|
|
51
51
|
forward: false;
|
|
52
52
|
reason: DropReason;
|
|
53
53
|
};
|
|
54
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* The dead-key signal in a door response (C13-R7), read from the JSON body's error.code: the
|
|
56
|
+
* gateway's durable refusals are durableErrorBody, `{error:{code, message}}`, with no error.type.
|
|
57
|
+
*/
|
|
55
58
|
export declare function deadKeyReason(status: number, body: string): ExitReason | null;
|
|
56
59
|
/** Whether a request path is origin-form and free of dot segments (raw or percent-encoded). */
|
|
57
60
|
export declare function isCleanOriginForm(url: string): boolean;
|
|
@@ -71,6 +74,14 @@ export interface SeatProxyDeps {
|
|
|
71
74
|
export interface SeatProxyState {
|
|
72
75
|
dead: ExitReason | null;
|
|
73
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The dead-candidate body as text, decoded by its content-encoding (gzip, deflate, br; identity or
|
|
79
|
+
* absent passes through; a comma list is undone last-applied first). The decoded size is capped too
|
|
80
|
+
* (zlib maxOutputLength), so a decompression bomb cannot blow up memory. Returns null when the body
|
|
81
|
+
* cannot be decoded: corrupt bytes, an unknown encoding, or over the cap once decoded. For the dead-key
|
|
82
|
+
* check only; the forwarded response keeps the original bytes.
|
|
83
|
+
*/
|
|
84
|
+
export declare function decodeDeadBody(buf: Buffer, contentEncoding: string | undefined, cap: number): string | null;
|
|
74
85
|
/** The request handler of a seat proxy for `cfg` with lease key `key`. */
|
|
75
86
|
export declare function createSeatProxyHandler(cfg: SeatProxyArgs, key: string, deps: SeatProxyDeps, state?: SeatProxyState): (req: http.IncomingMessage, res: http.ServerResponse) => void;
|
|
76
87
|
export interface SeatProxyMainDeps {
|
|
@@ -20,6 +20,7 @@ import fs from "node:fs";
|
|
|
20
20
|
import http from "node:http";
|
|
21
21
|
import https from "node:https";
|
|
22
22
|
import os from "node:os";
|
|
23
|
+
import zlib from "node:zlib";
|
|
23
24
|
import { randomBytes } from "node:crypto";
|
|
24
25
|
import { TurnOrigins, claudeConfigDir, TURN_HEADER } from "../turn-origin.mjs";
|
|
25
26
|
import { httpDoorAllowed } from "./test-mode.mjs";
|
|
@@ -190,26 +191,29 @@ export function armVerdict(pathname, rawHeaders) {
|
|
|
190
191
|
reason: a.hold !== "on" ? "unattended_hold_off" : "unattended_fence_off",
|
|
191
192
|
};
|
|
192
193
|
}
|
|
193
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* The dead-key signal in a door response (C13-R7), read from the JSON body's error.code: the
|
|
196
|
+
* gateway's durable refusals are durableErrorBody, `{error:{code, message}}`, with no error.type.
|
|
197
|
+
*/
|
|
194
198
|
export function deadKeyReason(status, body) {
|
|
195
199
|
if (status !== 401 && status !== 409 && status !== 410)
|
|
196
200
|
return null;
|
|
197
|
-
let
|
|
201
|
+
let code;
|
|
198
202
|
try {
|
|
199
203
|
const j = JSON.parse(body);
|
|
200
|
-
|
|
204
|
+
code =
|
|
201
205
|
j && typeof j === "object" && j.error && typeof j.error === "object"
|
|
202
|
-
? j.error.
|
|
206
|
+
? j.error.code
|
|
203
207
|
: undefined;
|
|
204
208
|
}
|
|
205
209
|
catch {
|
|
206
210
|
return null;
|
|
207
211
|
}
|
|
208
|
-
if (status === 409 &&
|
|
212
|
+
if (status === 409 && code === "durable_lease_fenced")
|
|
209
213
|
return "fenced_exit";
|
|
210
|
-
if (status === 401 &&
|
|
214
|
+
if (status === 401 && code === "durable_key_revoked")
|
|
211
215
|
return "revoked_exit";
|
|
212
|
-
if (status === 410 && (
|
|
216
|
+
if (status === 410 && (code === "durable_session_ended" || code === "durable_unlocked"))
|
|
213
217
|
return "ended_exit";
|
|
214
218
|
return null;
|
|
215
219
|
}
|
|
@@ -252,6 +256,45 @@ function sendLocal(res, status, type, message) {
|
|
|
252
256
|
res.writeHead(status, { "content-type": "application/json" });
|
|
253
257
|
res.end(JSON.stringify({ type: "error", error: { type, message } }));
|
|
254
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* The dead-candidate body as text, decoded by its content-encoding (gzip, deflate, br; identity or
|
|
261
|
+
* absent passes through; a comma list is undone last-applied first). The decoded size is capped too
|
|
262
|
+
* (zlib maxOutputLength), so a decompression bomb cannot blow up memory. Returns null when the body
|
|
263
|
+
* cannot be decoded: corrupt bytes, an unknown encoding, or over the cap once decoded. For the dead-key
|
|
264
|
+
* check only; the forwarded response keeps the original bytes.
|
|
265
|
+
*/
|
|
266
|
+
export function decodeDeadBody(buf, contentEncoding, cap) {
|
|
267
|
+
const encs = (contentEncoding ?? "")
|
|
268
|
+
.split(",")
|
|
269
|
+
.map((e) => e.trim().toLowerCase())
|
|
270
|
+
.filter((e) => e !== "" && e !== "identity");
|
|
271
|
+
const opts = { maxOutputLength: Math.max(cap, 1) };
|
|
272
|
+
try {
|
|
273
|
+
let out = buf;
|
|
274
|
+
for (const enc of encs.reverse()) {
|
|
275
|
+
if (enc === "gzip" || enc === "x-gzip")
|
|
276
|
+
out = zlib.gunzipSync(out, opts);
|
|
277
|
+
else if (enc === "br")
|
|
278
|
+
out = zlib.brotliDecompressSync(out, opts);
|
|
279
|
+
else if (enc === "deflate") {
|
|
280
|
+
try {
|
|
281
|
+
out = zlib.inflateSync(out, opts);
|
|
282
|
+
}
|
|
283
|
+
catch (e) {
|
|
284
|
+
if (e instanceof RangeError)
|
|
285
|
+
throw e;
|
|
286
|
+
out = zlib.inflateRawSync(out, opts); // some servers send raw deflate
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
else
|
|
290
|
+
return null;
|
|
291
|
+
}
|
|
292
|
+
return out.toString("utf8");
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
255
298
|
/** Thrown by readAll when a body passes its cap. */
|
|
256
299
|
class OverCap extends Error {
|
|
257
300
|
}
|
|
@@ -393,15 +436,18 @@ export function createSeatProxyHandler(cfg, key, deps, state = { dead: null }) {
|
|
|
393
436
|
const deadCandidate = status === 401 || status === 409 || status === 410;
|
|
394
437
|
if (deadCandidate) {
|
|
395
438
|
void readAll(ur, deadBodyCap).then((buf) => {
|
|
396
|
-
const
|
|
439
|
+
const text = decodeDeadBody(buf, ur.headers["content-encoding"], deadBodyCap);
|
|
440
|
+
const dead = text === null ? null : deadKeyReason(status, text);
|
|
397
441
|
if (dead)
|
|
398
442
|
state.dead = dead;
|
|
399
443
|
if (verdict.forward) {
|
|
444
|
+
if (text === null)
|
|
445
|
+
deps.log(`${pathname} ${status} undecodable_body`);
|
|
400
446
|
res.writeHead(status, responseHeaders(ur.rawHeaders));
|
|
401
447
|
res.end(buf);
|
|
402
448
|
}
|
|
403
449
|
else {
|
|
404
|
-
deps.log(`${pathname} ${status} drop ${verdict.reason}`);
|
|
450
|
+
deps.log(`${pathname} ${status} drop ${text === null ? "undecodable_body" : verdict.reason}`);
|
|
405
451
|
sendDrop(res, reqId());
|
|
406
452
|
}
|
|
407
453
|
if (dead) {
|
|
@@ -3,6 +3,8 @@ import net from "node:net";
|
|
|
3
3
|
export declare const SEAT_SOCKET_REFUSAL = "{\"ok\":false,\"error\":\"seat_op_refused\"}";
|
|
4
4
|
/** Our own hello cannot reach the daemon (no relay configured, or the relay failed). */
|
|
5
5
|
export declare const SEAT_SOCKET_UNAVAILABLE = "{\"ok\":false,\"error\":\"seat_hello_unavailable\"}";
|
|
6
|
+
/** A question op that cannot reach the daemon (no relay configured, or the relay failed). */
|
|
7
|
+
export declare const SEAT_SOCKET_QUESTION_UNAVAILABLE = "{\"ok\":false,\"error\":\"seat_question_unavailable\"}";
|
|
6
8
|
/** A frame (the bytes before the newline) longer than this many bytes is refused without parsing. */
|
|
7
9
|
export declare const SEAT_SOCKET_MAX_FRAME: number;
|
|
8
10
|
/** macOS sun_path is 104 bytes including the NUL. */
|
|
@@ -13,12 +15,28 @@ export interface SeatIdentity {
|
|
|
13
15
|
epoch: number;
|
|
14
16
|
spawnId: string;
|
|
15
17
|
}
|
|
16
|
-
|
|
18
|
+
/** S1 amendment 61: the seat's question ops (seat-ask.mjs). */
|
|
19
|
+
export declare const SEAT_QUESTION_OPS: readonly ["question_ask", "question_poll", "question_release"];
|
|
20
|
+
export type SeatQuestionOp = (typeof SEAT_QUESTION_OPS)[number];
|
|
21
|
+
export type SeatRelayFrame = {
|
|
17
22
|
op: "hello";
|
|
18
23
|
session_id: string;
|
|
19
24
|
epoch: number;
|
|
20
25
|
spawn_id: string;
|
|
21
|
-
}
|
|
26
|
+
} | {
|
|
27
|
+
op: SeatQuestionOp;
|
|
28
|
+
session_id: string;
|
|
29
|
+
epoch: number;
|
|
30
|
+
spawn_id: string;
|
|
31
|
+
[k: string]: unknown;
|
|
32
|
+
};
|
|
33
|
+
/** One frame to the daemon's control socket, its one-line answer back. */
|
|
34
|
+
export type HelloRelay = (frame: SeatRelayFrame) => Promise<string>;
|
|
35
|
+
/**
|
|
36
|
+
* The relay frame for the seat's own question op, or null (refused). Session, spawn id and epoch
|
|
37
|
+
* must all be the seat's own (epoch required); the payload must be the op's exact shape.
|
|
38
|
+
*/
|
|
39
|
+
export declare function ownQuestionFrame(frame: unknown, seat: SeatIdentity | null): SeatRelayFrame | null;
|
|
22
40
|
/** Whether a parsed frame is the seat's own hello. Everything else is refused. */
|
|
23
41
|
export declare function isOwnHello(frame: unknown, seat: SeatIdentity | null): boolean;
|
|
24
42
|
/** The reply line (without newline) for one request line. Never throws. */
|
|
@@ -10,19 +10,63 @@
|
|
|
10
10
|
// the hook unchanged; a repeat hello is relayed like the first. Every other op, and any op naming
|
|
11
11
|
// another session id or spawn id, gets the one fixed refusal line below, with no relay and no API
|
|
12
12
|
// call. The refusal echoes nothing from the request.
|
|
13
|
+
//
|
|
14
|
+
// S1 amendment 61 (signed off by Wassim 2026-11-02) adds three question ops, relayed the same way and
|
|
15
|
+
// only for the seat's own session, spawn id AND epoch (required here, not optional as for hello):
|
|
16
|
+
// {op:"question_ask", tool_use_id, questions} the plugin's AskUserQuestion PreToolUse hook
|
|
17
|
+
// {op:"question_poll", approval_id} while that hook waits for the owner
|
|
18
|
+
// {op:"question_release", approval_id} the hook stopped waiting; the answer goes to the inbox
|
|
19
|
+
// The payload is checked against S1 question.ts's shapes before any relay; the daemon adds the
|
|
20
|
+
// epoch and nonce of the live lease and calls the executor question routes.
|
|
13
21
|
import fs from "node:fs";
|
|
14
22
|
import net from "node:net";
|
|
15
23
|
import os from "node:os";
|
|
16
24
|
import path from "node:path";
|
|
17
25
|
import { createHash } from "node:crypto";
|
|
26
|
+
import { normalizeQuestions, QUESTION_TOOL_USE_ID_RE } from "../vendor/durable-contract/index.js";
|
|
18
27
|
/** The one fixed refusal line (bytes before the newline). A local literal: S1 freezes no seat-socket refusal line yet (gap c3-seat-proxy-enums-not-in-s1). */
|
|
19
28
|
export const SEAT_SOCKET_REFUSAL = '{"ok":false,"error":"seat_op_refused"}';
|
|
20
29
|
/** Our own hello cannot reach the daemon (no relay configured, or the relay failed). */
|
|
21
30
|
export const SEAT_SOCKET_UNAVAILABLE = '{"ok":false,"error":"seat_hello_unavailable"}';
|
|
31
|
+
/** A question op that cannot reach the daemon (no relay configured, or the relay failed). */
|
|
32
|
+
export const SEAT_SOCKET_QUESTION_UNAVAILABLE = '{"ok":false,"error":"seat_question_unavailable"}';
|
|
22
33
|
/** A frame (the bytes before the newline) longer than this many bytes is refused without parsing. */
|
|
23
34
|
export const SEAT_SOCKET_MAX_FRAME = 64 * 1024;
|
|
24
35
|
/** macOS sun_path is 104 bytes including the NUL. */
|
|
25
36
|
export const SOCKET_PATH_MAX = 103;
|
|
37
|
+
/** S1 amendment 61: the seat's question ops (seat-ask.mjs). */
|
|
38
|
+
export const SEAT_QUESTION_OPS = ["question_ask", "question_poll", "question_release"];
|
|
39
|
+
const UUID_LC = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
40
|
+
/**
|
|
41
|
+
* The relay frame for the seat's own question op, or null (refused). Session, spawn id and epoch
|
|
42
|
+
* must all be the seat's own (epoch required); the payload must be the op's exact shape.
|
|
43
|
+
*/
|
|
44
|
+
export function ownQuestionFrame(frame, seat) {
|
|
45
|
+
if (!seat || !frame || typeof frame !== "object" || Array.isArray(frame))
|
|
46
|
+
return null;
|
|
47
|
+
const f = frame;
|
|
48
|
+
if (!SEAT_QUESTION_OPS.includes(f.op))
|
|
49
|
+
return null;
|
|
50
|
+
if (f.session_id !== seat.sessionId || f.spawn_id !== seat.spawnId || f.epoch !== seat.epoch)
|
|
51
|
+
return null;
|
|
52
|
+
const base = { session_id: seat.sessionId, epoch: seat.epoch, spawn_id: seat.spawnId };
|
|
53
|
+
const keys = Object.keys(f).sort().join(",");
|
|
54
|
+
if (f.op === "question_ask") {
|
|
55
|
+
if (keys !== "epoch,op,questions,session_id,spawn_id,tool_use_id")
|
|
56
|
+
return null;
|
|
57
|
+
if (typeof f.tool_use_id !== "string" || !QUESTION_TOOL_USE_ID_RE.test(f.tool_use_id))
|
|
58
|
+
return null;
|
|
59
|
+
const questions = normalizeQuestions(f.questions);
|
|
60
|
+
if (!questions)
|
|
61
|
+
return null;
|
|
62
|
+
return { op: "question_ask", ...base, tool_use_id: f.tool_use_id, questions };
|
|
63
|
+
}
|
|
64
|
+
if (keys !== "approval_id,epoch,op,session_id,spawn_id")
|
|
65
|
+
return null;
|
|
66
|
+
if (typeof f.approval_id !== "string" || !UUID_LC.test(f.approval_id))
|
|
67
|
+
return null;
|
|
68
|
+
return { op: f.op, ...base, approval_id: f.approval_id };
|
|
69
|
+
}
|
|
26
70
|
/** Whether a parsed frame is the seat's own hello. Everything else is refused. */
|
|
27
71
|
export function isOwnHello(frame, seat) {
|
|
28
72
|
if (!seat || !frame || typeof frame !== "object" || Array.isArray(frame))
|
|
@@ -48,6 +92,20 @@ export async function answerFrame(line, seat, relay) {
|
|
|
48
92
|
catch {
|
|
49
93
|
return { reply: SEAT_SOCKET_REFUSAL, outcome: "refused" };
|
|
50
94
|
}
|
|
95
|
+
const question = ownQuestionFrame(frame, seat);
|
|
96
|
+
if (question) {
|
|
97
|
+
if (!relay)
|
|
98
|
+
return { reply: SEAT_SOCKET_QUESTION_UNAVAILABLE, outcome: "unavailable" };
|
|
99
|
+
try {
|
|
100
|
+
const first = String(await relay(question)).split("\n")[0];
|
|
101
|
+
return first
|
|
102
|
+
? { reply: first, outcome: "relayed" }
|
|
103
|
+
: { reply: SEAT_SOCKET_QUESTION_UNAVAILABLE, outcome: "unavailable" };
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { reply: SEAT_SOCKET_QUESTION_UNAVAILABLE, outcome: "unavailable" };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
51
109
|
if (!isOwnHello(frame, seat) || !seat)
|
|
52
110
|
return { reply: SEAT_SOCKET_REFUSAL, outcome: "refused" };
|
|
53
111
|
if (!relay)
|
|
@@ -36,6 +36,14 @@ export declare function statedEntry(e: TranscriptEntry): TranscriptEntry;
|
|
|
36
36
|
* origin=task entries only (S1 amendment 56); undefined otherwise or when not a TURN_PRODUCER_RE token.
|
|
37
37
|
*/
|
|
38
38
|
export declare function entryProducer(e: TranscriptEntry): string | undefined;
|
|
39
|
+
/**
|
|
40
|
+
* The inbox sha an entry answers (S1 amendment 60): the `[mnemom inbox <sha>]` marker on the first
|
|
41
|
+
* line of the entry's first text (after Claude Code's paste open tag, as the host daemon delivers by
|
|
42
|
+
* bracketed paste), else undefined. Only a prompt entry stated as human can carry one: a task or
|
|
43
|
+
* system entry that quotes a marker never does. Restrict-only: the stamp only makes a turn
|
|
44
|
+
* unattended, and the gateway admits it only while the sha is open (delivered, not acked).
|
|
45
|
+
*/
|
|
46
|
+
export declare function entryInboxSha(e: TranscriptEntry): string | undefined;
|
|
39
47
|
/** The header value: the leading chain element, then the stated entries up to and including the root. */
|
|
40
48
|
export declare function durableHeaderValue(entries: readonly TranscriptEntry[], chain: string): string;
|
|
41
49
|
/** Entries up to and including the last one whose promptId is `promptId`; none when it is absent. */
|
|
@@ -24,9 +24,10 @@
|
|
|
24
24
|
// origin.producer, e.g. session-task), here only. turn-origin.mjs is main's, so the plain proxies'
|
|
25
25
|
// header bytes never carry it.
|
|
26
26
|
import { createHash } from "node:crypto";
|
|
27
|
-
import { buildTurnHeader, entryOrigin, entrySource, TURN_PARTS_MAX, } from "../turn-origin.mjs";
|
|
27
|
+
import { buildTurnHeader, entryOrigin, entrySource, entryTexts, TURN_PARTS_MAX, } from "../turn-origin.mjs";
|
|
28
28
|
import { TURN_WAIT_MS, isCountTokensPath, } from "../vendor/durable-contract/index.js";
|
|
29
29
|
import { TURN_PRODUCER_RE } from "../vendor/shared/durable-contract/turn.js";
|
|
30
|
+
import { inboxMarkerOf } from "../vendor/shared/durable-contract/inbox.js";
|
|
30
31
|
const ID_RE = /^[A-Za-z0-9-]{8,64}$/;
|
|
31
32
|
const POLL_MS = 20;
|
|
32
33
|
const one = (v) => String((Array.isArray(v) ? v[0] : v) ?? "").trim();
|
|
@@ -84,9 +85,26 @@ export function entryProducer(e) {
|
|
|
84
85
|
const p = e.origin?.producer;
|
|
85
86
|
return typeof p === "string" && TURN_PRODUCER_RE.test(p) ? p : undefined;
|
|
86
87
|
}
|
|
88
|
+
/** Claude Code's open tag for a bracketed paste, which may lead a pasted prompt's text. */
|
|
89
|
+
const PASTE_OPEN_RE = /^<pasted_content id="[^"\n]{0,128}">\n?/;
|
|
90
|
+
/**
|
|
91
|
+
* The inbox sha an entry answers (S1 amendment 60): the `[mnemom inbox <sha>]` marker on the first
|
|
92
|
+
* line of the entry's first text (after Claude Code's paste open tag, as the host daemon delivers by
|
|
93
|
+
* bracketed paste), else undefined. Only a prompt entry stated as human can carry one: a task or
|
|
94
|
+
* system entry that quotes a marker never does. Restrict-only: the stamp only makes a turn
|
|
95
|
+
* unattended, and the gateway admits it only while the sha is open (delivered, not acked).
|
|
96
|
+
*/
|
|
97
|
+
export function entryInboxSha(e) {
|
|
98
|
+
if (entryOrigin(e) !== "human" || entrySource(e) === "system")
|
|
99
|
+
return undefined;
|
|
100
|
+
const first = entryTexts(e)[0];
|
|
101
|
+
if (typeof first !== "string")
|
|
102
|
+
return undefined;
|
|
103
|
+
return inboxMarkerOf(first.replace(PASTE_OPEN_RE, "")) ?? undefined;
|
|
104
|
+
}
|
|
87
105
|
/**
|
|
88
106
|
* main's buildTurnHeader, entry by entry, with `producer=<p>; ` before `pastes=` on an entry that
|
|
89
|
-
* states one; the same newest TURN_PARTS_MAX parts.
|
|
107
|
+
* states one and `; inbox=<sha>` after `h=` on an inbox delivery; the same newest TURN_PARTS_MAX parts.
|
|
90
108
|
*/
|
|
91
109
|
function statedParts(stated) {
|
|
92
110
|
const parts = [];
|
|
@@ -95,8 +113,11 @@ function statedParts(stated) {
|
|
|
95
113
|
if (!own)
|
|
96
114
|
continue;
|
|
97
115
|
const producer = entryProducer(e);
|
|
98
|
-
|
|
99
|
-
|
|
116
|
+
const inbox = entryInboxSha(e);
|
|
117
|
+
for (const part of own.split(", ")) {
|
|
118
|
+
const p = producer ? part.replace("; pastes=", `; producer=${producer}; pastes=`) : part;
|
|
119
|
+
parts.push(inbox ? `${p}; inbox=${inbox}` : p);
|
|
120
|
+
}
|
|
100
121
|
}
|
|
101
122
|
return parts.slice(-TURN_PARTS_MAX).join(", ");
|
|
102
123
|
}
|
package/dist/host/client.d.ts
CHANGED
|
@@ -89,6 +89,47 @@ export interface ExecutorApi {
|
|
|
89
89
|
nonce: string;
|
|
90
90
|
entries: readonly HeartbeatEntry[];
|
|
91
91
|
}): Promise<HttpResult>;
|
|
92
|
+
/** S1 amendment 60: the content-free inbox summary of a session this executor holds. */
|
|
93
|
+
inboxSummary(sessionId: string): Promise<HttpResult>;
|
|
94
|
+
/** Marks up to `limit` messages delivered at the live epoch and answers their delivery text. */
|
|
95
|
+
inboxTake(sessionId: string, body: {
|
|
96
|
+
epoch: number;
|
|
97
|
+
nonce: string;
|
|
98
|
+
limit: number;
|
|
99
|
+
}): Promise<HttpResult>;
|
|
100
|
+
inboxAck(sessionId: string, body: {
|
|
101
|
+
epoch: number;
|
|
102
|
+
nonce: string;
|
|
103
|
+
inbox_id: string;
|
|
104
|
+
sha: string;
|
|
105
|
+
}): Promise<HttpResult>;
|
|
106
|
+
/** Hands a delivered message back undone (admitted again, or dead at its delivery limit). */
|
|
107
|
+
inboxRelease(sessionId: string, body: {
|
|
108
|
+
epoch: number;
|
|
109
|
+
nonce: string;
|
|
110
|
+
inbox_id: string;
|
|
111
|
+
sha: string;
|
|
112
|
+
reason: "unconfirmed" | "refused" | "undeliverable";
|
|
113
|
+
}): Promise<HttpResult>;
|
|
114
|
+
/** S1 amendment 61: opens (or returns) the session's question approval; journals question.asked. */
|
|
115
|
+
questionAsk(sessionId: string, body: {
|
|
116
|
+
epoch: number;
|
|
117
|
+
nonce: string;
|
|
118
|
+
tool_use_id: string;
|
|
119
|
+
questions: unknown;
|
|
120
|
+
}): Promise<HttpResult>;
|
|
121
|
+
/** The question's state; an answer is handed over once (the api marks it taken). */
|
|
122
|
+
questionPoll(sessionId: string, body: {
|
|
123
|
+
epoch: number;
|
|
124
|
+
nonce: string;
|
|
125
|
+
approval_id: string;
|
|
126
|
+
}): Promise<HttpResult>;
|
|
127
|
+
/** The seat stopped waiting: the answer, now or later, goes to the inbox. */
|
|
128
|
+
questionRelease(sessionId: string, body: {
|
|
129
|
+
epoch: number;
|
|
130
|
+
nonce: string;
|
|
131
|
+
approval_id: string;
|
|
132
|
+
}): Promise<HttpResult>;
|
|
92
133
|
}
|
|
93
134
|
export declare function executorApi(opts: {
|
|
94
135
|
apiUrl: string;
|
package/dist/host/client.js
CHANGED
|
@@ -131,6 +131,13 @@ export function executorApi(opts) {
|
|
|
131
131
|
seatExit: (id, body) => call("exec_seat_exit", id, "POST", body),
|
|
132
132
|
fail: (id, body) => call("exec_fail", id, "POST", body),
|
|
133
133
|
heartbeat: (body) => call("exec_heartbeat", undefined, "POST", body),
|
|
134
|
+
inboxSummary: (id) => call("exec_inbox", id, "GET"),
|
|
135
|
+
inboxTake: (id, body) => call("exec_inbox_take", id, "POST", body),
|
|
136
|
+
inboxAck: (id, body) => call("exec_inbox_ack", id, "POST", body),
|
|
137
|
+
inboxRelease: (id, body) => call("exec_inbox_release", id, "POST", body),
|
|
138
|
+
questionAsk: (id, body) => call("exec_question", id, "POST", body),
|
|
139
|
+
questionPoll: (id, body) => call("exec_question_poll", id, "POST", body),
|
|
140
|
+
questionRelease: (id, body) => call("exec_question_release", id, "POST", body),
|
|
134
141
|
};
|
|
135
142
|
}
|
|
136
143
|
/** Map a claim response to what the daemon does with it (C12-R14, R31). */
|
package/dist/host/control.d.ts
CHANGED
|
@@ -16,7 +16,14 @@ export interface ControlHandlers {
|
|
|
16
16
|
status: number;
|
|
17
17
|
body: unknown;
|
|
18
18
|
}>;
|
|
19
|
+
/** S1 amendment 61; absent: the question ops are unknown_op. */
|
|
20
|
+
relayQuestion?(op: ControlQuestionOp, sessionId: string, spawnId: string, epoch: number, payload: Record<string, unknown>): Promise<{
|
|
21
|
+
status: number;
|
|
22
|
+
body: unknown;
|
|
23
|
+
}>;
|
|
19
24
|
}
|
|
25
|
+
export declare const CONTROL_QUESTION_OPS: readonly ["question_ask", "question_poll", "question_release"];
|
|
26
|
+
export type ControlQuestionOp = (typeof CONTROL_QUESTION_OPS)[number];
|
|
20
27
|
export declare function handleControl(h: ControlHandlers, line: string): Promise<Record<string, unknown>>;
|
|
21
28
|
export declare function startControlServer(socket: string, h: ControlHandlers, log: (s: string) => void): net.Server;
|
|
22
29
|
/** dev and inode of the socket file at `socket`, or null when there is none. */
|
package/dist/host/control.js
CHANGED
|
@@ -5,6 +5,10 @@
|
|
|
5
5
|
* {op:"seat_key", session_id, spawn_id} -> {ok:true, lease_key} once per spawn, then {ok:false}
|
|
6
6
|
* {op:"hello", session_id, spawn_id} -> {ok, status, body}: the daemon POSTs the executor hello
|
|
7
7
|
* (the executor key never leaves the daemon, C12-R27)
|
|
8
|
+
* {op:"question_ask"|"question_poll"|"question_release", session_id, spawn_id, epoch, ...}
|
|
9
|
+
* -> {ok, status, body}: S1 amendment 61; the daemon adds the
|
|
10
|
+
* live lease's nonce and calls exec_question[_poll|_release]
|
|
11
|
+
* for a registered seat of that spawn id AND epoch
|
|
8
12
|
* Anything else is refused locally.
|
|
9
13
|
*/
|
|
10
14
|
import * as fs from "node:fs";
|
|
@@ -47,6 +51,7 @@ export function ensureSocketDir(dir, uid = os.userInfo().uid) {
|
|
|
47
51
|
checkOwnSocketDir(dir, uid);
|
|
48
52
|
}
|
|
49
53
|
export const controlSocketPath = (dir) => path.join(dir, "daemon.sock");
|
|
54
|
+
export const CONTROL_QUESTION_OPS = ["question_ask", "question_poll", "question_release"];
|
|
50
55
|
const ID = /^[0-9a-f-]{36}$/i;
|
|
51
56
|
export async function handleControl(h, line) {
|
|
52
57
|
let req;
|
|
@@ -70,6 +75,16 @@ export async function handleControl(h, line) {
|
|
|
70
75
|
const r = await h.relayHello(sid, spawn);
|
|
71
76
|
return { ok: r.status === 200, status: r.status, body: r.body };
|
|
72
77
|
}
|
|
78
|
+
if (h.relayQuestion && CONTROL_QUESTION_OPS.includes(req.op)) {
|
|
79
|
+
const op = req.op;
|
|
80
|
+
if (!Number.isSafeInteger(req.epoch) || req.epoch < 0)
|
|
81
|
+
return { ok: false, error: "bad_request" };
|
|
82
|
+
const payload = op === "question_ask"
|
|
83
|
+
? { tool_use_id: req.tool_use_id, questions: req.questions }
|
|
84
|
+
: { approval_id: req.approval_id };
|
|
85
|
+
const r = await h.relayQuestion(op, sid, spawn, req.epoch, payload);
|
|
86
|
+
return { ok: r.status === 200, status: r.status, body: r.body };
|
|
87
|
+
}
|
|
73
88
|
return { ok: false, error: "unknown_op" };
|
|
74
89
|
}
|
|
75
90
|
export function startControlServer(socket, h, log) {
|
package/dist/host/daemon.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ClaimUnavailableCause, CwdCheckCause } from "../vendor/shared/durable-contract/codes.js";
|
|
2
2
|
import { type ExecutorApi, type SessionItem } from "./client.js";
|
|
3
3
|
import { type SeatPane } from "./tmux.js";
|
|
4
|
+
import type { ControlQuestionOp } from "./control.js";
|
|
4
5
|
export declare const TICK_MS = 1000;
|
|
5
6
|
export declare const ADOPT_RETRY_MS = 2000;
|
|
6
7
|
export { SPAWNED_KIND, spawnIdempotencyKey } from "./s1-names.js";
|
|
@@ -99,6 +100,16 @@ export interface DaemonDeps {
|
|
|
99
100
|
epoch: number;
|
|
100
101
|
spawnId: string;
|
|
101
102
|
}, turnId: string | null) => Promise<unknown>;
|
|
103
|
+
/**
|
|
104
|
+
* S1 amendment 60: deliver each seat's durable inbox into it (host/inbox-delivery.ts), once per
|
|
105
|
+
* pass after the seats are checked. Absent: no inbox delivery.
|
|
106
|
+
*/
|
|
107
|
+
readonly inbox?: (seats: readonly {
|
|
108
|
+
sessionId: string;
|
|
109
|
+
tmuxSession: string;
|
|
110
|
+
epoch: number;
|
|
111
|
+
ready: boolean;
|
|
112
|
+
}[]) => Promise<readonly string[]>;
|
|
102
113
|
}
|
|
103
114
|
export interface UnavailableInfo {
|
|
104
115
|
cause: ClaimUnavailableCause | null;
|
|
@@ -217,5 +228,14 @@ export declare class HostDaemon {
|
|
|
217
228
|
status: number;
|
|
218
229
|
body: unknown;
|
|
219
230
|
}>;
|
|
231
|
+
/**
|
|
232
|
+
* Relay a seat's question op (S1 amendment 61). Only for a registered, hello-accepted seat this
|
|
233
|
+
* daemon holds, with its own spawn id and epoch; the daemon adds the live lease's epoch and nonce
|
|
234
|
+
* (the executor key never leaves the daemon). The api checks the payload's closed shape.
|
|
235
|
+
*/
|
|
236
|
+
relayQuestion(op: ControlQuestionOp, sessionId: string, spawnId: string, epoch: number, payload: Record<string, unknown>): Promise<{
|
|
237
|
+
status: number;
|
|
238
|
+
body: unknown;
|
|
239
|
+
}>;
|
|
220
240
|
private publish;
|
|
221
241
|
}
|