omo-ai 5.1.11 → 5.1.13

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.
@@ -0,0 +1,257 @@
1
+ import { userInfo } from "node:os"
2
+ import { join } from "node:path"
3
+ import { pathToFileURL } from "node:url"
4
+
5
+ import { integerOption, parseArgs, THREAD_EXIT } from "./thread-args.js"
6
+ import { humanLines } from "./thread-output.js"
7
+
8
+ export { THREAD_EXIT } from "./thread-args.js"
9
+
10
+ /**
11
+ * `omo thread` - the session gateway for scripts and connectors: every thread operation the agent
12
+ * tools offer, run through the plugin's thread SDK (`plugin/runtime/thread-sdk/sdk.js`) as
13
+ * `cli:<uid>`. It never starts a host: sessions are listed from what the engine enumerates, and
14
+ * bindings, the outbox and receipts live in the gateway store. JSON shapes: docs/reference/omo-thread.md.
15
+ */
16
+
17
+ const USAGE = [
18
+ "usage: omo thread <list|send|read|bind|unbind|rebind|bindings|report|answer|outbox|ack> [options] [--json]",
19
+ "",
20
+ " list [--all-scope]",
21
+ " send <target> <text> [--mode auto|steer|follow_up] [--expected-turn <n>] [--idempotency-key <k>]",
22
+ " send --binding <id> [<target>] <text> [--idempotency-key <event-id>] [--mode auto|follow_up]",
23
+ " [--author-id <platform-user-id> --author-name <display> [--author-user-id <id>]]",
24
+ " read <target> [--limit <items>] [--max-bytes <n>] [--cursor <c>]",
25
+ " bind <session> --platform <p> --account <id> --chat <id> [--thread <id>] [--root-message <id>]",
26
+ " [--progress-message <id>] [--direction in|out|both] [--inbound-mode auto|follow_up]",
27
+ " [--events <kind,...>] [--policy <id>] [--ttl <seconds>|none] [--idempotency-key <k>]",
28
+ " unbind <binding-id> --revision <n> [--idempotency-key <k>]",
29
+ " rebind <binding-id> <session> --revision <n> [--idempotency-key <k>]",
30
+ " bindings [--session <s>] [--platform <p>] [--account <id>] [--chat <id>] [--thread <id>] [--status <s>] [--cursor <c>] [--limit <n>]",
31
+ " report <session> <milestone|report|question|completion> <text> [--binding <id>] [--request-id <id>] [--request-kind question|select|confirm|input|editor] [--idempotency-key <k>]",
32
+ " answer --binding <answering-binding-id> --token <reply-token> <text>",
33
+ " [--author-id <platform-user-id> --author-name <display> [--author-user-id <id>]]",
34
+ " outbox <binding-id> [--after <cursor>] [--limit <n>] [--ack]",
35
+ " ack <binding-id> <cursor> [--provider-message-id <id>]",
36
+ "",
37
+ " --all-scope resolve sessions in every workspace, not only this directory's",
38
+ " --json print the result as one JSON value",
39
+ ].join("\n")
40
+
41
+ const COMMON = ["--json", "--all-scope"]
42
+ const AUTHOR_FLAGS = ["--author-id", "--author-name", "--author-user-id"]
43
+
44
+ const COMMANDS = {
45
+ list: { values: [], booleans: [], arity: [0, 0], call: (sdk, _p, _o, scope) => sdk.list(scope) },
46
+ send: {
47
+ values: ["--mode", "--expected-turn", "--binding", "--idempotency-key", ...AUTHOR_FLAGS],
48
+ booleans: [],
49
+ arity: [1, 2],
50
+ call: (sdk, p, o, scope) => sdk.send({ ...scope, ...sendTarget(p, o), mode: o["--mode"], expected_turn_id: integerOption(o, "--expected-turn"), binding_id: o["--binding"], idempotency_key: o["--idempotency-key"], author: authorInput(o) }),
51
+ },
52
+ read: {
53
+ values: ["--limit", "--max-bytes", "--cursor"],
54
+ booleans: [],
55
+ arity: [1, 1],
56
+ call: async (sdk, p, o, scope) => lastItems(await sdk.read({ ...scope, thread: p[0], max_bytes: integerOption(o, "--max-bytes"), cursor: o["--cursor"] }), integerOption(o, "--limit")),
57
+ },
58
+ bind: {
59
+ values: ["--platform", "--account", "--chat", "--thread", "--root-message", "--progress-message", "--direction", "--inbound-mode", "--events", "--policy", "--ttl", "--idempotency-key"],
60
+ booleans: [],
61
+ arity: [1, 1],
62
+ call: (sdk, p, o, scope) => sdk.bind({ ...scope, session: p[0], idempotency_key: o["--idempotency-key"], binding: bindingInput(o) }),
63
+ },
64
+ unbind: { values: ["--revision", "--idempotency-key"], booleans: [], arity: [1, 1], call: (sdk, p, o) => sdk.unbind({ binding_id: p[0], expected_revision: integerOption(o, "--revision"), idempotency_key: o["--idempotency-key"] }) },
65
+ rebind: { values: ["--revision", "--idempotency-key"], booleans: [], arity: [2, 2], call: (sdk, p, o, scope) => sdk.rebind({ ...scope, binding_id: p[0], session: p[1], expected_revision: integerOption(o, "--revision"), idempotency_key: o["--idempotency-key"] }) },
66
+ bindings: {
67
+ values: ["--session", "--platform", "--account", "--chat", "--thread", "--status", "--cursor", "--limit"],
68
+ booleans: [],
69
+ arity: [0, 0],
70
+ call: (sdk, _p, o, scope) => sdk.bindings({ ...scope, session: o["--session"], platform: o["--platform"], account_id: o["--account"], chat_id: o["--chat"], thread_id: o["--thread"], status: o["--status"], cursor: o["--cursor"], limit: integerOption(o, "--limit") }),
71
+ },
72
+ report: {
73
+ values: ["--binding", "--request-id", "--request-kind", "--idempotency-key"],
74
+ booleans: [],
75
+ arity: [3, 3],
76
+ call: (sdk, p, o, scope) => sdk.report({ ...scope, session: p[0], kind: p[1], text: p[2], binding_id: o["--binding"], request_id: o["--request-id"], request_kind: o["--request-kind"], idempotency_key: o["--idempotency-key"] }),
77
+ },
78
+ answer: { values: ["--binding", "--token", ...AUTHOR_FLAGS], booleans: [], arity: [1, 1], call: (sdk, p, o) => sdk.answer({ binding_id: o["--binding"], reply_token: o["--token"], answer: p[0], author: authorInput(o) }) },
79
+ outbox: { values: ["--after", "--limit"], booleans: ["--ack"], arity: [1, 1], call: (sdk, p, o, _scope, flags) => drainOutbox(sdk, p[0], o, flags.has("--ack")) },
80
+ ack: { values: ["--provider-message-id"], booleans: [], arity: [2, 2], call: (sdk, p, o) => sdk.ack({ binding_id: p[0], cursor: Number(p[1]), provider_message_id: o["--provider-message-id"] }) },
81
+ }
82
+
83
+ const REQUIRED = { unbind: ["--revision"], rebind: ["--revision"], bind: ["--platform", "--account", "--chat"], answer: ["--binding", "--token"] }
84
+ const INTEGER_FLAGS = ["--expected-turn", "--limit", "--max-bytes", "--revision", "--after"]
85
+ const CHOICE_FLAGS = { "--mode": ["auto", "steer", "follow_up"], "--direction": ["in", "out", "both"] }
86
+
87
+ function sendTarget(positionals, options) {
88
+ if (positionals.length === 2) return { thread: positionals[0], text: positionals[1] }
89
+ return options["--binding"] === undefined ? { thread: positionals[0], text: undefined } : { text: positionals[0] }
90
+ }
91
+
92
+ function authorInput(o) {
93
+ if (o["--author-id"] === undefined) return undefined
94
+ return { platform_user_id: o["--author-id"], display: o["--author-name"], ...(o["--author-user-id"] === undefined ? {} : { user_id: o["--author-user-id"] }) }
95
+ }
96
+
97
+ function authorProblem(o) {
98
+ const given = AUTHOR_FLAGS.filter((flag) => o[flag] !== undefined)
99
+ if (given.length === 0) return undefined
100
+ if (o["--binding"] === undefined) return "--author-id/--author-name/--author-user-id need --binding: only a bound external thread has authors"
101
+ if (o["--author-id"] === undefined || o["--author-name"] === undefined) return "--author-id and --author-name go together (--author-user-id needs both)"
102
+ return undefined
103
+ }
104
+
105
+ function bindingInput(o) {
106
+ const direction = o["--direction"] === undefined ? undefined : { inbound: o["--direction"] !== "out", outbound: o["--direction"] !== "in" }
107
+ const ttl = o["--ttl"] === undefined ? undefined : o["--ttl"] === "none" ? null : Number(o["--ttl"])
108
+ return {
109
+ platform: o["--platform"],
110
+ account_id: o["--account"],
111
+ chat_id: o["--chat"],
112
+ thread_id: o["--thread"],
113
+ root_message_id: o["--root-message"],
114
+ progress_message_id: o["--progress-message"],
115
+ direction,
116
+ inbound_mode: o["--inbound-mode"],
117
+ outbound_events: o["--events"]?.split(",").filter(Boolean),
118
+ policy_id: o["--policy"],
119
+ ttl_seconds: ttl,
120
+ }
121
+ }
122
+
123
+ function lastItems(result, limit) {
124
+ if (limit === undefined || result.kind !== "ok") return result
125
+ return { ...result, items: result.items.slice(Math.max(0, result.items.length - limit)) }
126
+ }
127
+
128
+ /** The connector drain: read a page, and with `--ack` acknowledge through its newest row. */
129
+ async function drainOutbox(sdk, bindingId, options, ack) {
130
+ const page = await sdk.outbox({ binding_id: bindingId, after_cursor: integerOption(options, "--after"), limit: integerOption(options, "--limit") })
131
+ if (!ack || page.kind !== "ok") return page
132
+ const newest = page.rows.at(-1)
133
+ if (newest === undefined) return { ...page, acked: null }
134
+ const acked = await sdk.ack({ binding_id: bindingId, cursor: newest.cursor })
135
+ return acked.kind === "ok" ? { ...page, acked } : acked
136
+ }
137
+
138
+ function validate(name, parsed) {
139
+ const [min, max] = COMMANDS[name].arity
140
+ const count = parsed.positionals.length
141
+ if (count < min || count > max) return `expects ${min === max ? min : `${min}-${max}`} argument(s), got ${count}`
142
+ for (const flag of REQUIRED[name] ?? []) if (parsed.options[flag] === undefined) return `${flag} is required`
143
+ for (const flag of INTEGER_FLAGS) if (Number.isNaN(integerOption(parsed.options, flag) ?? 0)) return `${flag} must be a non-negative integer`
144
+ for (const [flag, choices] of Object.entries(CHOICE_FLAGS)) {
145
+ const value = parsed.options[flag]
146
+ if (value !== undefined && !choices.includes(value)) return `${flag} must be one of ${choices.join(", ")}, got '${value}'`
147
+ }
148
+ if (name === "send") {
149
+ const binding = parsed.options["--binding"] !== undefined
150
+ if (!binding && count !== 2) return "needs <target> <text> (or --binding <id>)"
151
+ if (parsed.positionals.at(-1).trim() === "") return "<text> is empty"
152
+ if (binding && parsed.options["--mode"] === "steer") return "--binding takes --mode auto or follow_up (capped by the binding's inbound mode); a binding message never steers"
153
+ if (binding && parsed.options["--expected-turn"] !== undefined) return "--binding takes no --expected-turn: a binding message never steers"
154
+ }
155
+ if (name === "send" || name === "answer") {
156
+ const author = authorProblem(parsed.options)
157
+ if (author !== undefined) return author
158
+ }
159
+ if (name === "ack" && !/^\d+$/.test(parsed.positionals[1])) return "<cursor> must be a non-negative integer"
160
+ return undefined
161
+ }
162
+
163
+ /** With `--json`, a failure the CLI answers itself is printed in the SDK's error shape, so stdout is always one JSON value. */
164
+ function refuse({ stdout, stderr }, json, code, line, nextAction) {
165
+ if (json) stdout.write(`${JSON.stringify({ kind: "error", error: { code, message: line, next_action: nextAction } })}\n`)
166
+ stderr.write(`${line}\n`)
167
+ }
168
+
169
+ /** `--json` among the options, before `--`: a usage error is still answered in JSON when the parse itself failed. */
170
+ function launchJson(args) {
171
+ const end = args.indexOf("--")
172
+ return (end === -1 ? args : args.slice(0, end)).includes("--json")
173
+ }
174
+
175
+ export function threadExitCode(result) {
176
+ if (result?.kind === "ok") return THREAD_EXIT.ok
177
+ const code = result?.error?.code
178
+ if (code === "host_unavailable") return THREAD_EXIT.unavailable
179
+ if (code === "internal_error") return THREAD_EXIT.failed
180
+ return THREAD_EXIT.refused
181
+ }
182
+
183
+ /**
184
+ * Loads the SDK the plugin ships. `node:sqlite` is probed lazily first (as `setup-detect.js` does):
185
+ * the gateway store needs it, and a runtime without it gets a named refusal, not a worker crash.
186
+ * The answer is `{ sdk }` or `{ error, code }`: `unsupported` for the runtime, `internal_error` for a
187
+ * plugin SDK that cannot be imported (a broken or partial install).
188
+ */
189
+ export async function loadThreadSdk({ pluginRoot, agentDir, env, cwd, engine, loadSqlite, importSdk, identity }) {
190
+ try {
191
+ await (loadSqlite ?? (() => import("node:sqlite")))()
192
+ } catch {
193
+ return { code: "unsupported", error: `node:sqlite is unavailable in this runtime (${process.versions.bun ? `bun ${process.versions.bun}` : `node ${process.versions.node}`}); the session gateway store needs it` }
194
+ }
195
+ let module
196
+ try {
197
+ module = await (importSdk ?? (() => import(pathToFileURL(join(pluginRoot, "runtime", "thread-sdk", "sdk.js")).href)))()
198
+ } catch (error) {
199
+ return { code: "internal_error", error: `the plugin's thread SDK could not be loaded from ${join(pluginRoot, "runtime", "thread-sdk")}: ${error instanceof Error ? error.message : String(error)}` }
200
+ }
201
+ const who = identity ?? { uid: process.getuid?.() ?? 0, user: userInfo().username }
202
+ const engineStatusAll = async () => engine.run(["host", "status", "--json", "--all", "--include-workers"], { env: { ...env, OMO_AGENT_DIR: agentDir } }).stdout
203
+ return { sdk: module.createThreadSdk({ agentDir, cwd, uid: who.uid, user: who.user, env, engineStatusAll }) }
204
+ }
205
+
206
+ export async function runThreadCommand(args, options) {
207
+ const { stdout, stderr, platform } = options
208
+ const name = args[0]
209
+ const json = launchJson(args)
210
+ if (name === "--help" || name === "-h") {
211
+ stdout.write(`${USAGE}\n`)
212
+ return THREAD_EXIT.ok
213
+ }
214
+ if (name === undefined || !Object.hasOwn(COMMANDS, name)) {
215
+ refuse(options, json, "invalid_arguments", `omo thread: ${name === undefined ? "expects a subcommand" : `unknown subcommand '${name}'`}`, "Run omo thread --help.")
216
+ stderr.write(`${USAGE}\n`)
217
+ return THREAD_EXIT.usage
218
+ }
219
+ // Same refusal as `omo daemon` on win32: the gateway's endpoints are unix sockets.
220
+ if (platform === "win32") {
221
+ refuse(options, json, "unsupported", "omo thread: a task host needs a unix socket, which win32 does not provide", "Run omo thread on macOS or Linux.")
222
+ return THREAD_EXIT.unsupported
223
+ }
224
+ const command = COMMANDS[name]
225
+ const parsed = parseArgs(args.slice(1), { booleans: [...COMMON, ...command.booleans], values: command.values })
226
+ const problem = parsed.error ?? validate(name, parsed)
227
+ if (problem !== undefined) {
228
+ refuse(options, json, "invalid_arguments", `omo thread ${name}: ${problem}`, `Run omo thread --help for the ${name} options.`)
229
+ return THREAD_EXIT.usage
230
+ }
231
+ const loaded = await loadThreadSdk(options)
232
+ if (loaded.error !== undefined && loaded.code === "internal_error") {
233
+ refuse(options, json, "internal_error", `omo thread: ${loaded.error}`, "Reinstall omo (omo update), then retry.")
234
+ return THREAD_EXIT.failed
235
+ }
236
+ if (loaded.error !== undefined) {
237
+ refuse(options, json, "unsupported", `omo thread: ${loaded.error}`, "Run omo thread under bun, or a node with node:sqlite.")
238
+ return THREAD_EXIT.unsupported
239
+ }
240
+ try {
241
+ const scope = parsed.flags.has("--all-scope") ? { all_scope: true } : {}
242
+ const result = await command.call(loaded.sdk, parsed.positionals, parsed.options, scope, parsed.flags)
243
+ const exitCode = threadExitCode(result)
244
+ if (result.kind !== "ok") {
245
+ if (json) stdout.write(`${JSON.stringify(result)}\n`)
246
+ stderr.write(`omo thread ${name}: ${result.error.code}: ${result.error.message}\n next: ${result.error.next_action}\n`)
247
+ return exitCode
248
+ }
249
+ if (json) stdout.write(`${JSON.stringify(name === "list" ? result.threads : result)}\n`)
250
+ else for (const line of humanLines(name, result)) stdout.write(`${line}\n`)
251
+ return exitCode
252
+ } finally {
253
+ // The command's result is already printed; a store that fails to close is logged, never a new outcome.
254
+ await loaded.sdk.dispose().catch((error) => stderr.write(`omo thread: closing the gateway store failed: ${error instanceof Error ? error.message : String(error)}\n`))
255
+ }
256
+ }
257
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omo-ai",
3
- "version": "5.1.11",
3
+ "version": "5.1.13",
4
4
  "type": "module",
