@mnemom/mnemom 0.17.34 → 0.17.36
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 +14 -2
- package/dist/commands/agent-run.js +28 -7
- package/dist/commands/agent-share-render.js +1 -1
- 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/commands/scan.d.ts +13 -3
- package/dist/commands/scan.js +48 -10
- 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 +62 -3
- package/dist/lib/api.d.ts +5 -0
- package/dist/lib/api.js +16 -0
- package/dist/lib/config.d.ts +7 -0
- package/dist/lib/config.js +21 -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/pr-source.js +2 -0
- package/dist/lib/scan/record.d.ts +60 -9
- package/dist/lib/scan/record.js +157 -39
- package/dist/lib/scan/repo-picker.d.ts +32 -0
- package/dist/lib/scan/repo-picker.js +80 -0
- 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/node_modules/@mnemom/aip-verifier/LICENSE +190 -0
- package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.cts +478 -0
- package/node_modules/@mnemom/aip-verifier/dist/claim-lint-CVITIRcU.d.ts +478 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.cjs +971 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.cjs.map +1 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.d.cts +330 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.d.ts +330 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.js +933 -0
- package/node_modules/@mnemom/aip-verifier/dist/index.js.map +1 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs +879 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.cjs.map +1 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.cts +96 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.d.ts +96 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.js +875 -0
- package/node_modules/@mnemom/aip-verifier/dist/render-lines.js.map +1 -0
- package/node_modules/@mnemom/aip-verifier/package.json +86 -0
- package/node_modules/@noble/ed25519/LICENSE +21 -0
- package/node_modules/@noble/ed25519/README.md +371 -0
- package/node_modules/@noble/ed25519/index.d.ts +391 -0
- package/node_modules/@noble/ed25519/index.js +933 -0
- package/node_modules/@noble/ed25519/index.ts +1134 -0
- package/node_modules/@noble/ed25519/package.json +54 -0
- package/node_modules/@noble/hashes/LICENSE +21 -0
- package/node_modules/@noble/hashes/README.md +478 -0
- package/node_modules/@noble/hashes/_blake.d.ts +20 -0
- package/node_modules/@noble/hashes/_blake.js +59 -0
- package/node_modules/@noble/hashes/_md.d.ts +95 -0
- package/node_modules/@noble/hashes/_md.js +209 -0
- package/node_modules/@noble/hashes/_u64.d.ts +36 -0
- package/node_modules/@noble/hashes/_u64.js +77 -0
- package/node_modules/@noble/hashes/argon2.d.ts +148 -0
- package/node_modules/@noble/hashes/argon2.js +577 -0
- package/node_modules/@noble/hashes/blake1.d.ts +162 -0
- package/node_modules/@noble/hashes/blake1.js +630 -0
- package/node_modules/@noble/hashes/blake2.d.ts +203 -0
- package/node_modules/@noble/hashes/blake2.js +573 -0
- package/node_modules/@noble/hashes/blake3.d.ts +68 -0
- package/node_modules/@noble/hashes/blake3.js +310 -0
- package/node_modules/@noble/hashes/eskdf.d.ts +91 -0
- package/node_modules/@noble/hashes/eskdf.js +225 -0
- package/node_modules/@noble/hashes/hkdf.d.ts +70 -0
- package/node_modules/@noble/hashes/hkdf.js +152 -0
- package/node_modules/@noble/hashes/hmac.d.ts +50 -0
- package/node_modules/@noble/hashes/hmac.js +93 -0
- package/node_modules/@noble/hashes/index.d.ts +1 -0
- package/node_modules/@noble/hashes/index.js +34 -0
- package/node_modules/@noble/hashes/legacy.d.ts +107 -0
- package/node_modules/@noble/hashes/legacy.js +348 -0
- package/node_modules/@noble/hashes/package.json +91 -0
- package/node_modules/@noble/hashes/pbkdf2.d.ts +66 -0
- package/node_modules/@noble/hashes/pbkdf2.js +176 -0
- package/node_modules/@noble/hashes/scrypt.d.ts +98 -0
- package/node_modules/@noble/hashes/scrypt.js +304 -0
- package/node_modules/@noble/hashes/sha2.d.ts +176 -0
- package/node_modules/@noble/hashes/sha2.js +446 -0
- package/node_modules/@noble/hashes/sha3-addons.d.ts +533 -0
- package/node_modules/@noble/hashes/sha3-addons.js +973 -0
- package/node_modules/@noble/hashes/sha3.d.ts +208 -0
- package/node_modules/@noble/hashes/sha3.js +475 -0
- package/node_modules/@noble/hashes/src/_blake.ts +67 -0
- package/node_modules/@noble/hashes/src/_md.ts +227 -0
- package/node_modules/@noble/hashes/src/_u64.ts +111 -0
- package/node_modules/@noble/hashes/src/argon2.ts +693 -0
- package/node_modules/@noble/hashes/src/blake1.ts +677 -0
- package/node_modules/@noble/hashes/src/blake2.ts +648 -0
- package/node_modules/@noble/hashes/src/blake3.ts +344 -0
- package/node_modules/@noble/hashes/src/eskdf.ts +265 -0
- package/node_modules/@noble/hashes/src/hkdf.ts +161 -0
- package/node_modules/@noble/hashes/src/hmac.ts +130 -0
- package/node_modules/@noble/hashes/src/index.ts +33 -0
- package/node_modules/@noble/hashes/src/legacy.ts +361 -0
- package/node_modules/@noble/hashes/src/pbkdf2.ts +219 -0
- package/node_modules/@noble/hashes/src/scrypt.ts +375 -0
- package/node_modules/@noble/hashes/src/sha2.ts +486 -0
- package/node_modules/@noble/hashes/src/sha3-addons.ts +1157 -0
- package/node_modules/@noble/hashes/src/sha3.ts +525 -0
- package/node_modules/@noble/hashes/src/utils.ts +978 -0
- package/node_modules/@noble/hashes/src/webcrypto.ts +283 -0
- package/node_modules/@noble/hashes/utils.d.ts +560 -0
- package/node_modules/@noble/hashes/utils.js +687 -0
- package/node_modules/@noble/hashes/webcrypto.d.ts +121 -0
- package/node_modules/@noble/hashes/webcrypto.js +228 -0
- package/package.json +5 -2
- package/plugins/integrity-manifest.json +6 -2
- package/plugins/mnemom/hooks/hooks.json +12 -0
- package/plugins/mnemom/hooks/seat-ask.mjs +221 -0
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
|
}
|
package/dist/host/daemon.js
CHANGED
|
@@ -16,10 +16,11 @@
|
|
|
16
16
|
import { randomUUID } from "node:crypto";
|
|
17
17
|
import { EXECUTOR_POLL_S, HEARTBEAT_DIVISOR, RESPAWN_CONCURRENCY, CLAIM_BACKOFF_BASE_S, CLAIM_BACKOFF_MAX_S, CLAIM_BACKOFF_JITTER, LEASE_SECONDS_DEFAULT, } from "../vendor/shared/durable-contract/constants.js";
|
|
18
18
|
import { TERMINAL_STATES } from "../vendor/shared/durable-contract/states.js";
|
|
19
|
+
import { questionAskMatches, questionAskResponseOk, } from "../vendor/shared/durable-contract/question.js";
|
|
19
20
|
import { IDLE_LOG } from "./gate.js";
|
|
20
21
|
import { classifyClaim, errorCode, } from "./client.js";
|
|
21
22
|
import { seatTmuxSession } from "./tmux.js";
|
|
22
|
-
import { CODE, SEAT_UNKNOWN, SPAWNED_KIND, spawnIdempotencyKey } from "./s1-names.js";
|
|
23
|
+
import { CODE, SEAT_BAD_ANSWER, SEAT_UNKNOWN, SPAWNED_KIND, spawnIdempotencyKey, } from "./s1-names.js";
|
|
23
24
|
export const TICK_MS = 1000;
|
|
24
25
|
export const ADOPT_RETRY_MS = 2000;
|
|
25
26
|
export { SPAWNED_KIND, spawnIdempotencyKey } from "./s1-names.js";
|
|
@@ -179,6 +180,19 @@ export class HostDaemon {
|
|
|
179
180
|
return did;
|
|
180
181
|
}
|
|
181
182
|
await this.checkSeats(did);
|
|
183
|
+
if (this.d.inbox) {
|
|
184
|
+
try {
|
|
185
|
+
did.push(...(await this.d.inbox([...this.seats.values()].map((x) => ({
|
|
186
|
+
sessionId: x.sessionId,
|
|
187
|
+
tmuxSession: x.tmuxSession,
|
|
188
|
+
epoch: x.epoch,
|
|
189
|
+
ready: x.helloAccepted,
|
|
190
|
+
})))));
|
|
191
|
+
}
|
|
192
|
+
catch (e) {
|
|
193
|
+
this.d.log(`inbox delivery: ${e.message}`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
182
196
|
await this.heartbeat(did);
|
|
183
197
|
if (polled && this.items.size > 0)
|
|
184
198
|
await this.claimWave(did);
|
|
@@ -587,6 +601,65 @@ export class HostDaemon {
|
|
|
587
601
|
}
|
|
588
602
|
return { status: r.status, body: r.body };
|
|
589
603
|
}
|
|
604
|
+
/**
|
|
605
|
+
* Relay a seat's question op (S1 amendment 61). Only for a registered, hello-accepted seat this
|
|
606
|
+
* daemon holds, with its own spawn id and epoch; the daemon adds the live lease's epoch and nonce
|
|
607
|
+
* (the executor key never leaves the daemon). The api checks the payload's closed shape.
|
|
608
|
+
*/
|
|
609
|
+
async relayQuestion(op, sessionId, spawnId, epoch, payload) {
|
|
610
|
+
const s = this.seats.get(sessionId);
|
|
611
|
+
if (!s || s.spawnId !== spawnId || !s.registered || !s.helloAccepted || s.epoch !== epoch)
|
|
612
|
+
return {
|
|
613
|
+
status: 404,
|
|
614
|
+
body: {
|
|
615
|
+
error: { code: SEAT_UNKNOWN, message: "no live seat with that spawn id and epoch" },
|
|
616
|
+
},
|
|
617
|
+
};
|
|
618
|
+
const fence = { epoch: s.epoch, nonce: this.d.nonce };
|
|
619
|
+
if (op === "question_ask") {
|
|
620
|
+
const ask = {
|
|
621
|
+
session_id: sessionId,
|
|
622
|
+
tool_use_id: String(payload.tool_use_id),
|
|
623
|
+
questions: payload.questions,
|
|
624
|
+
};
|
|
625
|
+
const r = await this.d.api.questionAsk(sessionId, {
|
|
626
|
+
...fence,
|
|
627
|
+
tool_use_id: ask.tool_use_id,
|
|
628
|
+
questions: ask.questions,
|
|
629
|
+
});
|
|
630
|
+
if (r.status !== 200)
|
|
631
|
+
return { status: r.status, body: r.body };
|
|
632
|
+
if (!questionAskResponseOk(r.body))
|
|
633
|
+
return {
|
|
634
|
+
status: 502,
|
|
635
|
+
body: { error: { code: SEAT_BAD_ANSWER, message: "not a question answer" } },
|
|
636
|
+
};
|
|
637
|
+
// The seat waits only on its own question (S1 question.ts questionAskMatches; the hook cannot
|
|
638
|
+
// import the contract, so the check is here): an answer about another is that question still
|
|
639
|
+
// being open, so the hook denies as for durable_question_busy and polls or releases nothing.
|
|
640
|
+
if (!(await questionAskMatches(r.body, ask)))
|
|
641
|
+
return {
|
|
642
|
+
status: 409,
|
|
643
|
+
body: {
|
|
644
|
+
error: {
|
|
645
|
+
code: CODE.durable_question_busy,
|
|
646
|
+
message: "another question of this session is still open",
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
};
|
|
650
|
+
return { status: r.status, body: r.body };
|
|
651
|
+
}
|
|
652
|
+
const r = op === "question_poll"
|
|
653
|
+
? await this.d.api.questionPoll(sessionId, {
|
|
654
|
+
...fence,
|
|
655
|
+
approval_id: String(payload.approval_id),
|
|
656
|
+
})
|
|
657
|
+
: await this.d.api.questionRelease(sessionId, {
|
|
658
|
+
...fence,
|
|
659
|
+
approval_id: String(payload.approval_id),
|
|
660
|
+
});
|
|
661
|
+
return { status: r.status, body: r.body };
|
|
662
|
+
}
|
|
590
663
|
// ---- status ------------------------------------------------------------------------------------
|
|
591
664
|
publish(state) {
|
|
592
665
|
this.d.publish({
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The homed inbox delivery (S1 amendment 60, signed off by Wassim 2026-11-02): the host daemon gives each
|
|
3
|
+
* seat it hosts the messages in its session's durable inbox, one at a time, as the seat's next
|
|
4
|
+
* prompt, and acks each once the turn answering it has ended. It replaces the Slack wake's pane
|
|
5
|
+
* typing (draft PR #3145's host/seat-wake.ts): Slack, the CLI and allowlisted agents all send
|
|
6
|
+
* through ROUTES.own_inbox_send, and the api, not a local file, holds what is waiting.
|
|
7
|
+
*
|
|
8
|
+
* Per seat, each pass:
|
|
9
|
+
* 1. Nothing in flight: when the seat is idle (its proxy said hello, the transcript's last turn
|
|
10
|
+
* finished, claude's input line is drawn and no turn is running) on two consecutive passes,
|
|
11
|
+
* read exec_inbox (content-free) and, when anything waits, exec_inbox_take one message. The api
|
|
12
|
+
* marks it delivered at the seat's epoch and journals inbox.delivered BEFORE the paste, so its
|
|
13
|
+
* sha is in the resolver's inbox_open by the time the turn reaches the gateway.
|
|
14
|
+
* 2. Paste the api's text (bracketed paste, then Enter), with every control character but TAB
|
|
15
|
+
* and LF removed first (`pasteSafe`), so no byte of a body can end the bracketed paste. Its
|
|
16
|
+
* first line is `[mnemom inbox <sha>] from ...`; the seat proxy stamps `inbox=<sha>` on that
|
|
17
|
+
* transcript entry (durable/turn-chain.ts), which A60 admits unattended while the sha is open.
|
|
18
|
+
* 3. Confirm the prompt landed: a transcript entry carrying the marker within CONFIRM_MS, else
|
|
19
|
+
* paste again while the seat is idle, waiting twice as long after each paste (at most
|
|
20
|
+
* PASTE_TRIES = INBOX_DELIVERY_LIMIT pastes). After that it is released (unconfirmed), so the
|
|
21
|
+
* api offers it again or, at its delivery limit, marks it dead. A paste whose Enter failed is
|
|
22
|
+
* submitted with Enter alone, never pasted again; a seat that is never idle again releases an
|
|
23
|
+
* unconfirmed message after INBOX_STALL_MS.
|
|
24
|
+
* 4. Once the turn after the marker has ended (idle again), exec_inbox_ack. A 409
|
|
25
|
+
* durable_inbox_not_open (already acked, or the epoch moved) also settles it.
|
|
26
|
+
*
|
|
27
|
+
* What is in flight is kept per session in `stateDir` (inbox id, sha, epoch, text), so a daemon
|
|
28
|
+
* restart neither pastes a message twice nor loses its ack. A record from an older epoch is dropped:
|
|
29
|
+
* the api re-offers that message to the new seat. When the dropped record's prompt had already
|
|
30
|
+
* reached the transcript, its sha is kept as handled first, and the re-offer is acked at the new
|
|
31
|
+
* epoch instead of pasted, so the conversation never sees one message twice.
|
|
32
|
+
*
|
|
33
|
+
* A take whose answer is lost or malformed may still have marked messages delivered at this epoch.
|
|
34
|
+
* Nothing is dropped on it: any message the answer does name is released (undeliverable), and the
|
|
35
|
+
* empty-take latch below lapses after INBOX_REOFFER_S, when the api re-offers the rest.
|
|
36
|
+
*
|
|
37
|
+
* Pure over injected deps; host/run.ts wires the executor API, tmux, the transcript and the log.
|
|
38
|
+
*/
|
|
39
|
+
import { type InboxReleaseReason } from "../vendor/shared/durable-contract/inbox.js";
|
|
40
|
+
import type { HttpResult } from "./client.js";
|
|
41
|
+
/** How long a first paste has to show up in the transcript before it is pasted again (doubling). */
|
|
42
|
+
export declare const CONFIRM_MS = 15000;
|
|
43
|
+
/** The longest wait between two pastes of one message. */
|
|
44
|
+
export declare const CONFIRM_MAX_MS = 300000;
|
|
45
|
+
/** Pastes of one taken message at most (the api's own take limit, S1 INBOX_DELIVERY_LIMIT). */
|
|
46
|
+
export declare const PASTE_TRIES = 5;
|
|
47
|
+
/** How long paste number `tries` (1-based) has to show up before the next one. */
|
|
48
|
+
export declare const confirmWindow: (tries: number) => number;
|
|
49
|
+
/**
|
|
50
|
+
* The delivery text as it may be pasted into a terminal: CR LF and CR become LF, and every other
|
|
51
|
+
* control character but TAB and LF (C0 including ESC, DEL, C1) is removed. ESC[201~ inside a
|
|
52
|
+
* bracketed paste would end it and the bytes after it would reach claude as typed keys (a CR
|
|
53
|
+
* submits, `!` runs a shell command, Shift+Tab changes the permission mode). S1 inboxBodyOk refuses
|
|
54
|
+
* these at send; this holds whatever the api admitted.
|
|
55
|
+
*/
|
|
56
|
+
export declare function pasteSafe(text: string): string;
|
|
57
|
+
/** At most one exec_inbox read per seat in this period while idle. */
|
|
58
|
+
export declare const INBOX_POLL_MS = 5000;
|
|
59
|
+
/**
|
|
60
|
+
* An unconfirmed paste on a seat that never shows idle again (a hung turn) is released after this
|
|
61
|
+
* long, so the api re-offers it and a later pass takes it again, instead of parking it forever.
|
|
62
|
+
*/
|
|
63
|
+
export declare const INBOX_STALL_MS: number;
|
|
64
|
+
/** The handled shas kept per session (a re-offer comes within one epoch change of its paste). */
|
|
65
|
+
export declare const HANDLED_MAX = 32;
|
|
66
|
+
export interface InboxSeat {
|
|
67
|
+
readonly sessionId: string;
|
|
68
|
+
readonly tmuxSession: string;
|
|
69
|
+
readonly epoch: number;
|
|
70
|
+
/** The seat's proxy said hello: claude is up and owns the conversation. */
|
|
71
|
+
readonly ready: boolean;
|
|
72
|
+
}
|
|
73
|
+
export interface InFlight {
|
|
74
|
+
readonly inboxId: string;
|
|
75
|
+
readonly sha: string;
|
|
76
|
+
readonly epoch: number;
|
|
77
|
+
readonly text: string;
|
|
78
|
+
pastedAt: number;
|
|
79
|
+
tries: number;
|
|
80
|
+
confirmed: boolean;
|
|
81
|
+
/**
|
|
82
|
+
* The last paste was followed by its Enter. False between a paste and its Enter: if the Enter
|
|
83
|
+
* fails the text is still in claude's input line, so the next pass presses Enter again, never
|
|
84
|
+
* pastes it a second time.
|
|
85
|
+
*/
|
|
86
|
+
submitted: boolean;
|
|
87
|
+
}
|
|
88
|
+
export interface InboxExecApi {
|
|
89
|
+
inboxSummary(sessionId: string): Promise<HttpResult>;
|
|
90
|
+
inboxTake(sessionId: string, body: {
|
|
91
|
+
epoch: number;
|
|
92
|
+
nonce: string;
|
|
93
|
+
limit: number;
|
|
94
|
+
}): Promise<HttpResult>;
|
|
95
|
+
inboxAck(sessionId: string, body: {
|
|
96
|
+
epoch: number;
|
|
97
|
+
nonce: string;
|
|
98
|
+
inbox_id: string;
|
|
99
|
+
sha: string;
|
|
100
|
+
}): Promise<HttpResult>;
|
|
101
|
+
inboxRelease(sessionId: string, body: {
|
|
102
|
+
epoch: number;
|
|
103
|
+
nonce: string;
|
|
104
|
+
inbox_id: string;
|
|
105
|
+
sha: string;
|
|
106
|
+
reason: InboxReleaseReason;
|
|
107
|
+
}): Promise<HttpResult>;
|
|
108
|
+
}
|
|
109
|
+
export interface InboxDeliveryDeps {
|
|
110
|
+
api: InboxExecApi;
|
|
111
|
+
nonce: () => string;
|
|
112
|
+
/** The session's transcript text (its tail is enough), or null when not on disk. */
|
|
113
|
+
transcript: (sessionId: string) => string | null;
|
|
114
|
+
capture: (tmuxSession: string) => Promise<string | null>;
|
|
115
|
+
/** Paste `text` into the pane's input line as one prompt (not submitted). */
|
|
116
|
+
paste: (tmuxSession: string, text: string) => Promise<void>;
|
|
117
|
+
/** Press Enter in the pane: submits what was pasted. */
|
|
118
|
+
submit: (tmuxSession: string) => Promise<void>;
|
|
119
|
+
load: (sessionId: string) => InFlight | null;
|
|
120
|
+
save: (sessionId: string, f: InFlight | null) => void;
|
|
121
|
+
/** The shas whose prompt reached the session's transcript but were never acked, oldest first. */
|
|
122
|
+
loadHandled: (sessionId: string) => string[];
|
|
123
|
+
saveHandled: (sessionId: string, shas: readonly string[]) => void;
|
|
124
|
+
now: () => number;
|
|
125
|
+
log: (line: string) => void;
|
|
126
|
+
}
|
|
127
|
+
interface Rec {
|
|
128
|
+
readonly type?: unknown;
|
|
129
|
+
readonly isMeta?: unknown;
|
|
130
|
+
readonly message?: {
|
|
131
|
+
readonly role?: unknown;
|
|
132
|
+
readonly content?: unknown;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** The marker sha a user record's prompt carries, or null. */
|
|
136
|
+
export declare function recordInboxSha(r: Rec): string | null;
|
|
137
|
+
/**
|
|
138
|
+
* What a transcript says about a delivery: whether its last turn finished (the last conversation
|
|
139
|
+
* record is the assistant's own text with no tool call; an empty transcript is a fresh session,
|
|
140
|
+
* which is idle too), whether a prompt carrying `sha` is in it, and whether the turn that prompt
|
|
141
|
+
* started has ended.
|
|
142
|
+
*/
|
|
143
|
+
export declare function transcriptView(transcript: string, sha: string | null): {
|
|
144
|
+
finished: boolean;
|
|
145
|
+
seen: boolean;
|
|
146
|
+
answered: boolean;
|
|
147
|
+
};
|
|
148
|
+
/** claude's input line is drawn and no turn is running. */
|
|
149
|
+
export declare function paneIdle(screen: string | null): boolean;
|
|
150
|
+
/**
|
|
151
|
+
* A stored in-flight record, or null when any field is missing or out of range (a record from
|
|
152
|
+
* another daemon version, say): it is then treated as absent, so it can never re-paste unbounded.
|
|
153
|
+
* A record written before `submitted` existed was submitted (its paste and Enter were one step).
|
|
154
|
+
*/
|
|
155
|
+
export declare function inFlightOf(v: unknown): InFlight | null;
|
|
156
|
+
export declare function fileInFlight(dir: string): Pick<InboxDeliveryDeps, "load" | "save" | "loadHandled" | "saveHandled">;
|
|
157
|
+
export declare class InboxDelivery {
|
|
158
|
+
private readonly d;
|
|
159
|
+
private readonly state;
|
|
160
|
+
constructor(d: InboxDeliveryDeps);
|
|
161
|
+
/** One pass over the daemon's seats. Returns what it did (for logs and tests). */
|
|
162
|
+
sync(seats: readonly InboxSeat[]): Promise<string[]>;
|
|
163
|
+
private seat;
|
|
164
|
+
/** Save `f` as not yet submitted, paste it, press Enter, then save it as submitted. */
|
|
165
|
+
private put;
|
|
166
|
+
private markHandled;
|
|
167
|
+
/**
|
|
168
|
+
* A re-offer of a message whose prompt is already in the conversation: ack it at this epoch
|
|
169
|
+
* without pasting. A failed ack keeps the sha handled; the api re-offers it again later.
|
|
170
|
+
*/
|
|
171
|
+
private ackHandled;
|
|
172
|
+
private release;
|
|
173
|
+
}
|
|
174
|
+
export {};
|