5
5
  "description": "OmO Native - the standalone omo command with the OMO extension built in: bun add -g omo-ai",
6
6
  "bin": {
@@ -17,7 +17,7 @@
17
17
  "postinstall": "node bin/senpi-patch.mjs"
18
18
  },
19
19
  "dependencies": {
20
- "@code-yeongyu/senpi": "2026.10.1-3"
20
+ "@code-yeongyu/senpi": "2026.10.3"
21
21
  },
22
22
  "repository": {
23
23
  "type": "git",
@@ -7,6 +7,70 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [5.1.13] - 2026-10-03
11
+
12
+ **MCP tools work again when memory is on.** Since 5.1.10, a finished memory child or delegated task shut the shared MCP servers down for the whole process, so every MCP call failed with `MCP server <name> is disabled`. This release runs on senpi 2026.10.3, which gives each session its own binding to the shared servers; it was verified with the shipped bundle. If you can't update yet, turn memory off until you do: set `"memory": { "enabled": false }` in `~/.omo/omo.json`, or start omo with `--omo-senpi-memory-disabled=true`.
13
+
14
+ ### Fixed
15
+
16
+ **MCP calls no longer fail with `MCP server <name> is disabled` after a child session ends.** A memory child or a delegated task that finished released the MCP service for every session in the process; now it releases only its own binding, and the shared servers stay up. Thanks to @Tygb99 for the report. ([#9461](https://github.com/code-yeongyu/oh-my-openagent/issues/9461), [senpi#2524](https://github.com/code-yeongyu/senpi/pull/2524))
17
+
18
+ **Codex edition (LazyCodex): a project's own registered agent roles can be spawned.** The spawn guard accepted only LazyCodex's 12 bundled roles, so a role registered in `.codex/agents` or `$CODEX_HOME/agents` was refused even though Codex offered it; it now accepts any role with a registered role file and still refuses unregistered ones. Thanks to @aconley-vultr for the report. ([lazycodex#171](https://github.com/code-yeongyu/lazycodex/issues/171), [lazycodex#164](https://github.com/code-yeongyu/lazycodex/issues/164), [#9462](https://github.com/code-yeongyu/oh-my-openagent/pull/9462))
19
+
20
+ **Cancelling a delegated task while its host can't list its sessions keeps the cancel pending** instead of finishing it with the session still open; it completes once the host answers again. ([#9450](https://github.com/code-yeongyu/oh-my-openagent/issues/9450), [#9459](https://github.com/code-yeongyu/oh-my-openagent/pull/9459))
21
+
22
+ **Skills are listed in a stable order**, so the tool prefix of a prompt stays the same between turns and keeps hitting the prompt cache. ([#9433](https://github.com/code-yeongyu/oh-my-openagent/pull/9433))
23
+
24
+ **OpenCode edition fixes, thanks to @cynkai:** an explicitly chosen model no longer falls back through the built-in chain ([#8808](https://github.com/code-yeongyu/oh-my-openagent/pull/8808)); a plugin-load telemetry failure no longer prints into the TUI ([#8804](https://github.com/code-yeongyu/oh-my-openagent/pull/8804)); a background task whose session stopped on an error is finalized ([#8814](https://github.com/code-yeongyu/oh-my-openagent/pull/8814)) and skips fallback models no connected provider serves ([#8844](https://github.com/code-yeongyu/oh-my-openagent/pull/8844)); Sisyphus stays available when Hephaestus isn't registered ([#8841](https://github.com/code-yeongyu/oh-my-openagent/pull/8841)); and `omo.schema.json` embeds the `[opencode]` schema once and no longer marks defaulted fields as required ([#8871](https://github.com/code-yeongyu/oh-my-openagent/pull/8871), [#8873](https://github.com/code-yeongyu/oh-my-openagent/pull/8873)). Tool argument rewrites in the OpenCode edition also keep the original arguments object ([#9466](https://github.com/code-yeongyu/oh-my-openagent/pull/9466)).
25
+
26
+ `omo host status --all` and `omo thread list` no longer show an older session entry as the latest activity when the final JSONL entry is large, partial or invalid. Both surfaces use the same bounded final-record policy, show `null` when freshness cannot be proved, and list known activity newest-first with unknown activity last. ([#9222](https://github.com/code-yeongyu/oh-my-openagent/pull/9222))
27
+
28
+ A message to a published terminal session that the engine still reports as not answering is kept offline instead of being sent straight to its socket, so `omo thread list`, a send and a steer always agree on whether that session can be reached right now; the message is delivered once a later check lists the session live again. A bound send that names its session no longer pays the same lookup twice. ([#9222](https://github.com/code-yeongyu/oh-my-openagent/pull/9222))
29
+
30
+ ### Changed
31
+
32
+ **omo runs on senpi 2026.10.3** (the MCP fix above, plus the Bun `-e` host-launch and WebView readiness fixes). Full list: [senpi 2026.10.3](https://github.com/code-yeongyu/senpi/releases/tag/v2026.10.3).
33
+
34
+ **OpenGateway tasks run without a concurrency limit by default.** ([#9427](https://github.com/code-yeongyu/oh-my-openagent/pull/9427))
35
+
36
+ ### Added
37
+
38
+ _The session gateway entries below are its store foundation; the thread tools become usable with 5.1.14._
39
+
40
+ **Every session can reach every other one, terminal sessions included.** ([#9143](https://github.com/code-yeongyu/oh-my-openagent/issues/9143)) Messages between sessions now go through a session gateway: one small database in the agent directory, with no daemon to run and nothing to configure. A terminal session opens a private control socket, so a Desktop thread, a task child or a script can list it, read it and send to it; the thread tools list terminal sessions with their real names and times. A message never interrupts what you are doing: while you are typing, or a submission of yours is on its way, it waits, and the session shows one line saying a remote message is queued. A message to a session that is not running is kept for 24 hours and taken when the session runs again. Fixed limits stop runaway loops: a session cannot message itself or reply straight back into the chain that messaged it, a chain of messages stops after 4 hops or 64 messages, one turn reaches at most 16 sessions, and one sender gets a burst of 8 messages to a session, then one every 5 seconds.
41
+
42
+ **`omo thread` talks to running sessions from scripts and chat connectors.** ([#9143](https://github.com/code-yeongyu/oh-my-openagent/issues/9143)) `omo thread list|send|read|bind|unbind|rebind|bindings|report|answer|outbox|ack` runs the thread tools' operations without an agent session, with `--json` output that is one JSON value (also on failure) and exit codes a script can branch on: 0 done, 1 refused, 2 usage, 3 nothing answering, 4 unsupported here, 5 internal error. JavaScript can import the same operations from the plugin (`runtime/thread-sdk/sdk.js`) without spawning `omo`. It never starts a host. Reference: [`docs/reference/omo-thread.md`](https://github.com/code-yeongyu/oh-my-openagent/blob/dev/docs/reference/omo-thread.md).
43
+
44
+ **A session can be bound to a chat thread, and report back through it.** ([#9143](https://github.com/code-yeongyu/oh-my-openagent/issues/9143)) A binding attaches one session to one external thread (any platform; OmO ships no chat connector itself). Messages from the thread arrive with `omo thread send --binding`, admitted once per platform event. The session posts milestones, reports, questions and its completion to the binding's outbox with `thread_report`, and a connector reads them with `omo thread outbox` and acknowledges them. A question carries a reply token that only an answer through the same binding can use. A completion is written only when you ask for one, and survives a restart. At most one active binding holds a thread; unbinding and rebinding name the revision they expect, and a rebind never extends the binding's lifetime.
45
+
46
+ **A chat connector can say who wrote each message, and how it should land.** ([#9143](https://github.com/code-yeongyu/oh-my-openagent/issues/9143)) `omo thread send --binding` takes `--author-id` and `--author-name` (and `--author-user-id`). The session sees the author as quoted fields in the message's provenance header, outside the text, so a message cannot pretend to come from someone else. Each author gets their own rate budget in a busy thread. `--mode follow_up` lets one message land more gently than its binding allows, and never more strongly. `answer` takes the same author flags, and the outbox records who answered a question. A connector can watch `<agent dir>/gateway/outbox.marker` to learn about new outbox rows. The reference now spells out that outbox delivery is at-least-once, and that `(binding_id, cursor)` is the key to dedupe by.
47
+
48
+ **`omo host status --all` shows when each terminal session was last active.** ([#9143](https://github.com/code-yeongyu/oh-my-openagent/issues/9143)) Every terminal (`tui`) row now carries `last_activity_at`: the time of the newest entry in its session file, the same time `omo thread list` shows, or `null` when it is not known.
49
+
50
+ **`omo daemon adopt` moves a session from a host into your terminal.** ([#9143](https://github.com/code-yeongyu/oh-my-openagent/issues/9143)) `omo daemon adopt <session>` asks the host holding a Desktop thread or a background session to hand it over, then resumes it in the current terminal, in the session's own directory. A session in the middle of a turn is refused unless you pass `--interrupt`, and input the interrupt took out of its queue becomes your first prompts. A session a Desktop window has open is refused unless you pass `--force`. `omo daemon status` now lists terminal sessions as `tui` rows, and `handoff` and `stop --all` leave them alone.
51
+
52
+ ### Removed
53
+
54
+ **`omo daemon attach` and the shared-host opt-in env (`OMO_ENABLE_SHARED_HOST`) are removed; interactive sessions always run standalone.** ([#9143](https://github.com/code-yeongyu/oh-my-openagent/issues/9143)) A terminal session no longer joins an engine host; it always runs its own session in its own process, and other sessions reach it through the gateway. `OMO_ENABLE_SHARED_HOST` no longer changes anything. To continue a session that lives on a host in a terminal, use `omo daemon adopt`.
55
+
56
+ ## [5.1.12] - 2026-10-03
57
+
58
+ **The desktop app can now see when computer use is waiting on a system permission, and a cancelled task on Windows no longer shows up as a crash.** This release runs on the senpi 2026.10.2 engine, which carries the upstream pi v1.0.0 changes.
59
+
60
+ ### Added
61
+
62
+ **A computer-use permission request reaches the root session as an event.** When a computer-use action, an eval cell or a delegated task stops because the system has not granted a permission computer use needs (such as Screen Recording or Accessibility), omo emits `omo.computer.permission_required` on the root session, with that session's id, once per missing permission. The desktop app can show the request instead of a bare tool error, and other failures never turn into permission events. ([#9393](https://github.com/code-yeongyu/oh-my-openagent/pull/9393))
63
+
64
+ ### Changed
65
+
66
+ **omo runs on senpi 2026.10.2.** Reloading or replacing a session while a monitor or an eval cell is running no longer kills the process, and the engine carries the upstream pi v1.0.0 changes. Full list: [senpi 2026.10.2](https://github.com/code-yeongyu/senpi/releases/tag/v2026.10.2).
67
+
68
+ ### Fixed
69
+
70
+ **On Windows, a cancelled delegated task is recorded as cancelled, not as a crash.** A killed task child could be recorded as an error carrying Bun's child-reaper message when enough of those messages filled the end of its error output; it now counts as killed, while any real error output still marks a crash. ([#9443](https://github.com/code-yeongyu/oh-my-openagent/pull/9443))
71
+
72
+ **A platform package is published only if its release binary passed the smoke.** The platform publish uploaded its npm package before the release-binary smoke ran, so a platform whose smoke failed still published its package (5.1.8 published both Windows x64 packages from a run whose smoke failed). The package is now uploaded only after that platform's smoke passes, so a failed smoke stops exactly that platform's publish while the others still publish, and the linux-arm64 smokes now finish before their packages publish. ([#9385](https://github.com/code-yeongyu/oh-my-openagent/issues/9385), [#9439](https://github.com/code-yeongyu/oh-my-openagent/pull/9439))
73
+
10
74
  ## [5.1.11] - 2026-10-02
11
75
 
12
76
  **New tasks no longer queue forever behind a delegated task that lost its connection to the host.** Such a task used to stay suspended with its lane taken until the session restarted; now it reconnects, or ends within a bounded time and frees the lane. This release runs on the senpi 2026.10.1-3 engine.
@@ -17,6 +81,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
17
81
 
18
82
  Cancelling a delegated task finishes only once the host confirms the child closed, a stopped task whose handle was let go still ends as cancelled, and a stopped task is never continued afterwards. ([#9407](https://github.com/code-yeongyu/oh-my-openagent/pull/9407))
19
83
 
84
+ **The OpenCode edition's skill tool lists skills and commands in a stable order, so its tool definitions stay cacheable.** Skills and commands in the same scope were listed in discovery order, which varies between processes, so the tool definition changed from session to session and each new session and subagent missed the prompt cache. They are now ordered by scope, then by name. Thanks to @kimchupa-l10n for the report. ([#9432](https://github.com/code-yeongyu/oh-my-openagent/issues/9432))
85
+
20
86
  ## [5.1.10] - 2026-10-02
21
87
 
22
88
  **Hotfix: `omo -p` exits again after a delegated task.** Since 5.1.8, a headless or scripted run whose agent handed work to an in-process task printed its answer and then never exited, so CI jobs, cron entries and wrappers that wait on it hung until killed. Interactive sessions leaked a loopback bridge server and its socket for every such task. This release runs on the senpi 2026.10.1-3 engine.
@@ -0,0 +1,2 @@
1
+ // omo:haTcOnW4Q7NFRriYnPezqeOoaLFn_eHDlCpcX3dHCV4:ELjiv3uXvehilVGUyp2URd8J4PhruHKfVy8EzTtMliw
2
+ import{chmodSync as e,closeSync as i,existsSync as t,mkdirSync as n,openSync as r}from"node:fs";import{join as d}from"node:path";import{setTimeout as s}from"node:timers/promises";import{parentPort as a}from"node:worker_threads";function o(e,i,t){return Object.assign(Error(`gateway store lock wait exceeded: ${e} waited ${i} ms for the write lock (limit ${t} ms); another process holds it`),{code:"gateway_lock_wait_exceeded"})}import{join as _}from"node:path";function u(e){return _(e,"gateway")}function l(e,i){return _(u(e),"inbox",i)}import{execFile as c}from"node:child_process";var p="epoch:",E=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function g(e){return new Promise(i=>{c("ps",["-o","lstart=","-p",String(e)],{env:{...process.env,LC_ALL:"C",LANG:"C",TZ:"UTC"},windowsHide:!0,timeout:5e3},(e,t)=>{let n=null===e?function(e){let i=/^[A-Z][a-z]{2} ([A-Z][a-z]{2}) +(\d{1,2}) (\d{2}):(\d{2}):(\d{2}) (\d{4})$/.exec(e.trim());if(null===i)return null;let[,t,n,r,d,s,a]=i,o=E.indexOf(t);return o<0?null:Date.UTC(Number(a),o,Number(n),Number(r),Number(d),Number(s))/1e3}(t):null;i(null===n?null:`${p}${n}`)})})}async function m(e){if(!function(e){try{return process.kill(e,0),!0}catch(e){return!(e instanceof Error&&"code"in e&&"ESRCH"===e.code)}}(e.pid))return!0;if(null===e.process_start_time||!e.process_start_time.startsWith(p))return!1;let i=await g(e.pid);return null!==i&&i!==e.process_start_time}function b(e,i){return e.pid===i.pid&&e.process_start_time===i.process_start_time&&e.instance_id===i.instance_id}class f{params=[];sinkRows=[];db;constructor(e){this.db=e,e.function("gw_p",{deterministic:!1},e=>this.params[e]??null),e.function("gw_sink",{varargs:!0,deterministic:!1},(...e)=>(this.sinkRows.push(e),null))}exec(e){this.db.exec(e)}run(e,i=[]){this.params=i;try{this.db.exec(T(e))}finally{this.params=[]}return Number(this.all(["n"],"SELECT changes() AS n")[0]?.n??0)}all(e,i,t=[],n){this.params=t,this.sinkRows=[];try{let t=void 0===n?"0":`row_number() OVER (ORDER BY ${n})`;return this.db.exec(`SELECT gw_sink(${t}, ${e.join(", ")}) FROM (${T(i)})${void 0===n?"":` ORDER BY ${n}`}`),(void 0===n?this.sinkRows:this.sinkRows.toSorted((e,i)=>Number(e[0])-Number(i[0]))).map(i=>Object.fromEntries(e.map((e,t)=>[e,i[t+1]??null])))}finally{this.params=[],this.sinkRows=[]}}one(e,i,t=[]){return this.all(e,i,t)[0]}}function T(e){let i=0;return e.replace(/\?/g,()=>`gw_p(${i++})`)}function N(e){if("object"!=typeof e||null===e)return!1;let i=e;return 5===i.errcode||6===i.errcode||"string"==typeof i.message&&/database is (locked|busy)/i.test(i.message)}import{createHash as y,randomUUID as v}from"node:crypto";import{closeSync as h,constants as w,mkdirSync as L,openSync as k,readdirSync as R,readFileSync as O,rmSync as S,writeSync as A}from"node:fs";import{userInfo as q}from"node:os";import{join as x}from"node:path";function I(e,i,t){return i===t||function(e,i,t){let n=new Map;for(let i of e){let e=n.get(i.from);void 0===e?n.set(i.from,[i.to]):e.push(i.to)}let r=new Set([i]),d=[i];for(;d.length>0;){let e=d.pop();if(e===t)return!0;for(let i of n.get(e)??[])r.has(i)||(r.add(i),d.push(i))}return!1}(e,t,i)}var D=5e3,U=864e5,C=6048e5,M=1048576,H=2592e6,W=2592e6,X=256,F="session_released";import{existsSync as $,readFileSync as G}from"node:fs";import{join as P}from"node:path";function B(e){let i=P(e,"mailbox.jsonl");if($(i))return function(e){let i=e.slice(0,e.lastIndexOf("\n")+1),t=new Map;for(let e of i.split("\n")){if(0===e.length)continue;let i=JSON.parse(e);if(!j(i)||1!==i.version)throw Error("invalid legacy mailbox event");if("snapshot"===i.kind&&Array.isArray(i.items)){t.clear();for(let e of i.items){let i=K(e);t.set(i.message_seq,i)}}else if("enqueue"===i.kind){let e=K(i.item);t.set(e.message_seq,e)}else{if("remove"!==i.kind||"number"!=typeof i.message_seq)throw Error("invalid legacy mailbox event");t.delete(i.message_seq)}}return J([...t.values()])}(G(i,"utf8"));let t=P(e,"mailbox.json");if(!$(t))return null;let n=JSON.parse(G(t,"utf8"));if(!j(n)||!j(n.queues))throw Error(`invalid legacy mailbox state: ${t}`);let r=[];for(let e of Object.values(n.queues)){if(!Array.isArray(e))throw Error(`invalid legacy mailbox queue: ${t}`);for(let i of e)r.push(K(i))}return J(r)}function J(e){return e.toSorted((e,i)=>e.message_seq-i.message_seq)}function K(e){if(!j(e)||"string"!=typeof e.target||"string"!=typeof e.message||"number"!=typeof e.message_seq||!Number.isInteger(e.message_seq)||"auto"!==e.delivery&&"steer"!==e.delivery&&"follow_up"!==e.delivery||void 0!==e.expected_turn_id&&"string"!=typeof e.expected_turn_id||"string"!=typeof e.operation_id||"string"!=typeof e.accepted_at)throw Error("invalid legacy mailbox item");return{target:e.target,message:e.message,message_seq:e.message_seq,delivery:e.delivery,expected_turn_id:void 0===e.expected_turn_id?null:Y(e.expected_turn_id),operation_id:e.operation_id,accepted_at:e.accepted_at}}function Y(e){let i=/^turn-(0|[1-9]\d*)$/.exec(e);if(null===i)return null;let t=Number(i[1]);return Number.isSafeInteger(t)?t:null}function j(e){return"object"==typeof e&&null!==e&&!Array.isArray(e)}import{createHash as V,createHmac as z,randomUUID as Z,timingSafeEqual as Q}from"node:crypto";var ee=2592e6;function ie(e){return new Date(e).toISOString()}function te(e){return Buffer.from(e).toString("base64url")}function ne(e,i){return te(JSON.stringify({v:1,as_of:e,after:i}))}var re="rt1";function de(e,i){return z("sha256",e).update(`${re}.${i}`).digest().subarray(0,16).toString("base64url")}var se=new WeakMap;function ae(e,i,t){return ye(e,"DELETE FROM receipts WHERE principal = ? AND operation = ? AND idempotency_key = ? AND expires_at <= ?",[i.principal,i.operation,i.idempotency_key,t])}function oe(e,i,t=0){if(i<(se.get(e)??Number.NEGATIVE_INFINITY))return null;let n=function(e,i,t=256){let n=X,r=ye(e,"DELETE FROM receipts WHERE rowid IN (SELECT rowid FROM receipts WHERE expires_at <= ? LIMIT ?)",[i,t]),d=ye(e,"DELETE FROM deliveries WHERE delivery_id IN (SELECT d.delivery_id FROM deliveries d WHERE d.state IN ('applied', 'refused') AND d.updated_at <= ? AND NOT EXISTS (SELECT 1 FROM receipts r WHERE r.delivery_id = d.delivery_id) LIMIT ?)",[i-W,n]),s=ye(e,"DELETE FROM causal_edges WHERE rowid IN (SELECT e.rowid FROM causal_edges e JOIN causal_roots r ON r.root_id = e.root_id WHERE r.expires_at <= ? LIMIT ?)",[i,n]),a=ye(e,"DELETE FROM causal_roots WHERE root_id IN (SELECT r.root_id FROM causal_roots r WHERE r.expires_at <= ? AND NOT EXISTS (SELECT 1 FROM causal_edges e WHERE e.root_id = r.root_id) LIMIT ?)",[i,n]),o=ye(e,"DELETE FROM rate_buckets WHERE rowid IN (SELECT rowid FROM rate_buckets WHERE updated_at <= ? LIMIT ?)",[i-4e4,n]),_=ye(e,`DELETE FROM bindings WHERE binding_id IN (SELECT b.binding_id FROM bindings b WHERE b.status != 'active' AND b.updated_at <= ?\n AND NOT EXISTS (SELECT 1 FROM outbox o WHERE o.binding_id = b.binding_id)\n AND NOT EXISTS (SELECT 1 FROM completion_arms a WHERE a.binding_id = b.binding_id)\n AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.binding_id = b.binding_id AND d.state IN ${ge}) LIMIT ?)`,[ie(i-ee),n]),u=ye(e,"DELETE FROM outbox_cursors WHERE binding_id IN (SELECT c.binding_id FROM outbox_cursors c WHERE NOT EXISTS (SELECT 1 FROM bindings b WHERE b.binding_id = c.binding_id) LIMIT ?)",[n]),l=ye(e,"DELETE FROM session_meta WHERE durable_id IN (SELECT m.durable_id FROM session_meta m WHERE\n m.incarnation IS NULL\n AND NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.target_durable_id = m.durable_id)\n AND NOT EXISTS (SELECT 1 FROM bindings b WHERE b.session_durable_id = m.durable_id)\n AND NOT EXISTS (SELECT 1 FROM outbox o WHERE o.session_durable_id = m.durable_id)\n AND NOT EXISTS (SELECT 1 FROM completion_arms a WHERE a.session_durable_id = m.durable_id) LIMIT ?)",[n]);return{receipts:r,deliveries:d,causal_edges:s,causal_roots:a,rate_buckets:o,bindings:_,outbox_cursors:u,session_meta:l}}(e,i,Math.max(X-t,0)),r=n.receipts+t>=X||Object.values(n).some(e=>e>=X);return se.set(e,r?i:i+36e5),n}var _e=new Set(["invalid_arguments","caller_context_missing","not_found","ambiguous_target","scope_denied","name_conflict","not_resumable","orphaned","foreign_live_owner","no_active_turn","turn_conflict","not_steerable","message_too_large","queue_full","cursor_invalid","cursor_stale","idempotency_conflict","idempotency_in_progress","idempotency_uncertain","approval_unavailable","approval_route_locked","partial_commit","unsupported","overloaded","transport_closed","host_unavailable","internal_error","model_not_found","model_ambiguous","thinking_level_unsupported","loop_detected","binding_conflict","binding_mismatch","binding_inactive","stale_revision","stale_token","already_answered","answer_in_progress"]);function ue(e,i,t,n){if("string"!=typeof(r=e)||!_e.has(r))throw Error(`unknown thread error code: ${String(e)}`);var r;return{code:e,message:i,next_action:t,...void 0===n?{}:{details:n}}}var le={not_steerable:{code:"not_steerable",next:"Send with delivery auto or follow_up; a steer needs a running turn."},turn_conflict:{code:"turn_conflict",next:"Read the target again and retry with its current turn id, or send with delivery auto."},expired:{code:"not_resumable",next:"The target did not take the message within its lifetime; send it again if it still matters."},binding_closed:{code:"invalid_arguments",next:"The binding this message was queued under was detached or rebound; re-read the binding and send again."}};function ce(e,i,t,n,r){let d={kind:"ok",thread_id:e.target_durable_id,delivery_id:e.delivery_id,message_seq:e.seq,effective_mode:e.mode_effective??e.mode_requested,endpoint_kind:t,deduplicated:n};if("admitted"===e.state||"applied"===e.state)return"started"===e.admission_kind||"steered"===e.admission_kind?{...d,delivery:{kind:e.admission_kind,turn_id:String(e.turn_epoch??0)}}:{...d,delivery:{kind:"queued",queue_position:Math.max(i,1)}};if("refused"===e.state){let i=le[e.reason??""]??{code:"internal_error",next:"Call thread_list and check the target."};return{kind:"error",error:ue(i.code,`Delivery ${e.delivery_id} was refused: ${e.reason??"unknown"}.`,i.next,{delivery_id:e.delivery_id})}}return"uncertain"===e.state?{kind:"error",error:ue("idempotency_uncertain",`Delivery ${e.delivery_id} may or may not have reached the target.`,"Read the target transcript before deciding whether to send again.",{delivery_id:e.delivery_id})}:{...d,delivery:{kind:r?"queued_offline":"queued",queue_position:Math.max(i,1)}}}var pe=[["CREATE TABLE deliveries (\n delivery_id TEXT PRIMARY KEY,\n target_durable_id TEXT NOT NULL,\n seq INTEGER NOT NULL,\n sender TEXT NOT NULL,\n sender_turn TEXT,\n envelope TEXT NOT NULL,\n body TEXT NOT NULL,\n bytes INTEGER NOT NULL,\n mode_requested TEXT NOT NULL CHECK (mode_requested IN ('auto', 'steer', 'follow_up')),\n mode_effective TEXT CHECK (mode_effective IS NULL OR mode_effective IN ('steer', 'follow_up')),\n expected_turn_id INTEGER,\n state TEXT NOT NULL CHECK (state IN ('queued', 'admitting', 'admitted', 'applied', 'refused', 'uncertain')),\n reason TEXT,\n admitted_by TEXT,\n claimed_at INTEGER,\n attempt INTEGER NOT NULL DEFAULT 0,\n admission_kind TEXT,\n turn_epoch INTEGER,\n root_id TEXT NOT NULL,\n hop INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n binding_id TEXT,\n binding_revision INTEGER,\n UNIQUE (target_durable_id, seq)\n )","CREATE INDEX deliveries_target_state_seq ON deliveries (target_durable_id, state, seq)","CREATE INDEX deliveries_root ON deliveries (root_id)","CREATE INDEX deliveries_sender_turn ON deliveries (sender, sender_turn)","CREATE TABLE receipts (\n principal TEXT NOT NULL,\n operation TEXT NOT NULL,\n idempotency_key TEXT NOT NULL,\n args_hash TEXT NOT NULL,\n status TEXT NOT NULL CHECK (status IN ('prepared', 'completed', 'uncertain')),\n delivery_id TEXT,\n owner_instance TEXT NOT NULL,\n result TEXT,\n error_note TEXT,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL,\n PRIMARY KEY (principal, operation, idempotency_key)\n )","CREATE INDEX receipts_expiry ON receipts (expires_at)","CREATE TABLE causal_roots (\n root_id TEXT PRIMARY KEY,\n origin_principal TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n expires_at INTEGER NOT NULL\n )","CREATE TABLE causal_edges (\n root_id TEXT NOT NULL,\n from_durable_id TEXT NOT NULL,\n to_durable_id TEXT NOT NULL,\n delivery_id TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n PRIMARY KEY (root_id, from_durable_id, to_durable_id)\n )","CREATE TABLE rate_buckets (\n sender TEXT NOT NULL,\n target_durable_id TEXT NOT NULL,\n tokens REAL NOT NULL,\n updated_at INTEGER NOT NULL,\n PRIMARY KEY (sender, target_durable_id)\n )","CREATE TABLE session_meta (\n durable_id TEXT PRIMARY KEY,\n next_seq INTEGER NOT NULL,\n applied_seq INTEGER NOT NULL DEFAULT 0\n )","CREATE TABLE bindings (\n seq INTEGER PRIMARY KEY AUTOINCREMENT,\n binding_id TEXT NOT NULL UNIQUE,\n schema_version INTEGER NOT NULL DEFAULT 1,\n revision INTEGER NOT NULL CHECK (revision >= 1),\n status TEXT NOT NULL CHECK (status IN ('active', 'detached', 'expired')),\n platform TEXT NOT NULL CHECK (platform IN ('discord', 'telegram', 'slack', 'notion', 'feishu', 'herdr', 'custom')),\n account_id TEXT NOT NULL,\n chat_id TEXT NOT NULL,\n thread_id TEXT NOT NULL,\n root_message_id TEXT,\n progress_message_id TEXT,\n session_realm_id TEXT NOT NULL,\n session_durable_id TEXT NOT NULL,\n direction_inbound INTEGER NOT NULL CHECK (direction_inbound IN (0, 1)),\n direction_outbound INTEGER NOT NULL CHECK (direction_outbound IN (0, 1)),\n inbound_mode TEXT NOT NULL CHECK (inbound_mode IN ('auto', 'follow_up')),\n outbound_events TEXT NOT NULL,\n policy_id TEXT NOT NULL,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n lease_started_at TEXT NOT NULL,\n ttl_seconds INTEGER,\n expires_at TEXT,\n CHECK (direction_inbound = 1 OR direction_outbound = 1)\n )","CREATE UNIQUE INDEX bindings_one_active_thread ON bindings (platform, account_id, chat_id, thread_id) WHERE status = 'active'","CREATE INDEX bindings_session ON bindings (session_durable_id, status)","CREATE TABLE outbox (\n cursor INTEGER PRIMARY KEY AUTOINCREMENT,\n binding_id TEXT NOT NULL,\n revision INTEGER NOT NULL,\n event_kind TEXT NOT NULL CHECK (event_kind IN ('milestone', 'report', 'question', 'completion', 'question_closed')),\n payload TEXT NOT NULL,\n state TEXT NOT NULL CHECK (state IN ('pending', 'acked')),\n provider_message_id TEXT,\n created_at INTEGER NOT NULL,\n acked_at INTEGER\n )","CREATE INDEX outbox_binding_cursor ON outbox (binding_id, cursor)","CREATE TABLE gateway_meta (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n )"],["ALTER TABLE session_meta ADD COLUMN incarnation TEXT","ALTER TABLE outbox ADD COLUMN session_durable_id TEXT","ALTER TABLE outbox ADD COLUMN reply_token TEXT","ALTER TABLE outbox ADD COLUMN ui_request_id TEXT","ALTER TABLE outbox ADD COLUMN incarnation TEXT","ALTER TABLE outbox ADD COLUMN question_state TEXT CHECK (question_state IS NULL OR question_state IN ('pending', 'answered', 'expired', 'cancelled'))","ALTER TABLE outbox ADD COLUMN answer TEXT","ALTER TABLE outbox ADD COLUMN answered_at INTEGER","ALTER TABLE outbox ADD COLUMN outcome TEXT CHECK (outcome IS NULL OR outcome IN ('completed', 'failed', 'cancelled'))","CREATE UNIQUE INDEX outbox_reply_token ON outbox (reply_token) WHERE reply_token IS NOT NULL","CREATE TABLE outbox_cursors (\n binding_id TEXT PRIMARY KEY,\n acked_cursor INTEGER NOT NULL,\n updated_at INTEGER NOT NULL\n )","CREATE TABLE completion_arms (\n arm_seq INTEGER PRIMARY KEY AUTOINCREMENT,\n session_durable_id TEXT NOT NULL,\n binding_id TEXT NOT NULL,\n revision INTEGER NOT NULL,\n text TEXT NOT NULL,\n armed_at INTEGER NOT NULL\n )","CREATE INDEX completion_arms_session ON completion_arms (session_durable_id, armed_at)","INSERT OR IGNORE INTO gateway_meta (key, value) VALUES ('realm_id', 'realm-' || lower(hex(randomblob(16))))","INSERT OR IGNORE INTO gateway_meta (key, value) VALUES ('token_secret', lower(hex(randomblob(32))))"],["ALTER TABLE outbox ADD COLUMN ui_request_kind TEXT CHECK (ui_request_kind IS NULL OR ui_request_kind IN ('question', 'select', 'confirm', 'input', 'editor'))","ALTER TABLE outbox ADD COLUMN answer_state TEXT CHECK (answer_state IS NULL OR answer_state IN ('in_flight', 'delivered'))"],["ALTER TABLE outbox ADD COLUMN answered_by TEXT"],["ALTER TABLE session_meta ADD COLUMN endpoint_socket TEXT","ALTER TABLE session_meta ADD COLUMN endpoint_kind TEXT CHECK (endpoint_kind IS NULL OR endpoint_kind IN ('tui', 'rpc_host'))"]],Ee=["delivery_id","target_durable_id","seq","sender","sender_turn","envelope","body","bytes","mode_requested","mode_effective","expected_turn_id","state","reason","admitted_by","claimed_at","attempt","admission_kind","turn_epoch","root_id","hop","created_at","updated_at","expires_at","binding_id","binding_revision"],ge="('queued', 'admitting', 'admitted')",me=/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$/;function be(e,i,t){return{kind:"refused",code:e,message:i,...void 0===t?{}:{details:t}}}function fe(e){let i=e=>null==e?null:Number(e);return{delivery_id:String(e.delivery_id),target_durable_id:String(e.target_durable_id),seq:Number(e.seq),sender:String(e.sender),sender_turn:null===e.sender_turn?null:String(e.sender_turn),envelope:JSON.parse(String(e.envelope)),body:String(e.body),bytes:Number(e.bytes),mode_requested:e.mode_requested,mode_effective:e.mode_effective??null,expected_turn_id:i(e.expected_turn_id),state:e.state,reason:null===e.reason?null:String(e.reason),admitted_by:null===e.admitted_by?null:JSON.parse(String(e.admitted_by)),claimed_at:i(e.claimed_at),attempt:Number(e.attempt),admission_kind:null===e.admission_kind?null:String(e.admission_kind),turn_epoch:i(e.turn_epoch),root_id:String(e.root_id),hop:Number(e.hop),created_at:Number(e.created_at),updated_at:Number(e.updated_at),expires_at:Number(e.expires_at),binding_id:null===e.binding_id?null:String(e.binding_id),binding_revision:i(e.binding_revision)}}function Te(e,i,t,n){return e.sql.all(Ee,`SELECT ${Ee.join(", ")} FROM deliveries WHERE ${i}`,t,n).map(fe)}function Ne(e,i){return Te(e,"delivery_id = ?",[i])[0]}function ye(e,i,t=[]){let n=e.sql.run(i,t);return e.stats.writes+=n,n}function ve(e,i){if("queued"!==i.state&&"admitting"!==i.state&&"admitted"!==i.state)return 0;let t=e.sql.one(["n"],`SELECT COUNT(*) AS n FROM deliveries WHERE target_durable_id = ? AND state IN ${ge} AND seq <= ?`,[i.target_durable_id,i.seq]);return Number(t?.n??1)}async function he(e,i,t){let n=Date.now();for(;;)try{return e.sql.exec("BEGIN IMMEDIATE"),e.stats.transactions++,!0}catch(r){if(!N(r))throw r;if(e.emit({kind:"busy",op:i}),!t)return!1;let d=Date.now()-n;if(d+2*e.config.busy_timeout_ms>e.config.lock_wait_max_ms)throw e.emit({kind:"lock_wait_exceeded",op:i,waited_ms:d}),o(i,d,e.config.lock_wait_max_ms);await e.delay(e.config.busy_timeout_ms)}}function we(e){try{e.sql.exec("ROLLBACK")}catch{return}}async function Le(e,i,t){await he(e,i,!0);try{let i=t();return e.sql.exec("COMMIT"),i}catch(i){throw we(e),i}}function ke(e,i,t,n){let r=l(e.config.agent_dir,i);L(r,{recursive:!0,mode:448});let d,s=x(r,t);try{d=k(s,w.O_CREAT|w.O_EXCL|w.O_WRONLY,384)}catch(e){if(n&&e instanceof Error&&"code"in e&&"EEXIST"===e.code)return s;throw e}try{A(d,JSON.stringify({pid:e.self.pid,process_start_time:e.self.process_start_time}))}finally{h(d)}return s}function Re(e,i,t){let n=x(l(e.config.agent_dir,i),t);try{S(n),e.stats.marker_unlinks++}catch(e){if(!(e instanceof Error&&"code"in e&&"ENOENT"===e.code))throw e}}function Oe(e,i){try{return R(l(e.config.agent_dir,i)).filter(e=>!e.startsWith("."))}catch(e){if(e instanceof Error&&"code"in e&&"ENOENT"===e.code)return[];throw e}}function Se(e,i){let t=e.sql.one(["next_seq"],"SELECT next_seq FROM session_meta WHERE durable_id = ?",[i]),n=Number(void 0===t?e.sql.one(["m"],"SELECT COALESCE(MAX(seq), 0) + 1 AS m FROM deliveries WHERE target_durable_id = ?",[i])?.m??1:t.next_seq);return ye(e,"INSERT INTO session_meta (durable_id, next_seq) VALUES (?, ?) ON CONFLICT(durable_id) DO UPDATE SET next_seq = excluded.next_seq",[i,n+1]),n}function Ae(e,i){ye(e,`INSERT INTO deliveries (${Ee.join(", ")}) VALUES (${Ee.map(()=>"?").join(", ")})`,[i.delivery_id,i.target_durable_id,i.seq,i.sender,i.sender_turn,JSON.stringify(i.envelope),i.body,i.bytes,i.mode_requested,i.mode_effective,i.expected_turn_id,i.state,i.reason,null===i.admitted_by?null:JSON.stringify(i.admitted_by),i.claimed_at,i.attempt,i.admission_kind,i.turn_epoch,i.root_id,i.hop,i.created_at,i.updated_at,i.expires_at,i.binding_id,i.binding_revision])}function qe(e){return y("sha256").update(JSON.stringify(e)).digest("hex")}function xe(e){return Number(e.sql.one(["user_version"],"SELECT user_version FROM pragma_user_version()")?.user_version??0)}function Ie(e,i,t){let n=e.sql.one(["args_hash","status","delivery_id","owner_instance","result","expires_at"],"SELECT args_hash, status, delivery_id, owner_instance, result, expires_at FROM receipts WHERE principal = ? AND operation = 'deliver' AND idempotency_key = ?",[i,t]);if(void 0!==n)return{args_hash:String(n.args_hash),status:n.status,delivery_id:null===n.delivery_id?null:String(n.delivery_id),owner_instance:String(n.owner_instance),result:null===n.result?null:String(n.result),expires_at:Number(n.expires_at)}}function De(e,i,t){if(t.args_hash!==i.receipt.args_hash)return be("idempotency_conflict","The idempotency key was already used with different arguments.");if("completed"===t.status&&null!==t.result)return{kind:"replay",result:JSON.parse(t.result)};let n=null===t.delivery_id?void 0:Ne(e,t.delivery_id),r=void 0!==n&&("admitted"===n.state||"applied"===n.state||"refused"===n.state);if("prepared"===t.status&&!r&&t.owner_instance===e.config.instance_id)return be("idempotency_in_progress","The same delivery is already in progress.");if("prepared"===t.status&&r){let t={...ce(n,ve(e,n),i.endpoint_kind,!0,!1)};return ye(e,"UPDATE receipts SET status = 'completed', result = ?, updated_at = ? WHERE principal = ? AND operation = 'deliver' AND idempotency_key = ?",[JSON.stringify(t),i.now,i.sender_principal,i.receipt.idempotency_key]),{kind:"replay",result:t}}return"prepared"===t.status&&ye(e,"UPDATE receipts SET status = 'uncertain', error_note = ?, updated_at = ? WHERE principal = ? AND operation = 'deliver' AND idempotency_key = ?",["the sender lost the acknowledgement before the target admitted the delivery",i.now,i.sender_principal,i.receipt.idempotency_key]),be("idempotency_uncertain","The earlier delivery may or may not reach the target; it is never sent twice.",{delivery_id:t.delivery_id,state:n?.state??null})}async function Ue(e,i){if(!me.test(i.target_durable_id)||!me.test(i.delivery_id))return be("invalid_arguments","The target session id is not a durable session id.");if(!await he(e,"enqueue",!1))return{kind:"busy"};let t=null,n=!1;try{let r=ae(e,{principal:i.sender_principal,operation:"deliver",idempotency_key:i.receipt.idempotency_key},i.now),d=Ie(e,i.sender_principal,i.receipt.idempotency_key);if(void 0!==d){let t=De(e,i,d);return e.sql.exec("COMMIT"),n=!0,t}let s=function(e,i){if(null===i.binding)return{expires_at:null};let t=e.sql.one(["revision","status","session_durable_id","direction_inbound","expires_at"],"SELECT revision, status, session_durable_id, direction_inbound, expires_at FROM bindings WHERE binding_id = ?",[i.binding.binding_id]),n=null==t?.expires_at?null:Date.parse(String(t.expires_at));return void 0===t||"active"!==t.status||Number(t.revision)!==i.binding.revision||String(t.session_durable_id)!==i.target_durable_id||1!==Number(t.direction_inbound)||null!==n&&n<=i.now?be("invalid_arguments","The binding is not active for this session at this revision.",{binding:i.binding.binding_id}):{expires_at:n}}(e,i);if("kind"in s)return we(e),s;let a=e.sql.one(["n","b"],`SELECT COUNT(*) AS n, COALESCE(SUM(bytes), 0) AS b FROM deliveries WHERE target_durable_id = ? AND state IN ${ge}`,[i.target_durable_id]),o=Buffer.byteLength(i.body);if(Number(a?.n??0)>=128||Number(a?.b??0)+o>M)return we(e),be("queue_full","The target's delivery backlog is full.",{max_messages:128,max_bytes:M});let _=function(e,i){let t;if(null!==i.cause_delivery_id){let n=Ne(e,i.cause_delivery_id);if(void 0===n||n.target_durable_id!==i.sender_node||"admitting"!==n.state&&"admitted"!==n.state&&"applied"!==n.state)return be("invalid_arguments","The delivery this send continues is not one the sender received.",{guard:"unknown_cause"});t={root_id:n.root_id,hop:n.hop+1,via:[...n.envelope.via,i.sender_node],new_root:!1}}else t={root_id:`root-${v()}`,hop:1,via:[i.sender_node],new_root:!0};if(null!==i.claimed_root_id&&i.claimed_root_id!==t.root_id)return be("invalid_arguments","The causal root is derived by the gateway and cannot be chosen by the caller.",{guard:"forged_root"});if(!t.new_root){let n=e.sql.one(["expires_at"],"SELECT expires_at FROM causal_roots WHERE root_id = ?",[t.root_id]);if(void 0===n||Number(n.expires_at)<=i.now)return be("loop_detected","This causal chain is older than its lifetime.",{guard:"root_expired",root_id:t.root_id})}if(i.target_durable_id===i.sender_node)return be("loop_detected","A session cannot deliver to itself through the gateway.",{guard:"self_send"});if(t.hop>4)return be("loop_detected",`This delivery would be hop ${t.hop}; at most 4 are allowed.`,{guard:"hop_limit",hop:t.hop});let n=Number(e.sql.one(["n"],"SELECT COUNT(*) AS n FROM deliveries WHERE root_id = ? AND state != 'refused'",[t.root_id])?.n??0);return n>=64?be("loop_detected",`This causal chain already carried ${n} deliveries.`,{guard:"causal_budget",root_id:t.root_id}):I(e.sql.all(["from_durable_id","to_durable_id"],"SELECT from_durable_id, to_durable_id FROM causal_edges WHERE root_id = ?",[t.root_id]).map(e=>({from:String(e.from_durable_id),to:String(e.to_durable_id)})),i.sender_node,i.target_durable_id)?be("loop_detected","The target already leads back to the sender in this causal chain.",{guard:"cycle",root_id:t.root_id}):t}(e,i);if("kind"in _)return we(e),_;let u=function(e,i){if(null===i.sender_turn)return;let t=new Set(e.sql.all(["t"],"SELECT DISTINCT target_durable_id AS t FROM deliveries WHERE sender = ? AND sender_turn = ? AND state != 'refused'",[i.sender_principal,i.sender_turn]).map(e=>String(e.t)));return t.has(i.target_durable_id)||t.size<16?void 0:be("overloaded","One turn may reach at most 16 sessions.",{budget:"fanout"})}(e,i)??function(e,i){let t=i.rate_principal??i.sender_principal,n=e.sql.one(["tokens","updated_at"],"SELECT tokens, updated_at FROM rate_buckets WHERE sender = ? AND target_durable_id = ?",[t,i.target_durable_id]),r=void 0===n?8:Math.min(8,Number(n.tokens)+Math.max(0,i.now-Number(n.updated_at))/D);if(r<1)return be("overloaded","Too many deliveries to this session; wait before sending again.",{budget:"pair_rate",retry_after_ms:Math.ceil((1-r)*D)});ye(e,"INSERT INTO rate_buckets (sender, target_durable_id, tokens, updated_at) VALUES (?, ?, ?, ?) ON CONFLICT(sender, target_durable_id) DO UPDATE SET tokens = excluded.tokens, updated_at = excluded.updated_at",[t,i.target_durable_id,r-1,i.now])}(e,i);if(void 0!==u)return we(e),u;_.new_root&&ye(e,"INSERT INTO causal_roots (root_id, origin_principal, created_at, expires_at) VALUES (?, ?, ?, ?)",[_.root_id,i.sender_principal,i.now,i.now+C]),ye(e,"INSERT OR IGNORE INTO causal_edges (root_id, from_durable_id, to_durable_id, delivery_id, created_at) VALUES (?, ?, ?, ?, ?)",[_.root_id,i.sender_node,i.target_durable_id,i.delivery_id,i.now]);let l={delivery_id:i.delivery_id,target_durable_id:i.target_durable_id,seq:Se(e,i.target_durable_id),sender:i.sender_principal,sender_turn:i.sender_turn,envelope:{origin:i.origin,actor:i.actor,via:_.via,root_id:_.root_id,hop:_.hop,delivery_id:i.delivery_id},body:i.body,bytes:o,mode_requested:i.mode,mode_effective:null,expected_turn_id:i.expected_turn_id,state:"queued",reason:null,admitted_by:null,claimed_at:null,attempt:0,admission_kind:null,turn_epoch:null,root_id:_.root_id,hop:_.hop,created_at:i.now,updated_at:i.now,expires_at:Math.min(i.now+U,s.expires_at??Number.POSITIVE_INFINITY),binding_id:i.binding?.binding_id??null,binding_revision:i.binding?.revision??null};Ae(e,l),ye(e,"INSERT INTO receipts (principal, operation, idempotency_key, args_hash, status, delivery_id, owner_instance, created_at, updated_at, expires_at) VALUES (?, 'deliver', ?, ?, 'prepared', ?, ?, ?, ?, ?)",[i.sender_principal,i.receipt.idempotency_key,i.receipt.args_hash,l.delivery_id,e.config.instance_id,i.now,i.now,i.now+H]),t=ke(e,l.target_durable_id,l.delivery_id,!1);let c=ve(e,l);return oe(e,i.now,r),await e.hook("beforeDbCommit"),e.sql.exec("COMMIT"),n=!0,await e.hook("afterDbCommit"),{kind:"inserted",row:l,queue_position:c}}catch(i){throw n||(we(e),null!==t&&S(t,{force:!0})),i}}function Ce(e,i){if(null===e)return"";let t=i.get(e);if(void 0===t){try{t=O(e,"utf8")}catch(e){if(!(e instanceof Error&&"code"in e&&"ENOENT"===e.code))throw e;t=""}i.set(e,t)}return t}function Me(e,i,t){if(null===e.runtime_instance||null===i)return!1;let n=t().get(e.runtime_instance);return void 0!==n&&i<=n}function He(e,i,t){for(let n of Ce(e,t).split("\n")){if(!n.includes(i))continue;let e;try{e=JSON.parse(n)}catch{continue}if("object"!=typeof e||null===e)continue;let t=e;if("custom_message"===t.type&&"session_control_delivery"===t.customType&&t.details?.delivery_id===i)return!0}return!1}function We(e){return`${JSON.stringify(e.admitted_by)}#${e.attempt}`}function Xe(e,i,t,n,r={}){"queued"!==t?"refused"!==t?(ye(e,"UPDATE deliveries SET state = ?, updated_at = ? WHERE delivery_id = ?",[t,n,i.delivery_id]),"applied"===t&&ye(e,"UPDATE session_meta SET applied_seq = MAX(applied_seq, ?) WHERE durable_id = ?",[i.seq,i.target_durable_id])):ye(e,"UPDATE deliveries SET state = 'refused', reason = ?, updated_at = ? WHERE delivery_id = ?",[r.reason??null,n,i.delivery_id]):ye(e,"UPDATE deliveries SET state = 'queued', admitted_by = NULL, claimed_at = NULL, mode_effective = NULL, admission_kind = NULL, turn_epoch = NULL, updated_at = ? WHERE delivery_id = ?",[n,i.delivery_id])}async function Fe(e,i){let t,n=i.target_durable_id,r=new Map,d=new Map,s=new Map,a=()=>(t??=function(e,i){let t=new Map;for(let n of Ce(e,i).split("\n")){if(!n.includes(F))continue;let e;try{e=JSON.parse(n)}catch{continue}let i=e;if("custom"!==i.type||i.customType!==F)continue;let r=i.data?.host_instance,d="string"==typeof i.data?.released_at?Date.parse(i.data.released_at):Number.NaN;"string"==typeof r&&0!==r.length&&Number.isFinite(d)&&t.set(r,Math.max(d,t.get(r)??d))}return t}(i.session_path,d),t);for(let t of Te(e,"target_durable_id = ? AND state IN ('admitting', 'admitted')",[n])){if(null===t.admitted_by||b(t.admitted_by,i.self))continue;let e=JSON.stringify(t.admitted_by),n=s.get(e);void 0===n&&(n=await m(t.admitted_by),s.set(e,n));let o=n||Me(t.admitted_by,t.claimed_at,a)?He(i.session_path,t.delivery_id,d)?"applied":"queued":"dual";r.set(t.delivery_id,{fingerprint:We(t),to:o})}return!0===e.config.test_hooks.announceBarrier&&e.emit({kind:"barrier",op:"reconcile"}),await Le(e,"reconcile",()=>{let t=new Set(i.ledger.pending),d=new Set(i.ledger.emitted),s=[],a=[];for(let o of Te(e,`target_durable_id = ? AND state IN ${ge}`,[n],"seq")){let n=o.state;if("queued"!==o.state){if(null!==o.admitted_by&&b(o.admitted_by,i.self))n=d.has(o.delivery_id)?"applied":t.has(o.delivery_id)?"admitted":"queued";else if(null===o.admitted_by)n="queued";else{let e=r.get(o.delivery_id);if(void 0===e||e.fingerprint!==We(o)||"dual"===e.to){a.push(o.delivery_id);continue}n=e.to}n!==o.state&&(Xe(e,o,n,i.now),s.push({delivery_id:o.delivery_id,from:o.state,to:n}))}else o.expires_at<=i.now&&(Xe(e,o,"refused",i.now,{reason:"expired"}),s.push({delivery_id:o.delivery_id,from:o.state,to:"refused"}))}for(let i of Oe(e,n))"queued"!==e.sql.one(["state"],"SELECT state FROM deliveries WHERE delivery_id = ?",[i])?.state&&Re(e,n,i);return{queued:Te(e,"target_durable_id = ? AND state = 'queued'",[n],"seq"),transitions:s,dual_runtime:a}})}function $e(){try{return q().username}catch{return"unknown"}}import{randomUUID as Ge}from"node:crypto";import{renameSync as Pe,writeFileSync as Be}from"node:fs";var Je=["question","select","confirm","input","editor"];var Ke=new Map([["yes",!0],["y",!0],["true",!0],["no",!1],["n",!1],["false",!1]]);function Ye(e){return""===e.replace(/[\s\p{Cf}]/gu,"")}function je(e){let i=Ke.get(e.trim().toLowerCase());return void 0===i?{value:e,answers:{},comment:e}:{value:e,answers:{},comment:e,confirmed:i}}function Ve(e,i){switch(e){case null:return Ye(i)?{ok:!1,reason:"The question declared no request kind, so the answer needs text; this one is blank. An input or editor that takes an empty answer must be reported with its request kind."}:{ok:!0,fields:je(i)};case"input":case"editor":return{ok:!0,fields:{value:i}};case"select":return Ye(i)?{ok:!1,reason:"A select answer names one of its options; the text is blank."}:{ok:!0,fields:{value:i}};case"question":return Ye(i)?{ok:!1,reason:"A question answer needs text; this one is blank."}:{ok:!0,fields:{answers:{},comment:i}};case"confirm":{let e=Ke.get(i.trim().toLowerCase());return void 0===e?{ok:!1,reason:"A confirm answer is yes or no (also y/n, true/false; any case, surrounding spaces ignored)."}:{ok:!0,fields:{confirmed:e}}}default:return function(e){throw Error(`unknown extension UI request kind: ${String(e)}`)}(e)}}var ze=["binding_id","schema_version","revision","status","platform","account_id","chat_id","thread_id","root_message_id","progress_message_id","session_realm_id","session_durable_id","direction_inbound","direction_outbound","inbound_mode","outbound_events","policy_id","created_at","updated_at","lease_started_at","ttl_seconds","expires_at"],Ze=["cursor","binding_id","revision","event_kind","payload","state","provider_message_id","created_at","reply_token","question_state","outcome","answered_by"];function Qe(e,i,t){return{kind:"refused",code:e,message:i,...void 0===t?{}:{details:t}}}function ei(e){return null==e?null:String(e)}function ii(e){return null==e?null:JSON.stringify(e)}function ti(e){return null==e?null:JSON.parse(String(e))}function ni(e,i,t,n){let r=(s=e.config.agent_dir,_(u(s),"outbox.marker")),d=`${r}.${process.pid}.${Ge()}.tmp`;var s;Be(d,JSON.stringify({binding_id:i,cursor:t,written_at:ie(n)}),{mode:384}),Pe(d,r)}function ri(e){return{schema_version:1,binding_id:String(e.binding_id),revision:Number(e.revision),status:e.status,platform:e.platform,account_id:String(e.account_id),chat_id:String(e.chat_id),thread_id:String(e.thread_id),root_message_id:ei(e.root_message_id),progress_message_id:ei(e.progress_message_id),session_realm_id:String(e.session_realm_id),session_durable_id:String(e.session_durable_id),direction:{inbound:1===Number(e.direction_inbound),outbound:1===Number(e.direction_outbound)},inbound_mode:e.inbound_mode,outbound_events:JSON.parse(String(e.outbound_events)),policy_id:String(e.policy_id),created_at:String(e.created_at),updated_at:String(e.updated_at),lease_started_at:String(e.lease_started_at),ttl_seconds:null===e.ttl_seconds||void 0===e.ttl_seconds?null:Number(e.ttl_seconds),expires_at:ei(e.expires_at)}}function di(e,i,t,n){let r=`SELECT ${ze.join(", ")} FROM bindings WHERE ${i}${void 0===n?"":` ORDER BY ${n.orderBy} LIMIT ?`}`;return e.sql.all(ze,r,void 0===n?t:[...t,n.limit],n?.orderBy).map(ri)}function si(e,i){return di(e,"binding_id = ?",[i])[0]}function ai(e,i){let t=e.sql.one(["value"],"SELECT value FROM gateway_meta WHERE key = ?",[i]);if(void 0===t)throw Error(`gateway_meta ${i} is missing`);return String(t.value)}function oi(e,i){let t=ie(i);ye(e,"UPDATE bindings SET status = 'expired', updated_at = ? WHERE status = 'active' AND expires_at IS NOT NULL AND expires_at <= ?",[t,t])}function _i(e,i){ye(e,"DELETE FROM outbox WHERE state = 'acked' AND acked_at IS NOT NULL AND acked_at <= ?",[i-ee]),ye(e,"DELETE FROM outbox WHERE state = 'pending' AND binding_id IN (SELECT binding_id FROM bindings WHERE status != 'active' AND updated_at <= ?)",[ie(i-ee)])}function ui(e,i){let t=e.sql.one(["args_hash","status","owner_instance","result"],"SELECT args_hash, status, owner_instance, result FROM receipts WHERE principal = ? AND operation = ? AND idempotency_key = ?",[i.principal,i.operation,i.idempotency_key]);if(void 0!==t)return{args_hash:String(t.args_hash),status:String(t.status),owner_instance:String(t.owner_instance),result:ei(t.result)}}async function li(e,i,t,n,r){return await Le(e,i,()=>{oi(e,t);let i=null===n?0:ae(e,n,t),d=function(e,i){if(null===i)return;let t=ui(e,i);return void 0!==t?t.args_hash!==i.args_hash?Qe("idempotency_conflict","The idempotency key was already used with different arguments."):"completed"===t.status&&null!==t.result?{...JSON.parse(t.result),kind:"ok",deduplicated:!0}:Qe("idempotency_uncertain","An earlier call under this key did not record its outcome."):void 0}(e,n);if(void 0!==d)return d;let s=r();return"ok"===s.kind&&function(e,i,t,n){null!==i&&ye(e,"INSERT INTO receipts (principal, operation, idempotency_key, args_hash, status, delivery_id, owner_instance, result, created_at, updated_at, expires_at) VALUES (?, ?, ?, ?, 'completed', NULL, ?, ?, ?, ?, ?)",[i.principal,i.operation,i.idempotency_key,i.args_hash,e.config.instance_id,JSON.stringify(n),t,t,t+H])}(e,n,t,{...s,deduplicated:!1}),oe(e,t,i),"ok"===s.kind?{...s,deduplicated:!1}:s})}async function ci(e,i){return await Le(e,"tool_receipt_begin",()=>{let t=ae(e,i,i.now),n=function(e,i){let t=ui(e,i);if(void 0===t)return ye(e,"INSERT INTO receipts (principal, operation, idempotency_key, args_hash, status, delivery_id, owner_instance, created_at, updated_at, expires_at) VALUES (?, ?, ?, ?, 'prepared', NULL, ?, ?, ?, ?)",[i.principal,i.operation,i.idempotency_key,i.args_hash,e.config.instance_id,i.now,i.now,i.now+H]),{kind:"accepted"};if(t.args_hash!==i.args_hash)return{kind:"conflict"};if("completed"===t.status)return{kind:"replay",result:null===t.result?null:JSON.parse(t.result)};if("prepared"===t.status&&t.owner_instance===e.config.instance_id)return{kind:"in_progress"};"prepared"===t.status&&ye(e,"UPDATE receipts SET status = 'uncertain', error_note = ?, updated_at = ? WHERE principal = ? AND operation = ? AND idempotency_key = ?",["the process that began this call ended before recording its outcome",i.now,i.principal,i.operation,i.idempotency_key]);let n=e.sql.one(["error_note"],"SELECT error_note FROM receipts WHERE principal = ? AND operation = ? AND idempotency_key = ?",[i.principal,i.operation,i.idempotency_key]);return{kind:"uncertain",error_note:ei(n?.error_note)}}(e,i);return oe(e,i.now,t),n})}function pi(e,i){return e.sql.all(["delivery_id"],`SELECT delivery_id, seq FROM deliveries WHERE binding_id = ? AND state IN ${ge}`,[i],"seq").map(e=>String(e.delivery_id))}async function Ei(e,i){let t=void 0===i.cursor?null:function(e){try{let i=JSON.parse(Buffer.from(e,"base64url").toString("utf8"));if(1!==i.v||"number"!=typeof i.as_of||!Number.isInteger(i.as_of))return null;let t=i.after;return null===t||Array.isArray(t)&&2===t.length&&t.every(e=>"string"==typeof e)?{v:1,as_of:i.as_of,after:t}:null}catch{return null}}(i.cursor);if(void 0!==i.cursor&&null===t)return Qe("cursor_invalid","The bindings cursor is not one this gateway issued.");let n=Math.min(Math.max(i.limit??50,1),200);return await Le(e,"list_bindings",()=>{oi(e,i.now);let r=t?.as_of??Number(e.sql.one(["m"],"SELECT COALESCE(MAX(rowid), 0) AS m FROM bindings")?.m??0),d=["rowid <= ?"],s=[r],a=[["session_durable_id","session_durable_id"],["platform","platform"],["account_id","account_id"],["chat_id","chat_id"],["thread_id","thread_id"],["status","status"]];for(let[e,t]of a){let n=i.filter[e];void 0!==n&&(d.push(`${t} = ?`),s.push(n))}null!=t?.after&&(d.push("(created_at > ? OR (created_at = ? AND binding_id > ?))"),s.push(t.after[0],t.after[0],t.after[1]));let o=di(e,d.join(" AND "),s,{orderBy:"created_at, binding_id",limit:n+1}),_=o.slice(0,n),u=_.at(-1);return{kind:"ok",bindings:_,next_cursor:o.length>n&&void 0!==u?ne(r,[u.created_at,u.binding_id]):null}})}function gi(e,i){return ei(e.sql.one(["incarnation"],"SELECT incarnation FROM session_meta WHERE durable_id = ?",[i])?.incarnation)}function mi(e,i){ye(e,"INSERT INTO outbox (binding_id, revision, event_kind, payload, state, provider_message_id, created_at, acked_at, session_durable_id, reply_token, ui_request_id, ui_request_kind, incarnation, question_state, answer, answered_at, outcome) VALUES (?, ?, ?, ?, 'pending', NULL, ?, NULL, ?, ?, ?, ?, ?, ?, NULL, NULL, ?)",[i.binding.binding_id,i.binding.revision,i.event,JSON.stringify({text:i.text}),i.now,i.binding.session_durable_id,i.reply_token??null,i.ui_request_id??null,i.ui_request_kind??null,i.incarnation??null,void 0===i.reply_token?null:"pending",i.outcome??null]);let t=Number(e.sql.one(["c"],"SELECT last_insert_rowid() AS c")?.c);return ni(e,i.binding.binding_id,t,i.now),t}async function bi(e,i){return await li(e,"report",i.now,i.receipt,()=>{let t=null===i.binding_id?function(e,i,t,n){let r=[...new Set(t.flatMap(t=>{let n=e.sql.one(["binding_id"],"SELECT binding_id FROM deliveries WHERE delivery_id = ? AND target_durable_id = ?",[t,i]),r=ei(n?.binding_id);return null===r?[]:[r]}))].sort(),d=e.sql.all(["binding_id"],"SELECT binding_id FROM bindings WHERE session_durable_id = ? AND status = 'active' AND direction_outbound = 1",[i],"binding_id").map(e=>String(e.binding_id)),[s,...a]=r;if(void 0!==s&&a.length>0)return Qe("invalid_arguments",`This session's current run answers messages from ${r.length} bound threads; name binding_id.`,{binding_ids:r});if(void 0!==s&&n&&d.some(e=>e!==s))return Qe("invalid_arguments","This session's current run answers a bound thread's message and a user message (typed in its terminal or sent by an extension) together; name binding_id.",{binding_ids:d});if(void 0!==s)return{binding_id:s};let[o,..._]=d;return void 0!==o&&0===_.length?{binding_id:o}:void 0===o?Qe("invalid_arguments","This session has no active outbound binding and its current run answers no bound message; name binding_id."):Qe("invalid_arguments",`This session has ${d.length} active outbound bindings and its current run answers no bound message; name binding_id.`,{binding_ids:d})}(e,i.session_durable_id,i.origin_delivery_ids,!0===i.origin_local_input):{binding_id:i.binding_id};if("kind"in t)return t;let n=t.binding_id,r=si(e,n);if(void 0===r)return Qe("not_found","No binding has this id.",{binding_id:n});if(r.session_durable_id!==i.session_durable_id)return Qe("scope_denied","The binding is attached to another session.",{binding_id:n});if("active"!==r.status)return Qe("binding_inactive",`The binding is ${r.status}.`,{binding_id:n,status:r.status});if(!r.direction.outbound)return Qe("unsupported","The binding carries no outbound direction.",{binding_id:n,direction:r.direction});if(!r.outbound_events.includes(i.event))return Qe("unsupported",`The binding is not subscribed to ${i.event} events.`,{binding_id:n,outbound_events:r.outbound_events});if(null!==i.ui_request_kind&&"question"!==i.event)return Qe("invalid_arguments","Only a question names a request kind.");let d={binding_id:n,revision:r.revision,event:i.event};if("completion"===i.event){ye(e,"INSERT INTO completion_arms (session_durable_id, binding_id, revision, text, armed_at) VALUES (?, ?, ?, ?, ?)",[i.session_durable_id,n,r.revision,i.text,i.now]);let t=Number(e.sql.one(["s"],"SELECT last_insert_rowid() AS s")?.s);return{kind:"ok",...d,cursor:null,reply_token:null,armed:!0,arm_seq:t}}if("question"===i.event){if(null===i.ui_request_id)return Qe("invalid_arguments","A question names the session's pending extension UI request (request_id).");let t=gi(e,i.session_durable_id),s=function(e,i){let t={b:i.binding_id,r:i.revision,s:i.session_durable_id,i:i.incarnation,q:i.ui_request_id,n:Z()},n=te(JSON.stringify(t));return`${re}.${n}.${de(e,n)}`}(ai(e,"token_secret"),{binding_id:n,revision:r.revision,session_durable_id:i.session_durable_id,incarnation:t,ui_request_id:i.ui_request_id}),a=mi(e,{binding:r,event:"question",text:i.text,now:i.now,reply_token:s,ui_request_id:i.ui_request_id,...null===i.ui_request_kind?{}:{ui_request_kind:i.ui_request_kind},incarnation:t});return _i(e,i.now),{kind:"ok",...d,cursor:a,reply_token:s,armed:!1,arm_seq:null}}let s=mi(e,{binding:r,event:i.event,text:i.text,now:i.now});return _i(e,i.now),{kind:"ok",...d,cursor:s,reply_token:null,armed:!1,arm_seq:null}})}function fi(e,i){return Number(e.sql.one(["acked_cursor"],"SELECT acked_cursor FROM outbox_cursors WHERE binding_id = ?",[i])?.acked_cursor??0)}async function Ti(e,i){let t=Math.min(Math.max(i.limit??100,1),500);return await Le(e,"read_outbox",()=>{oi(e,i.now),_i(e,i.now);let n=si(e,i.binding_id);if(void 0===n)return Qe("not_found","No binding has this id.",{binding_id:i.binding_id});let r=fi(e,n.binding_id),d=i.after_cursor??r,s=e.sql.all(Ze,`SELECT ${Ze.join(", ")} FROM outbox WHERE binding_id = ? AND cursor > ? ORDER BY cursor LIMIT ?`,[n.binding_id,d,t],"cursor").map(e=>function(e,i){let t=e.event_kind;return{cursor:Number(e.cursor),binding_id:String(e.binding_id),revision:Number(e.revision),event:t,text:String(JSON.parse(String(e.payload)).text??""),state:e.state,created_at:ie(Number(e.created_at)),edit_message_id:"milestone"===t?i.progress_message_id:null,provider_message_id:ei(e.provider_message_id),reply_token:ei(e.reply_token),question_state:e.question_state??null,outcome:e.outcome??null,answered_by:ti(e.answered_by)}}(e,n));return oe(e,i.now),{kind:"ok",binding_id:n.binding_id,revision:n.revision,status:n.status,rows:s,next_cursor:s.at(-1)?.cursor??d,acked_cursor:r}})}async function Ni(e,i){return await Le(e,"claim_answer",()=>{oi(e,i.now);let t=function(e,i){let t=i.split(".");if(3!==t.length||t[0]!==re)return null;let n=Buffer.from(de(e,t[1])),r=Buffer.from(t[2]);if(n.length!==r.length||!Q(n,r))return null;try{let e=JSON.parse(Buffer.from(t[1],"base64url").toString("utf8"));return"string"!=typeof e.b||"number"!=typeof e.r||"string"!=typeof e.s||"string"!=typeof e.q||null!==e.i&&"string"!=typeof e.i?null:{binding_id:e.b,revision:e.r,session_durable_id:e.s,incarnation:e.i??null,ui_request_id:e.q}}catch{return null}}(ai(e,"token_secret"),i.reply_token);if(null===t)return Qe("invalid_arguments","This is not a reply token this gateway issued.");if(t.binding_id!==i.binding_id)return Qe("binding_mismatch","The answer arrived through a different binding than the one that asked the question.",{binding_id:i.binding_id});let n=e.sql.one(["cursor","question_state","answer_state","answer","answered_at","answered_by","ui_request_id","ui_request_kind","session"],"SELECT cursor, question_state, answer_state, answer, answered_at, answered_by, ui_request_id, ui_request_kind, session_durable_id AS session FROM outbox WHERE reply_token = ? AND binding_id = ?",[i.reply_token,t.binding_id]);if(void 0===n)return Qe("not_found","The question this token belongs to is no longer in the outbox.");let r="answered"===n.question_state&&"delivered"!==n.answer_state,d=r&&Number(n.answered_at)+12e4<=i.now;if(r&&!d)return Qe("answer_in_progress","Another answer to this question is still being handed to the session.",{cursor:Number(n.cursor)});if("answered"===n.question_state&&!d)return Qe("already_answered",null===n.answer&&"delivered"===n.answer_state?"The session no longer waits for this question (answered or closed elsewhere).":"This question was already answered.",{cursor:Number(n.cursor)});let s="string"==typeof(o=n.ui_request_kind)&&Je.includes(o)?n.ui_request_kind:null,a=Ve(s,i.answer);var o;if(!a.ok)return Qe("invalid_arguments",a.reason,{ui_request_kind:s});let _=si(e,t.binding_id);return void 0===_||"active"!==_.status||_.revision!==t.revision||_.session_durable_id!==t.session_durable_id||gi(e,t.session_durable_id)!==t.incarnation||ei(n.ui_request_id)!==t.ui_request_id?Qe("stale_token","The binding or the session changed since the question was asked.",{binding_id:t.binding_id}):(ye(e,"UPDATE outbox SET question_state = 'answered', answer_state = 'in_flight', answer = ?, answered_at = ?, answered_by = ? WHERE reply_token = ?",[i.answer,i.now,ii(i.answered_by),i.reply_token]),{kind:"ok",session_durable_id:t.session_durable_id,ui_request_id:t.ui_request_id,ui_request_kind:s,cursor:Number(n.cursor),claimed_at:i.now,taken_over:d?{answer:ei(n.answer),answered_at:null===n.answered_at||void 0===n.answered_at?null:Number(n.answered_at),answered_by:ti(n.answered_by)}:null})})}var yi=a;if(null===yi)throw Error("the gateway store worker must run as a worker thread");var vi,hi,wi=[],Li=new Map,ki=!1;function Ri(e){yi?.postMessage({type:"event",event:e})}async function Oi(a,l){if("init"===a)return await async function(a){let{config:l}=a,c=u(l.agent_dir);n(d(c,"inbox"),{recursive:!0,mode:448}),e(c,448);let p=(E=l.agent_dir,_(u(E),"gateway.sqlite"));var E;i(r(p,"a",384)),e(p,384);let m=await(import("node:sqlite"));hi=new m.DatabaseSync(p,{timeout:l.busy_timeout_ms});let b=new f(hi);b.exec(`PRAGMA busy_timeout = ${Math.trunc(l.busy_timeout_ms)}`);let T=Date.now();for(;;)try{b.exec("PRAGMA journal_mode = WAL");break}catch(e){if(!N(e))throw e;let i=Date.now()-T;if(i+10>l.lock_wait_max_ms)throw Ri({kind:"lock_wait_exceeded",op:"open",waited_ms:i}),o("open",i,l.lock_wait_max_ms);await s(10)}b.exec("PRAGMA synchronous = FULL");for(let i of["-wal","-shm"])t(`${p}${i}`)&&e(`${p}${i}`,384);let y={pid:process.pid,process_start_time:await g(process.pid),instance_id:l.instance_id,runtime_instance:l.runtime_instance};vi={sql:b,config:l,self:y,stats:{writes:0,marker_unlinks:0,transactions:0},emit:Ri,hook:Si,delay:e=>s(e)},await async function(e){if(!(xe(e)>=pe.length))for(;;)if(!await Le(e,"migrate",()=>{let i=xe(e);if(i>=pe.length)return!1;for(let t of pe[i])e.sql.exec(t);return e.sql.exec(`PRAGMA user_version = ${i+1}`),!0}))return}(vi);let h=await async function(e,i){let t=0;for(let n of e.config.legacy_mailbox_directories){let r,d=`legacy_mailbox:${n}`;if(void 0!==e.sql.one(["value"],"SELECT value FROM gateway_meta WHERE key = ?",[d]))continue;try{r=B(n)}catch(i){e.emit({kind:"legacy_mailbox_invalid",directory:n,error:i instanceof Error?i.message:String(i)});continue}if(null===r)continue;let s=$e(),a=r.filter(e=>!me.test(e.target)).map(e=>({message_seq:e.message_seq,target:e.target,reason:"invalid_target"})),o=r.filter(e=>me.test(e.target));t+=await Le(e,"migrate_legacy",()=>{let t=0;for(let r of o){let d=`legacy-${qe([n,r.message_seq]).slice(0,32)}`;if(void 0!==Ne(e,d))continue;let a=`root-${v()}`,o=`legacy:${n}`;ye(e,"INSERT INTO causal_roots (root_id, origin_principal, created_at, expires_at) VALUES (?, ?, ?, ?)",[a,o,i,i+C]);let _=Date.parse(r.accepted_at);Ae(e,{delivery_id:d,target_durable_id:r.target,seq:Se(e,r.target),sender:o,sender_turn:null,envelope:{origin:{external:{platform:"legacy_mailbox",account_id:s,chat_id:n,thread_id:"@chat",message_id:r.operation_id}},actor:"legacy thread mailbox",via:[o],root_id:a,hop:1,delivery_id:d},body:r.message,bytes:Buffer.byteLength(r.message),mode_requested:r.delivery,mode_effective:null,expected_turn_id:r.expected_turn_id,state:"queued",reason:null,admitted_by:null,claimed_at:null,attempt:0,admission_kind:null,turn_epoch:null,root_id:a,hop:1,created_at:Number.isFinite(_)?_:i,updated_at:i,expires_at:i+U,binding_id:null,binding_revision:null}),ke(e,r.target,d,!0),t++}return ye(e,"INSERT INTO gateway_meta (key, value) VALUES (?, ?)",[d,JSON.stringify({migrated_at:i,count:t,skipped:a})]),t}),a.length>0&&e.emit({kind:"legacy_mailbox_skipped",directory:n,items:a})}return t}(vi,a.now);return vi.stats.writes=0,vi.stats.transactions=0,vi.stats.marker_unlinks=0,{self:y,legacy_migrated:h}}(l);if("close"===a)return hi?.close(),hi=void 0,vi=void 0,null;let c=function(){if(void 0===vi)throw Error("the gateway store is not open");return vi}();switch(a){case"enqueue":return await Ue(c,l);case"reconcile":return await Fe(c,l);case"claim":return await async function(e,i){return await Le(e,"claim",()=>{let t=Ne(e,i.delivery_id);return void 0===t||"queued"!==t.state?{kind:"lost",state:t?.state??null}:t.expires_at<=i.now?(Xe(e,t,"refused",i.now,{reason:"expired"}),Re(e,t.target_durable_id,t.delivery_id),{kind:"lost",state:"refused"}):(ye(e,"UPDATE deliveries SET state = 'admitting', admitted_by = ?, claimed_at = ?, attempt = attempt + 1, mode_effective = ?, updated_at = ? WHERE delivery_id = ? AND state = 'queued'",[JSON.stringify(i.self),i.now,i.lane,i.now,i.delivery_id]),Re(e,t.target_durable_id,t.delivery_id),{kind:"claimed",row:Ne(e,i.delivery_id)})})}(c,l);case"record_outcome":return await async function(e,i){return await Le(e,"record_outcome",()=>{let t=Ne(e,i.delivery_id);if(void 0===t||"admitting"!==t.state&&"admitted"!==t.state||null===t.admitted_by||!b(t.admitted_by,i.self))return{kind:"lost",state:t?.state??null};let n=i.outcome;return"requeue"===n.kind?Xe(e,t,"queued",i.now):"refused"===n.kind?Xe(e,t,"refused",i.now,{reason:n.reason}):(ye(e,"UPDATE deliveries SET admission_kind = ?, turn_epoch = ?, updated_at = ? WHERE delivery_id = ?",[n.admission_kind,n.turn_epoch,i.now,t.delivery_id]),t.state!==n.kind&&Xe(e,t,n.kind,i.now)),{kind:"claimed",row:Ne(e,i.delivery_id)}})}(c,l);case"refuse_queued":return await async function(e,i){return await Le(e,"refuse",()=>{let t=Ne(e,i.delivery_id);return void 0!==t&&"queued"===t.state&&(Xe(e,t,"refused",i.now,{reason:i.reason}),Re(e,t.target_durable_id,t.delivery_id),!0)})}(c,l);case"complete_receipt":return await async function(e,i){return await Le(e,"complete_receipt",()=>1===ye(e,"UPDATE receipts SET status = 'completed', result = ?, updated_at = ? WHERE principal = ? AND operation = 'deliver' AND idempotency_key = ? AND status = 'prepared' AND owner_instance = ?",[JSON.stringify(i.result),i.now,i.principal,i.idempotency_key,e.config.instance_id]))}(c,l);case"abandon_receipt":return await async function(e,i){return await Le(e,"abandon_receipt",()=>1===ye(e,"UPDATE receipts SET status = 'uncertain', error_note = ?, updated_at = ? WHERE principal = ? AND operation = 'deliver' AND idempotency_key = ? AND status = 'prepared' AND owner_instance = ?",[i.error_note,i.now,i.principal,i.idempotency_key,e.config.instance_id]))}(c,l);case"delivery_view":return function(e,i){let t=Ne(e,i);return void 0===t?null:{row:t,queue_position:ve(e,t)}}(c,l);case"delivery_receipt":return function(e,i){let t=Ie(e,i.principal,i.idempotency_key);if(void 0===t||t.expires_at<=i.now||null===t.delivery_id)return null;let n=Ne(e,t.delivery_id);if(void 0===n)return null;let r="completed"===t.status&&null!==t.result;return{args_hash:t.args_hash,completed:r,result:r?JSON.parse(t.result):null,target_durable_id:n.target_durable_id,binding_revision:n.binding_revision}}(c,l);case"recover_delivery":return await async function(e,i){return await Le(e,"recover_delivery",()=>{let t=Ie(e,i.principal,i.idempotency_key);return void 0===t||t.expires_at<=i.now?null:De(e,{now:i.now,sender_principal:i.principal,endpoint_kind:null,receipt:{idempotency_key:i.idempotency_key,args_hash:i.args_hash}},t)})}(c,l);case"list":return function(e,i){return void 0!==i.target_durable_id?Te(e,"target_durable_id = ?",[i.target_durable_id],"seq"):void 0!==i.root_id?Te(e,"root_id = ?",[i.root_id],"created_at, seq"):Te(e,"1 = 1",[],"created_at, seq")}(c,l);case"is_referenced":return function(e,i){return Number(e.sql.one(["n"],`SELECT COUNT(*) AS n FROM deliveries WHERE target_durable_id = ? AND state IN ${ge}`,[i])?.n??0)>0||Number(e.sql.one(["n"],"SELECT COUNT(*) AS n FROM bindings WHERE session_durable_id = ? AND status = 'active'",[i])?.n??0)>0||Oe(e,i).length>0}(c,l);case"journal_mode":return function(e){return String(e.sql.one(["journal_mode"],"SELECT journal_mode FROM pragma_journal_mode()")?.journal_mode??"")}(c);case"stats":return function(e){return{...e.stats}}(c);case"tool_receipt_begin":return await ci(c,l);case"tool_receipt_settle":return await async function(e,i){return await Le(e,"tool_receipt_settle",()=>{let t="result"in i;return 1===ye(e,`UPDATE receipts SET status = ?, ${t?"result":"error_note"} = ?, updated_at = ? WHERE principal = ? AND operation = ? AND idempotency_key = ? AND status = 'prepared' AND owner_instance = ?`,[t?"completed":"uncertain",t?JSON.stringify(i.result):i.error_note,i.now,i.principal,i.operation,i.idempotency_key,e.config.instance_id])})}(c,l);case"bind":return await async function(e,i){let t=i.binding;return await li(e,"bind",i.now,i.receipt,()=>{let n=di(e,"platform = ? AND account_id = ? AND chat_id = ? AND thread_id = ? AND status = 'active'",[t.platform,t.account_id,t.chat_id,t.thread_id])[0];if(void 0!==n)return Qe("binding_conflict",n.session_durable_id===t.session_durable_id?"This thread is already bound to that session.":"This thread is bound to another session; rebind it explicitly to move it.",{binding_id:n.binding_id,revision:n.revision,session:n.session_durable_id});let r=ie(i.now),d={schema_version:1,binding_id:`bnd-${Z()}`,revision:1,status:"active",platform:t.platform,account_id:t.account_id,chat_id:t.chat_id,thread_id:t.thread_id,root_message_id:t.root_message_id,progress_message_id:t.progress_message_id,session_realm_id:ai(e,"realm_id"),session_durable_id:t.session_durable_id,direction:t.direction,inbound_mode:t.inbound_mode,outbound_events:t.outbound_events,policy_id:t.policy_id,created_at:r,updated_at:r,lease_started_at:r,ttl_seconds:t.ttl_seconds,expires_at:null===t.ttl_seconds?null:ie(i.now+1e3*t.ttl_seconds)};return ye(e,`INSERT INTO bindings (${ze.join(", ")}) VALUES (${ze.map(()=>"?").join(", ")})`,[d.binding_id,d.schema_version,d.revision,d.status,d.platform,d.account_id,d.chat_id,d.thread_id,d.root_message_id,d.progress_message_id,d.session_realm_id,d.session_durable_id,d.direction.inbound?1:0,d.direction.outbound?1:0,d.inbound_mode,JSON.stringify(d.outbound_events),d.policy_id,d.created_at,d.updated_at,d.lease_started_at,d.ttl_seconds,d.expires_at]),{kind:"ok",binding:d}})}(c,l);case"unbind":return await async function(e,i){return await li(e,"unbind",i.now,i.receipt,()=>{let t=si(e,i.binding_id);if(void 0===t)return Qe("not_found","No binding has this id.",{binding_id:i.binding_id});if("active"!==t.status)return{kind:"ok",binding:t,already_closed:!0,in_flight:pi(e,t.binding_id)};if(t.revision!==i.expected_revision)return Qe("stale_revision","The binding changed since that revision.",{binding_id:t.binding_id,revision:t.revision});let n=ie(i.now);return ye(e,"UPDATE bindings SET status = 'detached', revision = revision + 1, updated_at = ? WHERE binding_id = ? AND revision = ?",[n,t.binding_id,t.revision]),ye(e,"DELETE FROM completion_arms WHERE binding_id = ?",[t.binding_id]),{kind:"ok",binding:si(e,t.binding_id),already_closed:!1,in_flight:pi(e,t.binding_id)}})}(c,l);case"rebind":return await async function(e,i){return await li(e,"rebind",i.now,i.receipt,()=>{let t=si(e,i.binding_id);if(void 0===t)return Qe("not_found","No binding has this id.",{binding_id:i.binding_id});if("active"!==t.status)return Qe("binding_inactive",`The binding is ${t.status}.`,{binding_id:t.binding_id,status:t.status});if(t.revision!==i.expected_revision)return Qe("stale_revision","The binding changed since that revision.",{binding_id:t.binding_id,revision:t.revision});if(t.session_durable_id===i.session_durable_id)return Qe("invalid_arguments","The binding is already attached to that session.",{binding_id:t.binding_id});let n=ie(i.now),r=e.sql.all(["delivery_id","target"],"SELECT delivery_id, target_durable_id AS target, seq FROM deliveries WHERE binding_id = ? AND binding_revision = ? AND state = 'queued'",[t.binding_id,t.revision],"seq").map(e=>({delivery_id:String(e.delivery_id),target:String(e.target)}));for(let t of r)ye(e,"UPDATE deliveries SET state = 'refused', reason = 'binding_closed', updated_at = ? WHERE delivery_id = ? AND state = 'queued'",[i.now,t.delivery_id]),Re(e,t.target,t.delivery_id);return ye(e,"UPDATE bindings SET session_durable_id = ?, revision = revision + 1, lease_started_at = ?, updated_at = ? WHERE binding_id = ? AND revision = ?",[i.session_durable_id,n,n,t.binding_id,t.revision]),ye(e,"DELETE FROM completion_arms WHERE binding_id = ?",[t.binding_id]),{kind:"ok",binding:si(e,t.binding_id),closed:r.map(e=>e.delivery_id)}})}(c,l);case"list_bindings":return await Ei(c,l);case"binding_view":return await async function(e,i){return await Le(e,"binding_view",()=>(oi(e,i.now),si(e,i.binding_id)??null))}(c,l);case"register_incarnation":return await async function(e,i){await Le(e,"register_incarnation",()=>{ye(e,"INSERT INTO session_meta (durable_id, next_seq, incarnation, endpoint_socket, endpoint_kind)\n VALUES (?, (SELECT COALESCE(MAX(seq), 0) + 1 FROM deliveries WHERE target_durable_id = ?), ?, ?, ?)\n ON CONFLICT(durable_id) DO UPDATE SET incarnation = excluded.incarnation,\n endpoint_socket = excluded.endpoint_socket, endpoint_kind = excluded.endpoint_kind",[i.durable_id,i.durable_id,i.incarnation,i.endpoint?.socket??null,i.endpoint?.kind??null])})}(c,l);case"clear_endpoint":return await async function(e,i){await Le(e,"clear_endpoint",()=>{ye(e,"UPDATE session_meta SET endpoint_socket = NULL, endpoint_kind = NULL WHERE durable_id = ? AND incarnation = ?",[i.durable_id,i.incarnation])})}(c,l);case"session_owner":return function(e,i){let t=e.sql.one(["incarnation","endpoint_socket","endpoint_kind"],"SELECT incarnation, endpoint_socket, endpoint_kind FROM session_meta WHERE durable_id = ?",[i]);return void 0===t||"string"!=typeof t.incarnation?null:{incarnation:t.incarnation,endpoint:"string"!=typeof t.endpoint_socket||"tui"!==t.endpoint_kind&&"rpc_host"!==t.endpoint_kind?null:{socket:t.endpoint_socket,kind:t.endpoint_kind}}}(c,l);case"report":return await bi(c,l);case"emit_completions":return await async function(e,i){return await Le(e,"emit_completions",()=>{oi(e,i.now);let t=["arm_seq","binding_id","revision","text"],n=void 0===i.through_arm_seq?e.sql.all(t,"SELECT arm_seq, binding_id, revision, text FROM completion_arms WHERE session_durable_id = ?",[i.session_durable_id],"arm_seq"):e.sql.all(t,"SELECT arm_seq, binding_id, revision, text FROM completion_arms WHERE session_durable_id = ? AND arm_seq <= ?",[i.session_durable_id,i.through_arm_seq],"arm_seq"),r=new Map;for(let i of n)ye(e,"DELETE FROM completion_arms WHERE arm_seq = ?",[Number(i.arm_seq)]),r.set(String(i.binding_id),i);let d=[];for(let t of[...r.values()].toSorted((e,i)=>String(e.binding_id)<String(i.binding_id)?-1:1)){let n=si(e,String(t.binding_id));void 0!==n&&"active"===n.status&&n.revision===Number(t.revision)&&n.session_durable_id===i.session_durable_id&&n.direction.outbound&&n.outbound_events.includes("completion")&&d.push({binding_id:n.binding_id,cursor:mi(e,{binding:n,event:"completion",text:String(t.text),now:i.now,outcome:i.outcome})})}return d}).then(t=>(e.emit({kind:"completions_emitted",session_durable_id:i.session_durable_id,cursors:t.map(e=>e.cursor)}),t))}(c,l);case"pending_completion_arms":return function(e,i){return Number(e.sql.one(["n"],"SELECT COUNT(*) AS n FROM completion_arms WHERE session_durable_id = ?",[i])?.n??0)}(c,l);case"latest_completion_arm":return function(e,i){let t=e.sql.one(["s"],"SELECT MAX(arm_seq) AS s FROM completion_arms WHERE session_durable_id = ?",[i])?.s;return null==t?null:Number(t)}(c,l);case"read_outbox":return await Ti(c,l);case"ack_outbox":return await async function(e,i){return await Le(e,"ack_outbox",()=>{oi(e,i.now);let t=si(e,i.binding_id);if(void 0===t)return Qe("not_found","No binding has this id.",{binding_id:i.binding_id});let n=fi(e,t.binding_id);if(i.cursor<=n)return{kind:"ok",binding_id:t.binding_id,acked_cursor:n,changed:!1};if(void 0===e.sql.one(["cursor"],"SELECT cursor FROM outbox WHERE binding_id = ? AND cursor = ?",[t.binding_id,i.cursor])){let n=Number(e.sql.one(["m"],"SELECT COALESCE(MAX(cursor), 0) AS m FROM outbox WHERE binding_id = ?",[t.binding_id])?.m??0);return Qe("cursor_invalid",i.cursor>n?"The cursor is past the newest outbox row of this binding.":"The cursor names no outbox row of this binding.",{newest:n})}return ye(e,"UPDATE outbox SET state = 'acked', acked_at = ? WHERE binding_id = ? AND cursor <= ? AND state = 'pending'",[i.now,t.binding_id,i.cursor]),ye(e,"INSERT INTO outbox_cursors (binding_id, acked_cursor, updated_at) VALUES (?, ?, ?) ON CONFLICT(binding_id) DO UPDATE SET acked_cursor = excluded.acked_cursor, updated_at = excluded.updated_at",[t.binding_id,i.cursor,i.now]),void 0!==i.provider_message_id&&(ye(e,"UPDATE outbox SET provider_message_id = ? WHERE binding_id = ? AND cursor = ?",[i.provider_message_id,t.binding_id,i.cursor]),"milestone"===e.sql.one(["event_kind"],"SELECT event_kind FROM outbox WHERE binding_id = ? AND cursor = ?",[t.binding_id,i.cursor])?.event_kind&&null===t.progress_message_id&&ye(e,"UPDATE bindings SET progress_message_id = ?, updated_at = ? WHERE binding_id = ?",[i.provider_message_id,ie(i.now),t.binding_id])),{kind:"ok",binding_id:t.binding_id,acked_cursor:i.cursor,changed:!0}})}(c,l);case"claim_answer":return await Ni(c,l);case"release_answer":return await async function(e,i){return await Le(e,"release_answer",()=>1===ye(e,"UPDATE outbox SET question_state = 'pending', answer_state = NULL, answer = NULL, answered_at = NULL, answered_by = NULL WHERE reply_token = ? AND question_state = 'answered' AND answer_state = 'in_flight' AND answered_at = ?",[i.reply_token,i.claimed_at]))}(c,l);case"confirm_answer":return await async function(e,i){return await Le(e,"confirm_answer",()=>1===ye(e,"UPDATE outbox SET question_state = 'answered', answer_state = 'delivered', answer = ?, answered_at = ?, answered_by = ? WHERE reply_token = ? AND answer_state IS NOT 'delivered'",[i.answer,i.claimed_at,ii(i.answered_by),i.reply_token]))}(c,l);case"mark_prior_delivered":return await async function(e,i){return await Le(e,"mark_prior_delivered",()=>1===ye(e,"UPDATE outbox SET answer_state = 'delivered', answer = ?, answered_at = ?, answered_by = ? WHERE reply_token = ? AND question_state = 'answered' AND answer_state = 'in_flight' AND answered_at = ?",[i.prior.answer,i.prior.answered_at,ii(i.prior.answered_by),i.reply_token,i.claimed_at]))}(c,l);default:throw Error(`unknown gateway store op: ${a}`)}}async function Si(e){let i=vi?.config.test_hooks[e];if(void 0===i)return;if("throw"===i)throw Error(`gateway test hook ${e}`);"sigkill"===i&&(process.kill(process.pid,"SIGKILL"),await new Promise(()=>{}));let t=new Promise(i=>Li.set(e,i));Ri({kind:"paused",hook:e}),await t}yi.on("message",e=>{if("resume"===e.type)return Li.get(e.hook)?.(),void Li.delete(e.hook);wi.push(e),async function(){if(ki)return;ki=!0;try{for(let e=wi.shift();void 0!==e;e=wi.shift())try{let i=await Oi(e.op,e.args);yi?.postMessage({type:"response",id:e.id,ok:!0,value:i})}catch(i){yi?.postMessage({type:"response",id:e.id,ok:!1,error:{message:i instanceof Error?i.message:String(i),stack:i instanceof Error?i.stack:void 0,code:"string"==typeof i?.code?i.code:void 0}})}}finally{ki=!1}}()});