negotium 0.19.2 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-helpers.js +1146 -212
- package/dist/agent-helpers.js.map +27 -19
- package/dist/background-bash.js.map +1 -1
- package/dist/browser-runtime.js.map +1 -1
- package/dist/{chunk-kkftbxh2.js → chunk-7he9nzfa.js} +1 -1
- package/dist/{chunk-55dd0xw7.js → chunk-ad6xqvqy.js} +2 -2
- package/dist/{chunk-55dd0xw7.js.map → chunk-ad6xqvqy.js.map} +2 -2
- package/dist/{chunk-exqz8bez.js → chunk-hh3f0yr7.js} +3 -3
- package/dist/{chunk-exqz8bez.js.map → chunk-hh3f0yr7.js.map} +2 -2
- package/dist/{chunk-aqqmzmft.js → chunk-nm59q3em.js} +1 -1
- package/dist/{chunk-cv7vdsrn.js → chunk-w3c6fc0p.js} +286 -9
- package/dist/{chunk-cv7vdsrn.js.map → chunk-w3c6fc0p.js.map} +10 -7
- package/dist/hosted-agent.js +290 -12
- package/dist/hosted-agent.js.map +10 -7
- package/dist/main.js +4599 -2724
- package/dist/main.js.map +49 -38
- package/dist/mcp-factories.js +1166 -162
- package/dist/mcp-factories.js.map +27 -19
- package/dist/media.js.map +1 -1
- package/dist/prompts.js.map +1 -1
- package/dist/query-runtime.js.map +3 -3
- package/dist/registry.js +3 -3
- package/dist/rollout.js +2 -2
- package/dist/runtime/src/agents/explicit-agent-switch.ts +101 -0
- package/dist/runtime/src/agents/mcp-tools/spawn-subagent.ts +126 -43
- package/dist/runtime/src/agents/public-helpers.ts +5 -0
- package/dist/runtime/src/agents/self-config-core.ts +28 -58
- package/dist/runtime/src/application/submit-runtime-gateway-turn.ts +34 -8
- package/dist/runtime/src/index.ts +75 -0
- package/dist/runtime/src/mcp/peer-bridge.ts +6 -1
- package/dist/runtime/src/mcp/runtime-spec.ts +145 -10
- package/dist/runtime/src/mcp/session-comm/actor-policy.ts +87 -0
- package/dist/runtime/src/mcp/session-comm/context.ts +42 -1
- package/dist/runtime/src/mcp/session-comm/default-host.ts +178 -2
- package/dist/runtime/src/mcp/session-comm/hub-remote-session.ts +553 -0
- package/dist/runtime/src/mcp/session-comm/limits.ts +8 -0
- package/dist/runtime/src/mcp/session-comm/peer-forward.ts +78 -3
- package/dist/runtime/src/mcp/session-comm/server.ts +167 -4
- package/dist/runtime/src/mcp/session-comm/topic-catalog.ts +7 -0
- package/dist/runtime/src/mcp/session-comm/topics.ts +32 -10
- package/dist/runtime/src/mcp-runtime-host.ts +2 -1
- package/dist/runtime/src/node-host.ts +26 -0
- package/dist/runtime/src/platform/mcp-config.ts +56 -2
- package/dist/runtime/src/query/active-rooms.ts +6 -0
- package/dist/runtime/src/runtime/actor-topic-reach.ts +85 -0
- package/dist/runtime/src/runtime/actor-topic-scope.ts +175 -0
- package/dist/runtime/src/runtime/ask-callbacks.ts +73 -20
- package/dist/runtime/src/runtime/gateway-capabilities.ts +22 -0
- package/dist/runtime/src/runtime/remote-session-grant.ts +221 -0
- package/dist/runtime/src/runtime/remote-session-inbox.ts +476 -0
- package/dist/runtime/src/runtime/remote-session-reply-outbox.ts +156 -0
- package/dist/runtime/src/runtime/turn-runner.ts +157 -33
- package/dist/runtime/src/runtime-gateway.ts +20 -0
- package/dist/runtime/src/storage/api-topics.ts +22 -3
- package/dist/runtime/src/storage/remote-session.ts +526 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +75 -2
- package/dist/runtime/src/topics/derive.ts +3 -0
- package/dist/runtime/src/types/api.ts +6 -0
- package/dist/runtime/src/types.ts +42 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-gateway.js.map +2 -2
- package/dist/runtime-helpers.js.map +1 -1
- package/dist/storage.js +16 -4
- package/dist/storage.js.map +4 -4
- package/dist/types/packages/core/src/agents/explicit-agent-switch.d.ts +26 -0
- package/dist/types/packages/core/src/agents/mcp-tools/spawn-subagent.d.ts +12 -2
- package/dist/types/packages/core/src/agents/public-helpers.d.ts +1 -0
- package/dist/types/packages/core/src/agents/self-config-core.d.ts +15 -1
- package/dist/types/packages/core/src/mcp/peer-bridge.d.ts +6 -1
- package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +69 -2
- package/dist/types/packages/core/src/mcp/session-comm/context.d.ts +17 -1
- package/dist/types/packages/core/src/mcp/session-comm/hub-remote-session.d.ts +149 -0
- package/dist/types/packages/core/src/mcp/session-comm/limits.d.ts +8 -0
- package/dist/types/packages/core/src/mcp/session-comm/peer-forward.d.ts +23 -2
- package/dist/types/packages/core/src/mcp/session-comm/topic-catalog.d.ts +6 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +10 -1
- package/dist/types/packages/core/src/query/active-rooms.d.ts +6 -0
- package/dist/types/packages/core/src/runtime/actor-topic-reach.d.ts +49 -0
- package/dist/types/packages/core/src/runtime/actor-topic-scope.d.ts +91 -0
- package/dist/types/packages/core/src/runtime/ask-callbacks.d.ts +8 -2
- package/dist/types/packages/core/src/runtime/remote-session-grant.d.ts +91 -0
- package/dist/types/packages/core/src/runtime/remote-session-inbox.d.ts +83 -0
- package/dist/types/packages/core/src/runtime/remote-session-reply-outbox.d.ts +37 -0
- package/dist/types/packages/core/src/runtime/turn-runner.d.ts +22 -3
- package/dist/types/packages/core/src/runtime-gateway.d.ts +20 -0
- package/dist/types/packages/core/src/storage/api-topics.d.ts +3 -0
- package/dist/types/packages/core/src/storage/remote-session.d.ts +135 -0
- package/dist/types/packages/core/src/storage/runtime-turn-requests.d.ts +12 -1
- package/dist/types/packages/core/src/topics/derive.d.ts +2 -0
- package/dist/types/packages/core/src/types/api.d.ts +6 -0
- package/dist/types/packages/core/src/types.d.ts +40 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
- /package/dist/{chunk-kkftbxh2.js.map → chunk-7he9nzfa.js.map} +0 -0
- /package/dist/{chunk-aqqmzmft.js.map → chunk-nm59q3em.js.map} +0 -0
|
@@ -172,6 +172,44 @@ export interface PeerRuntimeBridgeContext {
|
|
|
172
172
|
canSpawnSubagents: boolean;
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
/**
|
|
176
|
+
* The rooms the turn's human actor may reach on this node, asserted by the
|
|
177
|
+
* hub that owns per-person membership for the `otium` surface.
|
|
178
|
+
*
|
|
179
|
+
* Node topic ids, valid on the node the turn was submitted to. `visible` is
|
|
180
|
+
* every room the actor participates in (owner or member); `owned` is the
|
|
181
|
+
* subset the actor owns. The assertion rides the signed per-turn MCP token,
|
|
182
|
+
* so the agent can never widen it. With one, it is authoritative: reachable is
|
|
183
|
+
* exactly the current room plus `visible`, owned exactly `owned` — the node
|
|
184
|
+
* adds nothing of its own, not even subagent lineage. A turn without one
|
|
185
|
+
* (older hub, or a turn no person started: cron, self-schedule, a subagent's
|
|
186
|
+
* spawn/report/follow-up) is fail-closed to the current room plus the node's
|
|
187
|
+
* own subagent lineage (direct parent, `grant_subagent_tell` targets, own
|
|
188
|
+
* child workers).
|
|
189
|
+
*/
|
|
190
|
+
export interface ActorTopicScope {
|
|
191
|
+
visibleNodeTopicIds: string[];
|
|
192
|
+
ownedNodeTopicIds: string[];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Hub-issued authority for the `node/topic` branches of `session-comm` on
|
|
197
|
+
* the `otium` surface, one per turn. `capability` is an opaque bearer
|
|
198
|
+
* (`rsc1.<payload>.<hmac>`) only the hub can verify: it binds the person who
|
|
199
|
+
* spoke, the origin room and this turn's request id, and the hub re-checks
|
|
200
|
+
* the person's membership of the origin and target rooms on every call. The
|
|
201
|
+
* node holds no key, so it can neither mint nor verify one; the only thing it
|
|
202
|
+
* reads out of the payload is `e` (expiry), base64-decoded untrusted to order
|
|
203
|
+
* grants when several requests fold into one turn. `hubUrl` is where the
|
|
204
|
+
* node presents it (`/api/v1/session-comm/remote/*`). Absent — an older hub,
|
|
205
|
+
* a hub with the feature off, or a turn no person started (cron,
|
|
206
|
+
* self-schedule, subagent lineage) — remote session-comm stays fail-closed.
|
|
207
|
+
*/
|
|
208
|
+
export interface RemoteSessionGrant {
|
|
209
|
+
hubUrl: string;
|
|
210
|
+
capability: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
175
213
|
export interface AgentQueryOptions {
|
|
176
214
|
agent: AgentKind;
|
|
177
215
|
prompt: string;
|
|
@@ -188,6 +226,10 @@ export interface AgentQueryOptions {
|
|
|
188
226
|
userId?: string;
|
|
189
227
|
/** Product actor identity when execution uses a canonical node principal. */
|
|
190
228
|
actorUserId?: string;
|
|
229
|
+
/** Hub-asserted rooms the product actor may reach; absent means fail-closed. */
|
|
230
|
+
actorTopicScope?: ActorTopicScope;
|
|
231
|
+
/** Hub-issued per-turn authority for remote (`node/topic`) session-comm. */
|
|
232
|
+
remoteSession?: RemoteSessionGrant;
|
|
191
233
|
/** Credential namespace when it differs from the execution principal. */
|
|
192
234
|
vaultUserId?: string;
|
|
193
235
|
session?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const NEGOTIUM_VERSION = "0.
|
|
1
|
+
export const NEGOTIUM_VERSION = "0.20.0";
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../packages/core/src/runtime-gateway.ts"],
|
|
4
4
|
"sourcesContent": [
|
|
5
|
-
"import type { MessageDto } from \"#types/api\";\n\nexport const RUNTIME_GATEWAY_VERSION = 1 as const;\nexport const RUNTIME_GATEWAY_CONTROL_PATH = \"/api/v1/control/runtime/v1\";\n\nexport type RuntimeGatewayErrorKind = \"config\" | \"timeout\" | \"transport\" | \"protocol\" | \"http\";\n\nexport class RuntimeGatewayError extends Error {\n constructor(\n message: string,\n public readonly kind: RuntimeGatewayErrorKind,\n public readonly status?: number,\n ) {\n super(message);\n }\n}\n\nexport interface RuntimeGatewayHealth {\n ok: true;\n v: typeof RUNTIME_GATEWAY_VERSION;\n nodeId?: string;\n aiName?: string;\n capabilities: string[];\n cursor: number;\n}\n\n/** Remote-only MCP transport a gateway host may grant to a manager topic. */\nexport interface HostMcpServerSpec {\n type: \"sse\" | \"http\";\n url: string;\n headers?: Record<string, string>;\n timeout?: number;\n}\n\nconst HOST_MCP_SERVER_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nconst HOST_MCP_SERVER_KEYS = new Set([\"type\", \"url\", \"headers\", \"timeout\"]);\nconst HTTP_HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;\n\n/** Keep host grants remote-only so a gateway caller cannot spawn node processes. */\nexport function parseHostMcpServers(value: unknown): Record<string, HostMcpServerSpec> | undefined {\n if (value === undefined) return undefined;\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new TypeError(\"hostMcpServers must be an object\");\n }\n const parsedEntries: Array<[string, HostMcpServerSpec]> = [];\n for (const [name, raw] of Object.entries(value)) {\n if (!HOST_MCP_SERVER_NAME.test(name)) {\n throw new TypeError(`invalid host MCP server name: ${name}`);\n }\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n throw new TypeError(`hostMcpServers.${name} must be an object`);\n }\n const spec = raw as Record<string, unknown>;\n const unsupported = Object.keys(spec).find((key) => !HOST_MCP_SERVER_KEYS.has(key));\n if (unsupported) {\n throw new TypeError(`hostMcpServers.${name}.${unsupported} is not allowed`);\n }\n if (spec.type !== \"sse\" && spec.type !== \"http\") {\n throw new TypeError(`hostMcpServers.${name}.type must be sse or http`);\n }\n if (typeof spec.url !== \"string\") {\n throw new TypeError(`hostMcpServers.${name}.url must be a string`);\n }\n let url: URL;\n try {\n url = new URL(spec.url);\n } catch {\n throw new TypeError(`hostMcpServers.${name}.url must be a valid URL`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new TypeError(`hostMcpServers.${name}.url must use http or https`);\n }\n let headers: Record<string, string> | undefined;\n if (spec.headers !== undefined) {\n if (!spec.headers || typeof spec.headers !== \"object\" || Array.isArray(spec.headers)) {\n throw new TypeError(`hostMcpServers.${name}.headers must be an object`);\n }\n const headerEntries: Array<[string, string]> = [];\n for (const [key, headerValue] of Object.entries(spec.headers)) {\n if (!HTTP_HEADER_NAME.test(key) || typeof headerValue !== \"string\") {\n throw new TypeError(`hostMcpServers.${name}.headers must contain string values`);\n }\n try {\n new Headers([[key, headerValue]]);\n } catch {\n throw new TypeError(`hostMcpServers.${name}.headers contains an invalid header`);\n }\n headerEntries.push([key, headerValue]);\n }\n headers = Object.fromEntries(headerEntries);\n }\n if (\n spec.timeout !== undefined &&\n (typeof spec.timeout !== \"number\" || !Number.isFinite(spec.timeout) || spec.timeout <= 0)\n ) {\n throw new TypeError(`hostMcpServers.${name}.timeout must be a positive number`);\n }\n parsedEntries.push([\n name,\n {\n type: spec.type,\n url: url.toString(),\n ...(headers ? { headers } : {}),\n ...(spec.timeout !== undefined ? { timeout: spec.timeout } : {}),\n },\n ]);\n }\n return Object.fromEntries(parsedEntries);\n}\n\nexport interface RuntimeGatewayTurnInput {\n topicId: string;\n /** Canonical principal on the execution node. */\n userId: string;\n /** Product-side actor, when it differs from the execution principal. */\n actorUserId?: string;\n actorLabel?: string;\n vaultUserId?: string;\n /** Adapter provenance recorded on the canonical message. */\n sourceAdapter?: string;\n text: string;\n clientMessageId: string;\n requestId?: string;\n allowAutoContinue?: boolean;\n threadRootId?: string;\n /** Inline quote-reply target. Mutually exclusive with `threadRootId`. */\n parentId?: string;\n respond?: boolean;\n /** Run without adding the injected prompt or output to the visible transcript. */\n silent?: boolean;\n attachments?: string[];\n visualTools?: boolean;\n fileDeliveryTools?: boolean;\n /** Host-owned remote MCP servers granted to this manager topic. */\n hostMcpServers?: Record<string, HostMcpServerSpec>;\n}\n\nexport interface RuntimeGatewayTurnAcknowledgement {\n ok: true;\n v: typeof RUNTIME_GATEWAY_VERSION;\n accepted: true;\n deduplicated: boolean;\n requestId: string;\n clientMessageId: string;\n topicId: string;\n messageId: string;\n /** Canonical persisted message when the Node supports inline ACK materialization. */\n message?: MessageDto;\n cursor: number;\n}\n\nexport interface RuntimeGatewayTopicDeriveInput {\n userId: string;\n copyHistory: boolean;\n name?: string;\n}\n\nexport interface RuntimeGatewayDerivedTopic {\n id: string;\n title: string;\n kind?: string;\n agent?: string;\n defaultModel?: string;\n defaultEffort?: string;\n parentTopicId?: string;\n isFork?: boolean;\n}\n\nexport interface RuntimeGatewaySseEvent {\n id?: number;\n event: string;\n data: unknown;\n}\n\nexport interface RuntimeGatewayTopicUsage {\n queries: number;\n inputTokens: number;\n outputTokens: number;\n cacheCreationInputTokens: number;\n cacheReadInputTokens: number;\n estimatedCostUsd: number;\n}\n\nexport interface RuntimeGatewayManagerTopic {\n id: string;\n title: string;\n kind: \"manager\";\n agent?: \"claude\" | \"codex\" | \"maestro\";\n participants: Array<{ userId: string; role: \"owner\" | \"member\" }>;\n}\n\nexport interface RuntimeGatewayDeletedMessage {\n id: string;\n topicId: string;\n authorId: string;\n text: string;\n deleted: true;\n createdAt: string;\n}\n\nexport interface RuntimeGatewayCronJob {\n id: string;\n name: string;\n executionPrincipalUserId: string;\n actorOwnerUserId: string;\n topicId: string;\n source: \"prompt\" | \"script\";\n script: string | null;\n scriptExists: boolean;\n prompt: string | null;\n promptPreview: string | null;\n summary: string | null;\n schedule: string;\n timezone: string | null;\n enabled: boolean;\n agent: \"claude\" | \"codex\" | \"maestro\" | null;\n model: string | null;\n effort: string | null;\n nextRunAt: string;\n runCount: number;\n lastRun: Record<string, unknown> | null;\n createdAt: string;\n updatedAt: string;\n canMutate: boolean;\n}\n\nexport interface RuntimeGatewayCronCreateInput {\n userId: string;\n actorUserId: string;\n topicId: string;\n name: string;\n script: string;\n schedule: string;\n timezone?: string;\n agent?: \"claude\" | \"codex\" | \"maestro\";\n model?: string;\n effort?: string;\n}\n\nexport interface RuntimeGatewayCronPatch {\n name?: string;\n topicId?: string;\n script?: string;\n schedule?: string;\n timezone?: string | null;\n enabled?: boolean;\n agent?: \"claude\" | \"codex\" | \"maestro\" | null;\n model?: string | null;\n effort?: string | null;\n}\n\nexport type RuntimeGatewayToken = string | (() => string | Promise<string>);\nexport type RuntimeGatewayFetch = (\n input: string | URL | Request,\n init?: RequestInit,\n) => Promise<Response>;\n\nexport interface RuntimeGatewayClientOptions {\n baseUrl: string;\n token: RuntimeGatewayToken;\n /** Loopback control path or an authenticated relay forward prefix. */\n pathPrefix?: string;\n fetch?: RuntimeGatewayFetch;\n requestTimeoutMs?: number;\n compactTimeoutMs?: number;\n}\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nasync function json(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n throw new RuntimeGatewayError(\n \"Runtime Gateway returned invalid JSON\",\n \"protocol\",\n response.status,\n );\n }\n}\n\nfunction validHealth(value: unknown): value is RuntimeGatewayHealth {\n const body = record(value);\n return Boolean(\n body?.ok === true &&\n body.v === RUNTIME_GATEWAY_VERSION &&\n Array.isArray(body.capabilities) &&\n typeof body.cursor === \"number\",\n );\n}\n\nfunction validAcknowledgedMessage(value: unknown): value is MessageDto {\n const message = record(value);\n return Boolean(\n message &&\n typeof message.id === \"string\" &&\n typeof message.topicId === \"string\" &&\n typeof message.authorId === \"string\" &&\n typeof message.text === \"string\" &&\n typeof message.createdAt === \"string\",\n );\n}\n\nfunction validTurnAck(value: unknown): value is RuntimeGatewayTurnAcknowledgement {\n const body = record(value);\n return Boolean(\n body?.ok === true &&\n body.v === RUNTIME_GATEWAY_VERSION &&\n body.accepted === true &&\n typeof body.deduplicated === \"boolean\" &&\n typeof body.requestId === \"string\" &&\n typeof body.clientMessageId === \"string\" &&\n typeof body.topicId === \"string\" &&\n typeof body.messageId === \"string\" &&\n (body.message === undefined || validAcknowledgedMessage(body.message)) &&\n typeof body.cursor === \"number\",\n );\n}\n\nfunction validManagerTopic(value: unknown): value is RuntimeGatewayManagerTopic {\n const topic = record(value);\n return Boolean(\n typeof topic?.id === \"string\" &&\n typeof topic.title === \"string\" &&\n topic.kind === \"manager\" &&\n Array.isArray(topic.participants),\n );\n}\n\nfunction validDeletedMessage(value: unknown): value is RuntimeGatewayDeletedMessage {\n const message = record(value);\n return Boolean(\n typeof message?.id === \"string\" &&\n typeof message.topicId === \"string\" &&\n typeof message.authorId === \"string\" &&\n typeof message.text === \"string\" &&\n message.deleted === true &&\n typeof message.createdAt === \"string\",\n );\n}\n\nfunction validCronJob(value: unknown): value is RuntimeGatewayCronJob {\n const job = record(value);\n return Boolean(\n typeof job?.id === \"string\" &&\n typeof job.name === \"string\" &&\n typeof job.executionPrincipalUserId === \"string\" &&\n typeof job.actorOwnerUserId === \"string\" &&\n typeof job.topicId === \"string\" &&\n typeof job.schedule === \"string\" &&\n typeof job.enabled === \"boolean\" &&\n typeof job.nextRunAt === \"string\" &&\n typeof job.canMutate === \"boolean\",\n );\n}\n\nexport async function* parseRuntimeGatewaySse(\n body: ReadableStream<Uint8Array>,\n): AsyncGenerator<RuntimeGatewaySseEvent> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let event = \"message\";\n let id: number | undefined;\n let data: string[] = [];\n const flush = (): RuntimeGatewaySseEvent | null => {\n if (data.length === 0) return null;\n const raw = data.join(\"\\n\");\n data = [];\n try {\n return { event, id, data: JSON.parse(raw) };\n } catch {\n throw new RuntimeGatewayError(\"Runtime Gateway SSE event has invalid JSON\", \"protocol\");\n } finally {\n event = \"message\";\n id = undefined;\n }\n };\n try {\n while (true) {\n const chunk = await reader.read();\n if (chunk.done) break;\n buffer += decoder.decode(chunk.value, { stream: true }).replace(/\\r\\n/g, \"\\n\");\n let newline = buffer.indexOf(\"\\n\");\n while (newline >= 0) {\n const line = buffer.slice(0, newline);\n buffer = buffer.slice(newline + 1);\n if (line === \"\") {\n const parsed = flush();\n if (parsed) yield parsed;\n } else if (line.startsWith(\"event:\")) {\n event = line.slice(6).trim() || \"message\";\n } else if (line.startsWith(\"id:\")) {\n const parsed = Number.parseInt(line.slice(3).trim(), 10);\n if (Number.isFinite(parsed)) id = parsed;\n } else if (line.startsWith(\"data:\")) {\n data.push(line.slice(5).trimStart());\n }\n newline = buffer.indexOf(\"\\n\");\n }\n }\n buffer += decoder.decode();\n if (buffer.startsWith(\"data:\")) data.push(buffer.slice(5).trimStart());\n const parsed = flush();\n if (parsed) yield parsed;\n } finally {\n reader.releaseLock();\n }\n}\n\nexport class RuntimeGatewayClient {\n private readonly pathPrefix: string;\n private readonly request: RuntimeGatewayFetch;\n private readonly requestTimeoutMs: number;\n private readonly compactTimeoutMs: number;\n\n constructor(private readonly options: RuntimeGatewayClientOptions) {\n this.pathPrefix = options.pathPrefix ?? RUNTIME_GATEWAY_CONTROL_PATH;\n this.request = options.fetch ?? fetch;\n this.requestTimeoutMs = options.requestTimeoutMs ?? 5_000;\n this.compactTimeoutMs = options.compactTimeoutMs ?? 5 * 60_000;\n }\n\n private endpoint(path: string): string {\n if (!this.options.baseUrl || !this.options.token) {\n throw new RuntimeGatewayError(\"Runtime Gateway is not configured\", \"config\");\n }\n return `${this.options.baseUrl.replace(/\\/+$/, \"\")}${this.pathPrefix}${path}`;\n }\n\n private async authorization(): Promise<string> {\n try {\n const token =\n typeof this.options.token === \"function\" ? await this.options.token() : this.options.token;\n return `Bearer ${token}`;\n } catch (error) {\n throw new RuntimeGatewayError(\n error instanceof Error ? error.message : \"Runtime Gateway token resolution failed\",\n \"config\",\n );\n }\n }\n\n private async send(path: string, init: RequestInit, timeoutMs?: number): Promise<Response> {\n const authorization = await this.authorization();\n try {\n return await this.request(this.endpoint(path), {\n ...init,\n headers: { authorization, ...init.headers },\n signal: timeoutMs === undefined ? init.signal : AbortSignal.timeout(timeoutMs),\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"TimeoutError\") {\n throw new RuntimeGatewayError(\"Runtime Gateway request timed out\", \"timeout\");\n }\n throw new RuntimeGatewayError(\"Runtime Gateway request failed\", \"transport\");\n }\n }\n\n async health(): Promise<RuntimeGatewayHealth> {\n const response = await this.send(\"/health\", { method: \"GET\" }, this.requestTimeoutMs);\n if (!response.ok) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway health request failed\",\n \"http\",\n response.status,\n );\n }\n const body = await json(response);\n if (!validHealth(body)) {\n throw new RuntimeGatewayError(\"Runtime Gateway health response is incompatible\", \"protocol\");\n }\n return body;\n }\n\n async submitTurn(input: RuntimeGatewayTurnInput): Promise<RuntimeGatewayTurnAcknowledgement> {\n const response = await this.send(\n \"/turns\",\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input }),\n },\n this.requestTimeoutMs,\n );\n if (response.status !== 202) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway turn was not accepted\",\n \"http\",\n response.status,\n );\n }\n const body = await json(response);\n if (!validTurnAck(body)) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway turn acknowledgement is incompatible\",\n \"protocol\",\n );\n }\n return body;\n }\n\n async deriveTopic(\n topicId: string,\n input: RuntimeGatewayTopicDeriveInput,\n ): Promise<RuntimeGatewayDerivedTopic> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/derive`,\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input }),\n },\n this.requestTimeoutMs,\n );\n if (response.status !== 201) {\n throw new RuntimeGatewayError(\"Runtime Gateway topic derive failed\", \"http\", response.status);\n }\n const body = await json(response);\n const topic = record(body)?.topic;\n const topicRecord = record(topic);\n if (\n !topicRecord ||\n typeof topicRecord.id !== \"string\" ||\n typeof topicRecord.title !== \"string\"\n ) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway topic derive response is incompatible\",\n \"protocol\",\n );\n }\n return topic as unknown as RuntimeGatewayDerivedTopic;\n }\n\n async ensureManagerTopic(userId: string): Promise<RuntimeGatewayManagerTopic> {\n const response = await this.send(\n \"/manager-topic\",\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, userId }),\n },\n this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway manager topic request failed\",\n \"http\",\n response.status,\n );\n }\n const body = record(await json(response));\n if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION || !validManagerTopic(body.topic)) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway manager topic response is incompatible\",\n \"protocol\",\n );\n }\n return body.topic;\n }\n\n async deleteMessage(\n topicId: string,\n messageId: string,\n actorUserId: string,\n allowAdmin = false,\n ): Promise<RuntimeGatewayDeletedMessage> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/messages/${encodeURIComponent(messageId)}`,\n {\n method: \"DELETE\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n v: RUNTIME_GATEWAY_VERSION,\n actorUserId,\n ...(allowAdmin ? { allowAdmin: true } : {}),\n }),\n },\n this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway message delete failed\",\n \"http\",\n response.status,\n );\n }\n const body = record(await json(response));\n if (\n body?.ok !== true ||\n body.v !== RUNTIME_GATEWAY_VERSION ||\n !validDeletedMessage(body.message)\n ) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway message delete response is incompatible\",\n \"protocol\",\n );\n }\n return body.message;\n }\n\n private async cronRequest(path: string, init: RequestInit): Promise<Record<string, unknown>> {\n const response = await this.send(path, init, this.requestTimeoutMs);\n if (!response.ok) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron request failed\", \"http\", response.status);\n }\n const body = record(await json(response));\n if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron response is incompatible\", \"protocol\");\n }\n return body;\n }\n\n async listCronScripts(): Promise<string[]> {\n const body = await this.cronRequest(\"/cron/scripts\", { method: \"GET\" });\n if (!Array.isArray(body.scripts) || !body.scripts.every((value) => typeof value === \"string\")) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron scripts are incompatible\", \"protocol\");\n }\n return body.scripts;\n }\n\n async listCronJobs(\n userId: string,\n actorUserId: string,\n actorIsAdmin = false,\n ): Promise<RuntimeGatewayCronJob[]> {\n const search = new URLSearchParams({\n user: userId,\n actorUserId,\n actorIsAdmin: actorIsAdmin ? \"1\" : \"0\",\n });\n const body = await this.cronRequest(`/cron/jobs?${search}`, { method: \"GET\" });\n if (!Array.isArray(body.jobs) || !body.jobs.every(validCronJob)) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron jobs are incompatible\", \"protocol\");\n }\n return body.jobs;\n }\n\n async createCronJob(input: RuntimeGatewayCronCreateInput): Promise<RuntimeGatewayCronJob> {\n const body = await this.cronRequest(\"/cron/jobs\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input }),\n });\n if (!validCronJob(body.job)) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron job is incompatible\", \"protocol\");\n }\n return body.job;\n }\n\n async updateCronJob(\n jobId: string,\n userId: string,\n actorUserId: string,\n patch: RuntimeGatewayCronPatch,\n actorIsAdmin = false,\n ): Promise<RuntimeGatewayCronJob> {\n const body = await this.cronRequest(`/cron/jobs/${encodeURIComponent(jobId)}`, {\n method: \"PATCH\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n v: RUNTIME_GATEWAY_VERSION,\n userId,\n actorUserId,\n actorIsAdmin,\n patch,\n }),\n });\n if (!validCronJob(body.job)) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron job is incompatible\", \"protocol\");\n }\n return body.job;\n }\n\n async deleteCronJob(\n jobId: string,\n userId: string,\n actorUserId: string,\n actorIsAdmin = false,\n ): Promise<string> {\n const search = new URLSearchParams({\n user: userId,\n actorUserId,\n actorIsAdmin: actorIsAdmin ? \"1\" : \"0\",\n });\n const body = await this.cronRequest(`/cron/jobs/${encodeURIComponent(jobId)}?${search}`, {\n method: \"DELETE\",\n });\n if (typeof body.deleted !== \"string\") {\n throw new RuntimeGatewayError(\"Runtime Gateway cron delete is incompatible\", \"protocol\");\n }\n return body.deleted;\n }\n\n async *events(\n after: number,\n topicId?: string,\n signal?: AbortSignal,\n ): AsyncGenerator<RuntimeGatewaySseEvent> {\n const search = new URLSearchParams({ after: String(Math.max(0, after)) });\n if (topicId) search.set(\"topicId\", topicId);\n const response = await this.send(`/events?${search}`, { method: \"GET\", signal });\n if (!response.ok || !response.body) {\n throw new RuntimeGatewayError(\"Runtime Gateway event stream failed\", \"http\", response.status);\n }\n yield* parseRuntimeGatewaySse(response.body);\n }\n\n async getTopicUsage(topicId: string, userId: string): Promise<RuntimeGatewayTopicUsage> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/usage?user=${encodeURIComponent(userId)}`,\n { method: \"GET\" },\n this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway topic usage request failed\",\n \"http\",\n response.status,\n );\n }\n const usage = record(record(await json(response))?.usage);\n const read = (key: keyof RuntimeGatewayTopicUsage): number => {\n const value = usage?.[key];\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n };\n return {\n queries: read(\"queries\"),\n inputTokens: read(\"inputTokens\"),\n outputTokens: read(\"outputTokens\"),\n cacheCreationInputTokens: read(\"cacheCreationInputTokens\"),\n cacheReadInputTokens: read(\"cacheReadInputTokens\"),\n estimatedCostUsd: read(\"estimatedCostUsd\"),\n };\n }\n\n async abortTurn(topicId: string, userId: string): Promise<boolean> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/abort`,\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, userId }),\n },\n this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\"Runtime Gateway turn abort failed\", \"http\", response.status);\n }\n const body = record(await json(response));\n if (\n body?.ok !== true ||\n body.v !== RUNTIME_GATEWAY_VERSION ||\n typeof body.aborted !== \"boolean\"\n ) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway turn abort response is incompatible\",\n \"protocol\",\n );\n }\n return body.aborted;\n }\n\n async resetSession(\n topicId: string,\n userId: string,\n reason?: string,\n actorUserId?: string,\n ): Promise<string | undefined> {\n return this.sessionCommand(topicId, userId, \"reset\", reason, actorUserId);\n }\n\n async compactSession(\n topicId: string,\n userId: string,\n reason?: string,\n actorUserId?: string,\n ): Promise<string | undefined> {\n return this.sessionCommand(topicId, userId, \"compact\", reason, actorUserId);\n }\n\n private async sessionCommand(\n topicId: string,\n userId: string,\n command: \"reset\" | \"compact\",\n reason?: string,\n actorUserId?: string,\n ): Promise<string | undefined> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/session/${command}`,\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n v: RUNTIME_GATEWAY_VERSION,\n userId,\n ...(actorUserId ? { actorUserId } : {}),\n ...(reason ? { reason } : {}),\n }),\n },\n command === \"compact\" ? this.compactTimeoutMs : this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\n `Runtime Gateway session ${command} failed`,\n \"http\",\n response.status,\n );\n }\n const body = record(await json(response));\n if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION) {\n throw new RuntimeGatewayError(\n `Runtime Gateway session ${command} response is incompatible`,\n \"protocol\",\n );\n }\n return typeof body.result === \"string\" ? body.result : undefined;\n }\n}\n"
|
|
5
|
+
"import type { ActorTopicScope, RemoteSessionGrant } from \"#types\";\nimport type { MessageDto } from \"#types/api\";\n\nexport const RUNTIME_GATEWAY_VERSION = 1 as const;\nexport const RUNTIME_GATEWAY_CONTROL_PATH = \"/api/v1/control/runtime/v1\";\n\nexport type RuntimeGatewayErrorKind = \"config\" | \"timeout\" | \"transport\" | \"protocol\" | \"http\";\n\nexport class RuntimeGatewayError extends Error {\n constructor(\n message: string,\n public readonly kind: RuntimeGatewayErrorKind,\n public readonly status?: number,\n ) {\n super(message);\n }\n}\n\nexport interface RuntimeGatewayHealth {\n ok: true;\n v: typeof RUNTIME_GATEWAY_VERSION;\n nodeId?: string;\n aiName?: string;\n capabilities: string[];\n cursor: number;\n}\n\n/** Remote-only MCP transport a gateway host may grant to a manager topic. */\nexport interface HostMcpServerSpec {\n type: \"sse\" | \"http\";\n url: string;\n headers?: Record<string, string>;\n timeout?: number;\n}\n\nconst HOST_MCP_SERVER_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\nconst HOST_MCP_SERVER_KEYS = new Set([\"type\", \"url\", \"headers\", \"timeout\"]);\nconst HTTP_HEADER_NAME = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;\n\n/** Keep host grants remote-only so a gateway caller cannot spawn node processes. */\nexport function parseHostMcpServers(value: unknown): Record<string, HostMcpServerSpec> | undefined {\n if (value === undefined) return undefined;\n if (!value || typeof value !== \"object\" || Array.isArray(value)) {\n throw new TypeError(\"hostMcpServers must be an object\");\n }\n const parsedEntries: Array<[string, HostMcpServerSpec]> = [];\n for (const [name, raw] of Object.entries(value)) {\n if (!HOST_MCP_SERVER_NAME.test(name)) {\n throw new TypeError(`invalid host MCP server name: ${name}`);\n }\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) {\n throw new TypeError(`hostMcpServers.${name} must be an object`);\n }\n const spec = raw as Record<string, unknown>;\n const unsupported = Object.keys(spec).find((key) => !HOST_MCP_SERVER_KEYS.has(key));\n if (unsupported) {\n throw new TypeError(`hostMcpServers.${name}.${unsupported} is not allowed`);\n }\n if (spec.type !== \"sse\" && spec.type !== \"http\") {\n throw new TypeError(`hostMcpServers.${name}.type must be sse or http`);\n }\n if (typeof spec.url !== \"string\") {\n throw new TypeError(`hostMcpServers.${name}.url must be a string`);\n }\n let url: URL;\n try {\n url = new URL(spec.url);\n } catch {\n throw new TypeError(`hostMcpServers.${name}.url must be a valid URL`);\n }\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") {\n throw new TypeError(`hostMcpServers.${name}.url must use http or https`);\n }\n let headers: Record<string, string> | undefined;\n if (spec.headers !== undefined) {\n if (!spec.headers || typeof spec.headers !== \"object\" || Array.isArray(spec.headers)) {\n throw new TypeError(`hostMcpServers.${name}.headers must be an object`);\n }\n const headerEntries: Array<[string, string]> = [];\n for (const [key, headerValue] of Object.entries(spec.headers)) {\n if (!HTTP_HEADER_NAME.test(key) || typeof headerValue !== \"string\") {\n throw new TypeError(`hostMcpServers.${name}.headers must contain string values`);\n }\n try {\n new Headers([[key, headerValue]]);\n } catch {\n throw new TypeError(`hostMcpServers.${name}.headers contains an invalid header`);\n }\n headerEntries.push([key, headerValue]);\n }\n headers = Object.fromEntries(headerEntries);\n }\n if (\n spec.timeout !== undefined &&\n (typeof spec.timeout !== \"number\" || !Number.isFinite(spec.timeout) || spec.timeout <= 0)\n ) {\n throw new TypeError(`hostMcpServers.${name}.timeout must be a positive number`);\n }\n parsedEntries.push([\n name,\n {\n type: spec.type,\n url: url.toString(),\n ...(headers ? { headers } : {}),\n ...(spec.timeout !== undefined ? { timeout: spec.timeout } : {}),\n },\n ]);\n }\n return Object.fromEntries(parsedEntries);\n}\n\nexport interface RuntimeGatewayTurnInput {\n topicId: string;\n /** Canonical principal on the execution node. */\n userId: string;\n /** Product-side actor, when it differs from the execution principal. */\n actorUserId?: string;\n /**\n * The node topic ids `actorUserId` may reach on the receiving node, as the\n * host's membership store sees it. Omitted by hosts that predate it; the\n * node then confines the turn's cross-room tools to the current room.\n */\n actorTopicScope?: ActorTopicScope;\n /**\n * Hub-issued per-turn authority for remote (`node/topic`) session-comm:\n * `{ hubUrl, capability }`. `hubUrl` must be a loopback `http://` or any\n * `https://` origin (≤ 512 chars); `capability` an opaque `rsc1.` bearer\n * (≤ 2 KiB) the node presents back to the hub and cannot verify — it holds\n * no key. It does base64-decode the payload's `e` (expiry) for one purpose,\n * ordering grants when several requests fold into one turn\n * (`remoteSessionGrantExpiry`); that value is untrusted and takes part in no\n * security decision. Omitted\n * by hosts that predate it or have the feature off; the node then keeps\n * remote session-comm fail-closed on the `otium` surface. Malformed → 400.\n */\n remoteSession?: RemoteSessionGrant;\n actorLabel?: string;\n vaultUserId?: string;\n /** Adapter provenance recorded on the canonical message. */\n sourceAdapter?: string;\n text: string;\n clientMessageId: string;\n requestId?: string;\n allowAutoContinue?: boolean;\n threadRootId?: string;\n /** Inline quote-reply target. Mutually exclusive with `threadRootId`. */\n parentId?: string;\n respond?: boolean;\n /** Run without adding the injected prompt or output to the visible transcript. */\n silent?: boolean;\n attachments?: string[];\n visualTools?: boolean;\n fileDeliveryTools?: boolean;\n /** Host-owned remote MCP servers granted to this manager topic. */\n hostMcpServers?: Record<string, HostMcpServerSpec>;\n}\n\nexport interface RuntimeGatewayTurnAcknowledgement {\n ok: true;\n v: typeof RUNTIME_GATEWAY_VERSION;\n accepted: true;\n deduplicated: boolean;\n requestId: string;\n clientMessageId: string;\n topicId: string;\n messageId: string;\n /** Canonical persisted message when the Node supports inline ACK materialization. */\n message?: MessageDto;\n cursor: number;\n}\n\nexport interface RuntimeGatewayTopicDeriveInput {\n userId: string;\n copyHistory: boolean;\n name?: string;\n}\n\nexport interface RuntimeGatewayDerivedTopic {\n id: string;\n title: string;\n kind?: string;\n agent?: string;\n defaultModel?: string;\n defaultEffort?: string;\n parentTopicId?: string;\n isFork?: boolean;\n}\n\nexport interface RuntimeGatewaySseEvent {\n id?: number;\n event: string;\n data: unknown;\n}\n\nexport interface RuntimeGatewayTopicUsage {\n queries: number;\n inputTokens: number;\n outputTokens: number;\n cacheCreationInputTokens: number;\n cacheReadInputTokens: number;\n estimatedCostUsd: number;\n}\n\nexport interface RuntimeGatewayManagerTopic {\n id: string;\n title: string;\n kind: \"manager\";\n agent?: \"claude\" | \"codex\" | \"maestro\";\n participants: Array<{ userId: string; role: \"owner\" | \"member\" }>;\n}\n\nexport interface RuntimeGatewayDeletedMessage {\n id: string;\n topicId: string;\n authorId: string;\n text: string;\n deleted: true;\n createdAt: string;\n}\n\nexport interface RuntimeGatewayCronJob {\n id: string;\n name: string;\n executionPrincipalUserId: string;\n actorOwnerUserId: string;\n topicId: string;\n source: \"prompt\" | \"script\";\n script: string | null;\n scriptExists: boolean;\n prompt: string | null;\n promptPreview: string | null;\n summary: string | null;\n schedule: string;\n timezone: string | null;\n enabled: boolean;\n agent: \"claude\" | \"codex\" | \"maestro\" | null;\n model: string | null;\n effort: string | null;\n nextRunAt: string;\n runCount: number;\n lastRun: Record<string, unknown> | null;\n createdAt: string;\n updatedAt: string;\n canMutate: boolean;\n}\n\nexport interface RuntimeGatewayCronCreateInput {\n userId: string;\n actorUserId: string;\n topicId: string;\n name: string;\n script: string;\n schedule: string;\n timezone?: string;\n agent?: \"claude\" | \"codex\" | \"maestro\";\n model?: string;\n effort?: string;\n}\n\nexport interface RuntimeGatewayCronPatch {\n name?: string;\n topicId?: string;\n script?: string;\n schedule?: string;\n timezone?: string | null;\n enabled?: boolean;\n agent?: \"claude\" | \"codex\" | \"maestro\" | null;\n model?: string | null;\n effort?: string | null;\n}\n\nexport type RuntimeGatewayToken = string | (() => string | Promise<string>);\nexport type RuntimeGatewayFetch = (\n input: string | URL | Request,\n init?: RequestInit,\n) => Promise<Response>;\n\nexport interface RuntimeGatewayClientOptions {\n baseUrl: string;\n token: RuntimeGatewayToken;\n /** Loopback control path or an authenticated relay forward prefix. */\n pathPrefix?: string;\n fetch?: RuntimeGatewayFetch;\n requestTimeoutMs?: number;\n compactTimeoutMs?: number;\n}\n\nfunction record(value: unknown): Record<string, unknown> | null {\n return value && typeof value === \"object\" && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : null;\n}\n\nasync function json(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n throw new RuntimeGatewayError(\n \"Runtime Gateway returned invalid JSON\",\n \"protocol\",\n response.status,\n );\n }\n}\n\nfunction validHealth(value: unknown): value is RuntimeGatewayHealth {\n const body = record(value);\n return Boolean(\n body?.ok === true &&\n body.v === RUNTIME_GATEWAY_VERSION &&\n Array.isArray(body.capabilities) &&\n typeof body.cursor === \"number\",\n );\n}\n\nfunction validAcknowledgedMessage(value: unknown): value is MessageDto {\n const message = record(value);\n return Boolean(\n message &&\n typeof message.id === \"string\" &&\n typeof message.topicId === \"string\" &&\n typeof message.authorId === \"string\" &&\n typeof message.text === \"string\" &&\n typeof message.createdAt === \"string\",\n );\n}\n\nfunction validTurnAck(value: unknown): value is RuntimeGatewayTurnAcknowledgement {\n const body = record(value);\n return Boolean(\n body?.ok === true &&\n body.v === RUNTIME_GATEWAY_VERSION &&\n body.accepted === true &&\n typeof body.deduplicated === \"boolean\" &&\n typeof body.requestId === \"string\" &&\n typeof body.clientMessageId === \"string\" &&\n typeof body.topicId === \"string\" &&\n typeof body.messageId === \"string\" &&\n (body.message === undefined || validAcknowledgedMessage(body.message)) &&\n typeof body.cursor === \"number\",\n );\n}\n\nfunction validManagerTopic(value: unknown): value is RuntimeGatewayManagerTopic {\n const topic = record(value);\n return Boolean(\n typeof topic?.id === \"string\" &&\n typeof topic.title === \"string\" &&\n topic.kind === \"manager\" &&\n Array.isArray(topic.participants),\n );\n}\n\nfunction validDeletedMessage(value: unknown): value is RuntimeGatewayDeletedMessage {\n const message = record(value);\n return Boolean(\n typeof message?.id === \"string\" &&\n typeof message.topicId === \"string\" &&\n typeof message.authorId === \"string\" &&\n typeof message.text === \"string\" &&\n message.deleted === true &&\n typeof message.createdAt === \"string\",\n );\n}\n\nfunction validCronJob(value: unknown): value is RuntimeGatewayCronJob {\n const job = record(value);\n return Boolean(\n typeof job?.id === \"string\" &&\n typeof job.name === \"string\" &&\n typeof job.executionPrincipalUserId === \"string\" &&\n typeof job.actorOwnerUserId === \"string\" &&\n typeof job.topicId === \"string\" &&\n typeof job.schedule === \"string\" &&\n typeof job.enabled === \"boolean\" &&\n typeof job.nextRunAt === \"string\" &&\n typeof job.canMutate === \"boolean\",\n );\n}\n\nexport async function* parseRuntimeGatewaySse(\n body: ReadableStream<Uint8Array>,\n): AsyncGenerator<RuntimeGatewaySseEvent> {\n const reader = body.getReader();\n const decoder = new TextDecoder();\n let buffer = \"\";\n let event = \"message\";\n let id: number | undefined;\n let data: string[] = [];\n const flush = (): RuntimeGatewaySseEvent | null => {\n if (data.length === 0) return null;\n const raw = data.join(\"\\n\");\n data = [];\n try {\n return { event, id, data: JSON.parse(raw) };\n } catch {\n throw new RuntimeGatewayError(\"Runtime Gateway SSE event has invalid JSON\", \"protocol\");\n } finally {\n event = \"message\";\n id = undefined;\n }\n };\n try {\n while (true) {\n const chunk = await reader.read();\n if (chunk.done) break;\n buffer += decoder.decode(chunk.value, { stream: true }).replace(/\\r\\n/g, \"\\n\");\n let newline = buffer.indexOf(\"\\n\");\n while (newline >= 0) {\n const line = buffer.slice(0, newline);\n buffer = buffer.slice(newline + 1);\n if (line === \"\") {\n const parsed = flush();\n if (parsed) yield parsed;\n } else if (line.startsWith(\"event:\")) {\n event = line.slice(6).trim() || \"message\";\n } else if (line.startsWith(\"id:\")) {\n const parsed = Number.parseInt(line.slice(3).trim(), 10);\n if (Number.isFinite(parsed)) id = parsed;\n } else if (line.startsWith(\"data:\")) {\n data.push(line.slice(5).trimStart());\n }\n newline = buffer.indexOf(\"\\n\");\n }\n }\n buffer += decoder.decode();\n if (buffer.startsWith(\"data:\")) data.push(buffer.slice(5).trimStart());\n const parsed = flush();\n if (parsed) yield parsed;\n } finally {\n reader.releaseLock();\n }\n}\n\nexport class RuntimeGatewayClient {\n private readonly pathPrefix: string;\n private readonly request: RuntimeGatewayFetch;\n private readonly requestTimeoutMs: number;\n private readonly compactTimeoutMs: number;\n\n constructor(private readonly options: RuntimeGatewayClientOptions) {\n this.pathPrefix = options.pathPrefix ?? RUNTIME_GATEWAY_CONTROL_PATH;\n this.request = options.fetch ?? fetch;\n this.requestTimeoutMs = options.requestTimeoutMs ?? 5_000;\n this.compactTimeoutMs = options.compactTimeoutMs ?? 5 * 60_000;\n }\n\n private endpoint(path: string): string {\n if (!this.options.baseUrl || !this.options.token) {\n throw new RuntimeGatewayError(\"Runtime Gateway is not configured\", \"config\");\n }\n return `${this.options.baseUrl.replace(/\\/+$/, \"\")}${this.pathPrefix}${path}`;\n }\n\n private async authorization(): Promise<string> {\n try {\n const token =\n typeof this.options.token === \"function\" ? await this.options.token() : this.options.token;\n return `Bearer ${token}`;\n } catch (error) {\n throw new RuntimeGatewayError(\n error instanceof Error ? error.message : \"Runtime Gateway token resolution failed\",\n \"config\",\n );\n }\n }\n\n private async send(path: string, init: RequestInit, timeoutMs?: number): Promise<Response> {\n const authorization = await this.authorization();\n try {\n return await this.request(this.endpoint(path), {\n ...init,\n headers: { authorization, ...init.headers },\n signal: timeoutMs === undefined ? init.signal : AbortSignal.timeout(timeoutMs),\n });\n } catch (error) {\n if (error instanceof DOMException && error.name === \"TimeoutError\") {\n throw new RuntimeGatewayError(\"Runtime Gateway request timed out\", \"timeout\");\n }\n throw new RuntimeGatewayError(\"Runtime Gateway request failed\", \"transport\");\n }\n }\n\n async health(): Promise<RuntimeGatewayHealth> {\n const response = await this.send(\"/health\", { method: \"GET\" }, this.requestTimeoutMs);\n if (!response.ok) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway health request failed\",\n \"http\",\n response.status,\n );\n }\n const body = await json(response);\n if (!validHealth(body)) {\n throw new RuntimeGatewayError(\"Runtime Gateway health response is incompatible\", \"protocol\");\n }\n return body;\n }\n\n async submitTurn(input: RuntimeGatewayTurnInput): Promise<RuntimeGatewayTurnAcknowledgement> {\n const response = await this.send(\n \"/turns\",\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input }),\n },\n this.requestTimeoutMs,\n );\n if (response.status !== 202) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway turn was not accepted\",\n \"http\",\n response.status,\n );\n }\n const body = await json(response);\n if (!validTurnAck(body)) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway turn acknowledgement is incompatible\",\n \"protocol\",\n );\n }\n return body;\n }\n\n async deriveTopic(\n topicId: string,\n input: RuntimeGatewayTopicDeriveInput,\n ): Promise<RuntimeGatewayDerivedTopic> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/derive`,\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input }),\n },\n this.requestTimeoutMs,\n );\n if (response.status !== 201) {\n throw new RuntimeGatewayError(\"Runtime Gateway topic derive failed\", \"http\", response.status);\n }\n const body = await json(response);\n const topic = record(body)?.topic;\n const topicRecord = record(topic);\n if (\n !topicRecord ||\n typeof topicRecord.id !== \"string\" ||\n typeof topicRecord.title !== \"string\"\n ) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway topic derive response is incompatible\",\n \"protocol\",\n );\n }\n return topic as unknown as RuntimeGatewayDerivedTopic;\n }\n\n async ensureManagerTopic(userId: string): Promise<RuntimeGatewayManagerTopic> {\n const response = await this.send(\n \"/manager-topic\",\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, userId }),\n },\n this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway manager topic request failed\",\n \"http\",\n response.status,\n );\n }\n const body = record(await json(response));\n if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION || !validManagerTopic(body.topic)) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway manager topic response is incompatible\",\n \"protocol\",\n );\n }\n return body.topic;\n }\n\n async deleteMessage(\n topicId: string,\n messageId: string,\n actorUserId: string,\n allowAdmin = false,\n ): Promise<RuntimeGatewayDeletedMessage> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/messages/${encodeURIComponent(messageId)}`,\n {\n method: \"DELETE\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n v: RUNTIME_GATEWAY_VERSION,\n actorUserId,\n ...(allowAdmin ? { allowAdmin: true } : {}),\n }),\n },\n this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway message delete failed\",\n \"http\",\n response.status,\n );\n }\n const body = record(await json(response));\n if (\n body?.ok !== true ||\n body.v !== RUNTIME_GATEWAY_VERSION ||\n !validDeletedMessage(body.message)\n ) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway message delete response is incompatible\",\n \"protocol\",\n );\n }\n return body.message;\n }\n\n private async cronRequest(path: string, init: RequestInit): Promise<Record<string, unknown>> {\n const response = await this.send(path, init, this.requestTimeoutMs);\n if (!response.ok) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron request failed\", \"http\", response.status);\n }\n const body = record(await json(response));\n if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron response is incompatible\", \"protocol\");\n }\n return body;\n }\n\n async listCronScripts(): Promise<string[]> {\n const body = await this.cronRequest(\"/cron/scripts\", { method: \"GET\" });\n if (!Array.isArray(body.scripts) || !body.scripts.every((value) => typeof value === \"string\")) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron scripts are incompatible\", \"protocol\");\n }\n return body.scripts;\n }\n\n async listCronJobs(\n userId: string,\n actorUserId: string,\n actorIsAdmin = false,\n ): Promise<RuntimeGatewayCronJob[]> {\n const search = new URLSearchParams({\n user: userId,\n actorUserId,\n actorIsAdmin: actorIsAdmin ? \"1\" : \"0\",\n });\n const body = await this.cronRequest(`/cron/jobs?${search}`, { method: \"GET\" });\n if (!Array.isArray(body.jobs) || !body.jobs.every(validCronJob)) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron jobs are incompatible\", \"protocol\");\n }\n return body.jobs;\n }\n\n async createCronJob(input: RuntimeGatewayCronCreateInput): Promise<RuntimeGatewayCronJob> {\n const body = await this.cronRequest(\"/cron/jobs\", {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, ...input }),\n });\n if (!validCronJob(body.job)) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron job is incompatible\", \"protocol\");\n }\n return body.job;\n }\n\n async updateCronJob(\n jobId: string,\n userId: string,\n actorUserId: string,\n patch: RuntimeGatewayCronPatch,\n actorIsAdmin = false,\n ): Promise<RuntimeGatewayCronJob> {\n const body = await this.cronRequest(`/cron/jobs/${encodeURIComponent(jobId)}`, {\n method: \"PATCH\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n v: RUNTIME_GATEWAY_VERSION,\n userId,\n actorUserId,\n actorIsAdmin,\n patch,\n }),\n });\n if (!validCronJob(body.job)) {\n throw new RuntimeGatewayError(\"Runtime Gateway cron job is incompatible\", \"protocol\");\n }\n return body.job;\n }\n\n async deleteCronJob(\n jobId: string,\n userId: string,\n actorUserId: string,\n actorIsAdmin = false,\n ): Promise<string> {\n const search = new URLSearchParams({\n user: userId,\n actorUserId,\n actorIsAdmin: actorIsAdmin ? \"1\" : \"0\",\n });\n const body = await this.cronRequest(`/cron/jobs/${encodeURIComponent(jobId)}?${search}`, {\n method: \"DELETE\",\n });\n if (typeof body.deleted !== \"string\") {\n throw new RuntimeGatewayError(\"Runtime Gateway cron delete is incompatible\", \"protocol\");\n }\n return body.deleted;\n }\n\n async *events(\n after: number,\n topicId?: string,\n signal?: AbortSignal,\n ): AsyncGenerator<RuntimeGatewaySseEvent> {\n const search = new URLSearchParams({ after: String(Math.max(0, after)) });\n if (topicId) search.set(\"topicId\", topicId);\n const response = await this.send(`/events?${search}`, { method: \"GET\", signal });\n if (!response.ok || !response.body) {\n throw new RuntimeGatewayError(\"Runtime Gateway event stream failed\", \"http\", response.status);\n }\n yield* parseRuntimeGatewaySse(response.body);\n }\n\n async getTopicUsage(topicId: string, userId: string): Promise<RuntimeGatewayTopicUsage> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/usage?user=${encodeURIComponent(userId)}`,\n { method: \"GET\" },\n this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway topic usage request failed\",\n \"http\",\n response.status,\n );\n }\n const usage = record(record(await json(response))?.usage);\n const read = (key: keyof RuntimeGatewayTopicUsage): number => {\n const value = usage?.[key];\n return typeof value === \"number\" && Number.isFinite(value) ? value : 0;\n };\n return {\n queries: read(\"queries\"),\n inputTokens: read(\"inputTokens\"),\n outputTokens: read(\"outputTokens\"),\n cacheCreationInputTokens: read(\"cacheCreationInputTokens\"),\n cacheReadInputTokens: read(\"cacheReadInputTokens\"),\n estimatedCostUsd: read(\"estimatedCostUsd\"),\n };\n }\n\n async abortTurn(topicId: string, userId: string): Promise<boolean> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/abort`,\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({ v: RUNTIME_GATEWAY_VERSION, userId }),\n },\n this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\"Runtime Gateway turn abort failed\", \"http\", response.status);\n }\n const body = record(await json(response));\n if (\n body?.ok !== true ||\n body.v !== RUNTIME_GATEWAY_VERSION ||\n typeof body.aborted !== \"boolean\"\n ) {\n throw new RuntimeGatewayError(\n \"Runtime Gateway turn abort response is incompatible\",\n \"protocol\",\n );\n }\n return body.aborted;\n }\n\n async resetSession(\n topicId: string,\n userId: string,\n reason?: string,\n actorUserId?: string,\n ): Promise<string | undefined> {\n return this.sessionCommand(topicId, userId, \"reset\", reason, actorUserId);\n }\n\n async compactSession(\n topicId: string,\n userId: string,\n reason?: string,\n actorUserId?: string,\n ): Promise<string | undefined> {\n return this.sessionCommand(topicId, userId, \"compact\", reason, actorUserId);\n }\n\n private async sessionCommand(\n topicId: string,\n userId: string,\n command: \"reset\" | \"compact\",\n reason?: string,\n actorUserId?: string,\n ): Promise<string | undefined> {\n const response = await this.send(\n `/topics/${encodeURIComponent(topicId)}/session/${command}`,\n {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\" },\n body: JSON.stringify({\n v: RUNTIME_GATEWAY_VERSION,\n userId,\n ...(actorUserId ? { actorUserId } : {}),\n ...(reason ? { reason } : {}),\n }),\n },\n command === \"compact\" ? this.compactTimeoutMs : this.requestTimeoutMs,\n );\n if (!response.ok) {\n throw new RuntimeGatewayError(\n `Runtime Gateway session ${command} failed`,\n \"http\",\n response.status,\n );\n }\n const body = record(await json(response));\n if (body?.ok !== true || body.v !== RUNTIME_GATEWAY_VERSION) {\n throw new RuntimeGatewayError(\n `Runtime Gateway session ${command} response is incompatible`,\n \"protocol\",\n );\n }\n return typeof body.result === \"string\" ? body.result : undefined;\n }\n}\n"
|
|
6
6
|
],
|
|
7
|
-
"mappings": "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",
|
|
7
|
+
"mappings": "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",
|
|
8
8
|
"debugId": "A92FEBD510CE277D64756E2164756E21",
|
|
9
9
|
"names": []
|
|
10
10
|
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"/**\n * bash-rs completion watcher — turns `result.json` files written by the Rust\n * `bash-rs` background-bash daemon into session-inbox `tell`s. bash-rs is the\n * only backend; the retired TypeScript server called `injectMessage()` itself.\n *\n * Why a watcher instead of bash-rs calling back into negotium: bash-rs is a\n * standalone project (github.com/maestrojeong/bash-rs-mcp, sibling of\n * browser-rs-mcp) that knows nothing about topics/session-inbox/turns — it\n * only ever appends `{bash_id}/meta.json` (on spawn) and `{bash_id}/result.json`\n * (on completion) under `BASHRS_SPILL_ROOT`, and never reads or deletes\n * either. Any consumer, negotium or otherwise, watches that directory the\n * same way `runtime/inbox.ts` already watches the session-inbox itself:\n * `fs.watch` + a fallback poll, at-least-once, dedup by a stable id.\n *\n * `owner` on the wire is `${userId}\\0${topicId}` — see\n * `platform/mcp-config.ts::backgroundBashTransport` for what negotium sends\n * as `X-Background-Bash-User`/`X-Background-Bash-Topic`, and bash-rs's own\n * `security.rs::resolve_identity` for the matching NUL-join on the other end.\n */\n\nimport {\n closeSync,\n openSync,\n readdirSync,\n readFileSync,\n readSync,\n renameSync,\n rmSync,\n statSync,\n} from \"node:fs\";\nimport { join } from \"node:path\";\nimport { debouncedFlush, FALLBACK_INTERVAL_MS, watchDir } from \"#outbox/utils\";\nimport { BASHRS_SPILL_ROOT } from \"#platform/config\";\nimport { logger } from \"#platform/logger\";\nimport {\n acquireRuntimeProcessLease,\n PROCESS_LEASE_HEARTBEAT_MS,\n type RuntimeProcessLeaseHandle,\n} from \"#storage/runtime-process-leases\";\nimport { enqueueSessionInbox } from \"#storage/session-inbox\";\n\n/**\n * Where a finished background job's turn is delivered.\n *\n * The default appends to negotium's own session inbox. An embedding host that\n * owns a different inbox installs its own sink here — otium, for one, resolves\n * `RUN_DIR` to its own state dir and runs its own inbox worker, so writing to\n * negotium's path would drop the turn on the floor rather than deliver it.\n * Mirrors the `setFileHooks()` seam in `runtime/file-hooks.ts`.\n *\n * A sink must throw to signal failure: the caller leaves `result.json` in\n * place so the next sweep retries, which is what keeps delivery at-least-once.\n */\nexport interface BashrsCompletion {\n userId: string;\n topicId: string;\n /** Stable per-job id; use it to collapse a retried delivery into one turn. */\n bashId: string;\n message: string;\n}\n\nexport type BashrsCompletionSink = (completion: BashrsCompletion) => void;\n\nconst defaultSink: BashrsCompletionSink = ({ userId, topicId, bashId, message }) => {\n enqueueSessionInbox({\n userId,\n topicId,\n id: `bashrs:${bashId}`,\n entry: {\n type: \"tell\",\n from: \"__bg_bash__\",\n message,\n depth: 0,\n requestId: bashId,\n timestamp: new Date().toISOString(),\n },\n });\n};\n\nlet completionSink: BashrsCompletionSink = defaultSink;\n\n/** Install a host's delivery sink, or pass null to restore the default. */\nexport function setBashrsCompletionSink(sink: BashrsCompletionSink | null): void {\n completionSink = sink ?? defaultSink;\n}\n\nconst PROCESS_ROLE = \"worker:bashrs-completions\";\n/** How long a delivered job's spill dir (logs + result.json.injected) is kept\n * around before being swept — mirrors the TS server's own\n * `COMPLETED_RETENTION_MS`, for the same reason: a reader might still be\n * mid-way through fetching a spill path out of the injected message. */\nconst RETENTION_MS = 15 * 60_000;\n/** Read at most this many trailing bytes of stdout.log/stderr.log into the\n * injected message — full output stays on disk at the path we report. */\nconst TAIL_BYTES = 8 * 1024;\n\ninterface JobResult {\n bash_id: string;\n owner: string;\n exit_code: number | null;\n finished_at_ms: number;\n matched_line: string | null;\n /**\n * Which arm ended a watch. Absent for a plain background run, and absent\n * from results written by bash-rs before v0.1.4 — those fall back to the\n * generic completion notice.\n */\n watch_outcome?: \"matched\" | \"timeout\" | \"exited\";\n unknown: boolean;\n}\n\nfunction isJobResult(value: unknown): value is JobResult {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Record<string, unknown>;\n return typeof v.bash_id === \"string\" && typeof v.owner === \"string\";\n}\n\nfunction readTail(filePath: string): { text: string; truncated: boolean } {\n try {\n const { size } = statSync(filePath);\n const fd = openSync(filePath, \"r\");\n try {\n const start = Math.max(0, size - TAIL_BYTES);\n const buf = Buffer.alloc(size - start);\n readSync(fd, buf, 0, buf.length, start);\n return { text: buf.toString(\"utf8\"), truncated: start > 0 };\n } finally {\n closeSync(fd);\n }\n } catch {\n return { text: \"\", truncated: false };\n }\n}\n\nfunction buildMessage(dir: string, result: JobResult): string {\n const stdout = readTail(join(dir, \"stdout.log\"));\n const stderr = readTail(join(dir, \"stderr.log\"));\n const parts: string[] = [];\n if (stdout.text.trim() || stdout.truncated) {\n parts.push(\n `stdout${stdout.truncated ? ` (truncated, full output: ${join(dir, \"stdout.log\")})` : \"\"}:\\n${stdout.text.trim()}`,\n );\n }\n if (stderr.text.trim() || stderr.truncated) {\n parts.push(\n `stderr${stderr.truncated ? ` (truncated, full output: ${join(dir, \"stderr.log\")})` : \"\"}:\\n${stderr.text.trim()}`,\n );\n }\n const header = watchHeader(result) ?? `[background_bash ${result.bash_id} finished]`;\n const exitLine = result.unknown\n ? \"exit code: unknown (bash-rs restarted while this job was running)\"\n : `exit code: ${result.exit_code ?? \"unknown\"}`;\n return `${header}\\n${exitLine}\\n${parts.join(\"\\n\") || \"(no output)\"}`;\n}\n\n/**\n * A watch promises exactly one turn, and which outcome produced it is the\n * whole point of the notice: \"timed out\" and \"exited before matching\" mean\n * opposite things about whether the condition is still coming.\n */\nfunction watchHeader(result: JobResult): string | null {\n if (result.matched_line) {\n return `[background_bash_watch ${result.bash_id} matched]\\nmatched line: ${result.matched_line.slice(0, 500)}`;\n }\n if (result.watch_outcome === \"timeout\") {\n return `[background_bash_watch ${result.bash_id} timed out without a match]`;\n }\n if (result.watch_outcome === \"exited\") {\n return `[background_bash_watch ${result.bash_id} exited before matching]`;\n }\n return null;\n}\n\nfunction parseOwner(owner: string): { userId: string; topicId: string } | null {\n const nul = owner.indexOf(\"\\0\");\n if (nul < 0) return null;\n const userId = owner.slice(0, nul);\n const topicId = owner.slice(nul + 1);\n if (!userId || !topicId) return null;\n return { userId, topicId };\n}\n\nfunction injectedMarker(dir: string): string {\n return join(dir, \"result.json.injected\");\n}\n\n/** One sweep: deliver any new `result.json`, then reap old delivered jobs. */\nexport async function flushBashrsCompletions(): Promise<void> {\n let entries: string[];\n try {\n entries = readdirSync(BASHRS_SPILL_ROOT);\n } catch {\n return; // Nothing spawned yet.\n }\n\n const now = Date.now();\n for (const bashId of entries) {\n const dir = join(BASHRS_SPILL_ROOT, bashId);\n const resultPath = join(dir, \"result.json\");\n const marker = injectedMarker(dir);\n\n // Already delivered: sweep once the retention window has passed.\n try {\n const markerStat = statSync(marker);\n if (now - markerStat.mtimeMs > RETENTION_MS) {\n rmSync(dir, { recursive: true, force: true });\n }\n continue;\n } catch {\n // No marker yet — fall through to try delivering it.\n }\n\n let raw: string;\n try {\n raw = readFileSync(resultPath, \"utf8\");\n } catch {\n continue; // Job still running, or not a bash-rs dir at all.\n }\n\n let result: unknown;\n try {\n result = JSON.parse(raw);\n } catch (err) {\n logger.warn({ err, dir }, \"bashrs-completions: corrupt result.json, skipping\");\n continue;\n }\n if (!isJobResult(result)) {\n logger.warn({ dir }, \"bashrs-completions: result.json missing bash_id/owner, skipping\");\n continue;\n }\n\n const parsed = parseOwner(result.owner);\n if (!parsed) {\n // Not negotium's convention (e.g. clawgram's bare-topic owner, or a\n // standalone curl user) — nothing for this watcher to do with it.\n continue;\n }\n\n try {\n // The stable bash_id is carried through so at-least-once delivery on\n // either side — this watcher crashing before the rename, or the sink's\n // own retry semantics — collapses to one turn rather than several.\n completionSink({\n userId: parsed.userId,\n topicId: parsed.topicId,\n bashId: result.bash_id,\n message: buildMessage(dir, result),\n });\n renameSync(resultPath, marker);\n logger.info(\n { bashId: result.bash_id, userId: parsed.userId, topicId: parsed.topicId },\n \"bashrs-completions: delivered\",\n );\n } catch (err) {\n // Leave result.json in place — the next sweep retries. At-least-once,\n // same as every other outbox in this codebase.\n logger.warn({ err, dir }, \"bashrs-completions: delivery failed, will retry\");\n }\n }\n}\n\nlet watcher: ReturnType<typeof watchDir> | null = null;\nlet fallbackTimer: ReturnType<typeof setInterval> | null = null;\nlet debouncedTrigger: (() => void) | null = null;\nlet leadershipTimer: ReturnType<typeof setInterval> | null = null;\nlet workerLease: RuntimeProcessLeaseHandle | null = null;\nlet workerStarted = false;\n\nfunction stopLeaderResources(): void {\n if (watcher) {\n watcher.close();\n watcher = null;\n }\n if (fallbackTimer) {\n clearInterval(fallbackTimer);\n fallbackTimer = null;\n }\n debouncedTrigger = null;\n}\n\nfunction tryBecomeLeader(): void {\n if (!workerStarted || workerLease) return;\n const acquired = acquireRuntimeProcessLease(PROCESS_ROLE, {\n onLost: () => {\n if (workerLease !== acquired) return;\n workerLease = null;\n stopLeaderResources();\n logger.warn(\"bashrs-completions: worker leadership lost\");\n },\n });\n if (!acquired) return;\n\n workerLease = acquired;\n debouncedTrigger = debouncedFlush(flushBashrsCompletions, \"bashrs-completions\", 200);\n watcher = watchDir(BASHRS_SPILL_ROOT, () => debouncedTrigger?.());\n fallbackTimer = setInterval(() => debouncedTrigger?.(), FALLBACK_INTERVAL_MS);\n fallbackTimer.unref?.();\n\n logger.info(\n { dir: BASHRS_SPILL_ROOT, role: PROCESS_ROLE },\n \"bashrs-completions: worker leadership acquired\",\n );\n void flushBashrsCompletions();\n}\n\n/**\n * Join the bashrs-completions worker election — same one-leader-per-role\n * lease as `startSessionInboxWorker`, so exactly one runtime process watches\n * `BASHRS_SPILL_ROOT` regardless of how many are up. Returns a cleanup\n * function for graceful shutdown.\n */\nexport function startBashrsCompletionsWorker(): () => void {\n if (workerStarted) return () => {};\n workerStarted = true;\n\n tryBecomeLeader();\n leadershipTimer = setInterval(tryBecomeLeader, PROCESS_LEASE_HEARTBEAT_MS);\n leadershipTimer.unref?.();\n\n return () => {\n if (!workerStarted) return;\n workerStarted = false;\n if (leadershipTimer) {\n clearInterval(leadershipTimer);\n leadershipTimer = null;\n }\n stopLeaderResources();\n workerLease?.stop();\n workerLease = null;\n logger.info(\"bashrs-completions: worker stopped\");\n };\n}\n",
|
|
15
15
|
"import { type FSWatcher, mkdirSync, watch } from \"node:fs\";\nimport { logger } from \"#platform/logger\";\n\nexport const FALLBACK_INTERVAL_MS = 5_000; // fallback poll every 5s in case fs.watch misses events\n\nexport interface OutboxWatchLogger {\n error(fields: Record<string, unknown>, message: string): void;\n warn(fields: Record<string, unknown>, message: string): void;\n}\n\nexport interface OutboxWatchHost {\n logger: OutboxWatchLogger;\n}\n\nexport interface OutboxWatchOps {\n debouncedFlush(fn: () => Promise<void>, label: string, delayMs: number): () => void;\n watchDir(dir: string, onChange: () => void): FSWatcher | null;\n}\n\nexport function createOutboxWatchOps(host: OutboxWatchHost): OutboxWatchOps {\n function debouncedFlush(fn: () => Promise<void>, label: string, delayMs: number) {\n let timer: ReturnType<typeof setTimeout> | null = null;\n let running = false;\n let pending = false;\n\n async function run(): Promise<void> {\n if (running) {\n pending = true;\n return;\n }\n while (true) {\n running = true;\n pending = false;\n try {\n await fn();\n } catch (e) {\n host.logger.error({ err: e }, `${label}: Unhandled error`);\n } finally {\n running = false;\n }\n if (!pending) break;\n }\n }\n\n return () => {\n if (timer) clearTimeout(timer);\n timer = setTimeout(() => {\n timer = null;\n run();\n }, delayMs);\n };\n }\n\n function watchDir(dir: string, onChange: () => void): FSWatcher | null {\n try {\n mkdirSync(dir, { recursive: true });\n const watcher = watch(dir, { recursive: true }, () => onChange());\n watcher.on(\"error\", (e) => {\n host.logger.warn({ err: e, dir }, \"outbox: fs.watch error\");\n });\n return watcher;\n } catch (e) {\n host.logger.warn({ err: e, dir }, \"outbox: Failed to watch dir\");\n return null;\n }\n }\n\n return { debouncedFlush, watchDir };\n}\n\nconst defaultWatchOps = createOutboxWatchOps({ logger });\n\nexport const debouncedFlush = defaultWatchOps.debouncedFlush;\nexport const watchDir = defaultWatchOps.watchDir;\n",
|
|
16
16
|
"import { execFileSync } from \"node:child_process\";\nimport { randomBytes } from \"node:crypto\";\nimport {\n accessSync,\n chmodSync,\n constants,\n existsSync,\n mkdirSync,\n readFileSync,\n writeFileSync,\n} from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { fileURLToPath, pathToFileURL } from \"node:url\";\nimport {\n parseRuntimePort,\n readEnvText,\n resolveFallbackAgent,\n safeRuntimePathSegment,\n} from \"#platform/config-helpers\";\nimport { logger } from \"#platform/logger\";\nimport { type AgentKind, EFFORT_VALUES, type EffortLevel } from \"#types\";\n\nexport function envText(envKey: string): string | undefined {\n return readEnvText(process.env, envKey);\n}\n\nconst HOME = homedir();\n\nfunction resolveStateDir(): string {\n const configured = envText(\"NEGOTIUM_STATE_DIR\");\n if (process.env.NODE_ENV === \"test\" && !configured) {\n throw new Error(\n \"NEGOTIUM_STATE_DIR must be set before importing #platform/config in test processes\",\n );\n }\n return configured ? resolve(configured) : resolve(HOME, \".negotium\");\n}\n\n// new URL(\"../..\", import.meta.url) causes webpack to treat \"../..\" as a module import.\n// Split into fileURLToPath → dirname → resolve to avoid that.\nfunction resolveProjectRoot(): string {\n const moduleDir = dirname(fileURLToPath(import.meta.url));\n const packagedRuntime = resolve(moduleDir, \"runtime\");\n if (existsSync(resolve(packagedRuntime, \"src\"))) return packagedRuntime;\n return resolve(moduleDir, \"../..\");\n}\n\nexport const PROJECT_ROOT = resolveProjectRoot();\n\n/** Resolve a dependency executable from either a package-local or hoisted install. */\n/**\n * Names a package-manager shim can carry in `node_modules/.bin`.\n *\n * POSIX gets one extension-less executable. Windows has no execute bit, so the\n * installer writes an extension instead — bun produces `<name>.exe` plus a\n * `<name>.bunx` stub, npm a `.cmd`/`.ps1` pair — and probing only the bare name\n * finds nothing, leaving the resolved path pointing at a file that does not\n * exist.\n */\nfunction dependencyBinNames(name: string): string[] {\n if (process.platform !== \"win32\") return [name];\n return [name, `${name}.exe`, `${name}.cmd`, `${name}.bat`, `${name}.bunx`];\n}\n\nfunction resolveDependencyBin(name: string): string {\n let dir = PROJECT_ROOT;\n while (true) {\n const binDir = resolve(dir, \"node_modules\", \".bin\");\n for (const candidate of dependencyBinNames(name)) {\n const path = resolve(binDir, candidate);\n if (existsSync(path)) return path;\n }\n const parent = dirname(dir);\n // Exhausted: hand back the canonical spelling so the failure names the\n // path that was expected.\n if (parent === dir) return resolve(binDir, name);\n dir = parent;\n }\n}\n\n// Each machine is one negotium node; all node state lives in one dotdir.\n// NEGOTIUM_STATE_DIR overrides (useful for tests and multi-node-on-one-box).\nexport const STATE_DIR = resolveStateDir();\n\nfunction resolveLocalStateDir(envKey: string, stateName: string): string {\n const envValue = envText(envKey);\n if (envValue) return resolve(envValue);\n return resolve(STATE_DIR, stateName);\n}\n\nfunction parsePortEnv(envValue: string | undefined, fallback: number): number {\n return parseRuntimePort(envValue, fallback);\n}\n\nexport const WORKSPACE_DIR = resolveLocalStateDir(\"NEGOTIUM_WORKSPACE_DIR\", \"workspace\");\nexport const TOPIC_WORKSPACE_DIR = resolve(WORKSPACE_DIR, \"topics\");\nexport const SHARED_WIKI_DIR = resolve(WORKSPACE_DIR, \"wiki\");\nexport const CRON_WORKSPACE_DIR = resolve(WORKSPACE_DIR, \"cron\");\nexport const BROWSER_DIR = resolveLocalStateDir(\"NEGOTIUM_BROWSER_DIR\", \"browser\");\nexport const BROWSER_PROFILES_DIR = resolve(BROWSER_DIR, \"profiles\");\nexport const BINARIES_DIR = resolve(STATE_DIR, \"binaries\");\nexport const SECRETS_DIR = resolve(STATE_DIR, \"secrets\");\n// Legacy names remain public, but their durable contents now live under data.\nexport const DM_WORKSPACE_DIR = resolve(STATE_DIR, \"data\", \"dm\");\nexport const SESSION_WORKSPACE_DIR = resolve(STATE_DIR, \"data\", \"sessions\");\n// The Claude Agent SDK ships a platform-matched Claude Code binary. Keep that\n// SDK/CLI pair together by default; only use an external executable when an\n// operator explicitly opts in. This avoids silently pairing an older SDK with\n// a newer globally installed Claude Code release.\nconst CLAUDE_EXECUTABLE_ENV = envText(\"NEGOTIUM_CLAUDE_EXECUTABLE\");\nexport const CLAUDE_EXECUTABLE = CLAUDE_EXECUTABLE_ENV ? resolve(CLAUDE_EXECUTABLE_ENV) : undefined;\n\n// Fallback output language for model-generated prose (topic/channel/manager\n// replies and the archiver's summaries, briefs, articles, completion reply).\n// Defaults to English; set `NEGOTIUM_LANG` to the user's mother tongue (e.g.\n// `Korean`, `ko`). The assistant still mirrors whatever language the user\n// writes in; `NEGOTIUM_MEMORY_LANG` can narrow just the archiver. Fixed system\n// chrome / degraded-path strings emitted by code stay English (can't translate\n// an arbitrary value at runtime).\nexport const DEFAULT_OUTPUT_LANGUAGE = \"English\";\n\nexport function resolveOutputLanguage(): string {\n const raw = envText(\"NEGOTIUM_LANG\")?.trim();\n return raw && raw.length > 0 ? raw : DEFAULT_OUTPUT_LANGUAGE;\n}\n\n/**\n * Suffix the managed Rust helpers carry on this platform.\n *\n * The installers write the binary under its plain name, which on Windows is\n * not executable — the release asset is `browser-rs.exe` and the resolver has\n * to look for exactly that. Empty everywhere else, so POSIX paths are\n * unchanged.\n */\nexport const BINARY_EXTENSION = process.platform === \"win32\" ? \".exe\" : \"\";\n\n/** Browser.rs release tested with this Negotium version. */\nexport const BROWSER_RS_VERSION = \"v0.6.0\";\n/** Require the authenticated listener and the current Browser.rs tool contract. */\nexport const BROWSER_RS_MIN_SECURE_VERSION = \"0.2.1\";\n\nfunction versionAtLeast(actualVersion: string, minimumVersion: string): boolean {\n const actual = actualVersion.split(\".\").map(Number);\n const minimum = minimumVersion.split(\".\").map(Number);\n if (actual.some(Number.isNaN) || minimum.some(Number.isNaN)) return false;\n for (let index = 0; index < minimum.length; index += 1) {\n if ((actual[index] ?? 0) > (minimum[index] ?? 0)) return true;\n if ((actual[index] ?? 0) < (minimum[index] ?? 0)) return false;\n }\n return true;\n}\n\n/**\n * Ceiling for the `--version` probe, not a latency expectation.\n *\n * The probe exists so a wedged binary cannot hang startup; a healthy one\n * answers in milliseconds. At 2s it also failed whenever the machine was merely\n * busy — process startup under a loaded test suite regularly exceeds that —\n * which turned a correctness check into a load measurement and made\n * `resolveBrowserRsBin` intermittently report a perfectly good binary as\n * unusable. Generous enough that only a genuinely stuck process trips it.\n */\nconst BROWSER_RS_VERSION_PROBE_TIMEOUT_MS = 15_000;\n\nfunction browserRsMeetsMinimumVersion(candidate: string): boolean {\n try {\n const output = execFileSync(candidate, [\"--version\"], {\n encoding: \"utf8\",\n timeout: BROWSER_RS_VERSION_PROBE_TIMEOUT_MS,\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n // This module is imported by every CLI invocation, so on Windows the\n // probe blinked a console window each time `negotium` was run. Ignored\n // on POSIX.\n windowsHide: true,\n }).trim();\n const match = output.match(/^browser-rs (\\d+)\\.(\\d+)\\.(\\d+)$/);\n if (!match) return false;\n return versionAtLeast(match.slice(1).join(\".\"), BROWSER_RS_MIN_SECURE_VERSION);\n } catch {\n return false;\n }\n}\n\n/**\n * Resolve the preferred Browser.rs engine without consulting PATH. The\n * versioned private location keeps different Negotium releases reproducible\n * and avoids changing a user's global browser-rs installation.\n */\nexport function resolveBrowserRsBin(envValue?: string): string | undefined {\n const override = envValue?.trim();\n if (\n !override &&\n !versionAtLeast(BROWSER_RS_VERSION.replace(/^v/, \"\"), BROWSER_RS_MIN_SECURE_VERSION)\n ) {\n return undefined;\n }\n const candidate = override\n ? resolve(override)\n : resolve(BINARIES_DIR, \"browser-rs\", BROWSER_RS_VERSION, `browser-rs${BINARY_EXTENSION}`);\n try {\n accessSync(candidate, constants.X_OK);\n return browserRsMeetsMinimumVersion(candidate) ? candidate : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Backend that stores documents published by `publish_html` and serves them\n * at `<base>/snippets/<id>`. Unset means the deployment has nowhere to\n * publish, and the publish tools are omitted from the catalog rather than\n * offering a link that cannot be minted.\n */\nexport const SNIPPETS_API_URL = (\n envText(\"NEGOTIUM_SNIPPETS_API_URL\") ??\n envText(\"SNIPPETS_API_URL\") ??\n \"\"\n).replace(/\\/+$/, \"\");\n\nexport const BROWSER_RS_BIN = resolveBrowserRsBin(envText(\"NEGOTIUM_BROWSER_RS_BIN\"));\n\n/** bash-rs release tested with this Negotium version — see apps/negotium/install-bash-rs.mjs. */\nexport const BASH_RS_VERSION = \"v0.1.10\";\n\n/**\n * Resolve the bash-rs binary the same way `resolveBrowserRsBin` resolves\n * Browser.rs: a versioned private location, no PATH lookup, `undefined`\n * (rather than throwing) when it's missing. There is no substitute: the manager\n * throws when a turn tries to prepare background-bash without a binary (see\n * `background-bash/manager.ts`), the turn runner catches that and continues\n * with a reminder that the tools are gone, so background-bash is simply\n * unavailable on a platform without a prebuilt binary.\n */\nexport function resolveBashRsBin(envValue?: string): string | undefined {\n const override = envValue?.trim();\n const candidate = override\n ? resolve(override)\n : resolve(BINARIES_DIR, \"bash-rs\", BASH_RS_VERSION, `bash-rs${BINARY_EXTENSION}`);\n try {\n accessSync(candidate, constants.X_OK);\n return candidate;\n } catch {\n return undefined;\n }\n}\n\nexport const BASH_RS_BIN = resolveBashRsBin(envText(\"NEGOTIUM_BASH_RS_BIN\"));\n\n// Managed Browser.rs terminates MCP transports and security policy itself.\nexport function resolveBrowserMcpBin(envValue?: string): string {\n const override = envValue?.trim();\n if (override) return resolve(override);\n return (\n BROWSER_RS_BIN ??\n resolve(BINARIES_DIR, \"browser-rs\", BROWSER_RS_VERSION, `browser-rs${BINARY_EXTENSION}`)\n );\n}\n\nexport const PLAYWRIGHT_MCP_BIN = resolveBrowserMcpBin(envText(\"NEGOTIUM_BROWSER_MCP_BIN\"));\n\n// --- Browser egress proxy ---\n//\n// On a datacenter host (AWS) the browser's egress IP is a known cloud range,\n// so anti-bot services (Cloudflare, DataDome, reCAPTCHA) challenge or block it\n// far more than a residential IP would. Routing the automation browser through\n// a residential/ISP proxy moves the egress IP out of the datacenter range.\n//\n// Operators set BROWSER_PROXY_URL, e.g. http://user:pass@proxy.host:8080 or\n// socks5://proxy.host:1080. Credentials in the URL are split out because\n// Playwright takes them as separate fields. BROWSER_PROXY_BYPASS is an optional\n// comma-separated no-proxy list (e.g. \"localhost,127.0.0.1,*.internal\").\n//\n// NOTE: Chromium does not support authentication for SOCKS proxies — put\n// credentials only on http/https proxy URLs.\nexport type BrowserProxyConfig = {\n server: string;\n username?: string;\n password?: string;\n bypass?: string;\n};\n\nexport function resolveBrowserProxy(): BrowserProxyConfig | null {\n const raw = envText(\"BROWSER_PROXY_URL\");\n if (!raw) return null;\n let url: URL;\n try {\n url = new URL(raw);\n } catch {\n logger.warn({ raw }, \"Ignoring malformed BROWSER_PROXY_URL\");\n return null;\n }\n // Playwright wants the server without embedded credentials.\n const server = `${url.protocol}//${url.host}`;\n const proxy: BrowserProxyConfig = { server };\n if (url.username) proxy.username = decodeURIComponent(url.username);\n if (url.password) proxy.password = decodeURIComponent(url.password);\n const bypass = envText(\"BROWSER_PROXY_BYPASS\");\n if (bypass) proxy.bypass = bypass;\n return proxy;\n}\n\n// --- Node/tsx runtime for the `codex` agent's MCP servers ---\n//\n// codex 0.135's rmcp stdio MCP client cannot reliably complete the initialize\n// handshake with servers spawned via `bun` (the JSON-RPC initialize is\n// dropped/raced and no tools ever reach the model). Pure-node servers connect\n// reliably, so codex turns launch the SAME .ts servers via node + tsx instead\n// of `bun run`. claude/maestro keep using `bun run` (fast, native, unaffected).\n//\n// tsx transpiles .ts on the fly and resolves the `@/*` tsconfig path aliases,\n// but only when it can find the tsconfig — and MCP servers run with cwd set to\n// the user's workspace dir, not PROJECT_ROOT — so we pass TSX_TSCONFIG_PATH\n// explicitly via env. Requires package.json `\"type\": \"module\"` so the servers'\n// top-level `await` loads as ESM under node.\nexport const TSX_BIN = resolveDependencyBin(\"tsx\");\n/**\n * In-process tsx loader used by Node MCP entrypoints (avoids the tsx CLI child\n * process).\n *\n * `node --import` takes a module specifier, not a filesystem path. A POSIX\n * absolute path happens to be a valid one; a Windows `C:\\…` is not — Node reads\n * the drive letter as a URL scheme and refuses it with\n * ERR_UNSUPPORTED_ESM_URL_SCHEME, which took out every tsx-hosted MCP server\n * (the Codex tool surface) at spawn. Hand it a `file://` URL there, and leave\n * the POSIX value exactly as it was.\n */\nconst TSX_LOADER_PATH = createRequire(import.meta.url).resolve(\"tsx\");\nexport const TSX_LOADER =\n process.platform === \"win32\" ? pathToFileURL(TSX_LOADER_PATH).href : TSX_LOADER_PATH;\nexport const TSCONFIG_PATH = resolve(PROJECT_ROOT, \"tsconfig.json\");\n\nexport const SESSION_COMM_SERVER = resolve(PROJECT_ROOT, \"src/mcp/session-comm/server.ts\");\n\nexport const TASK_SERVER = resolve(PROJECT_ROOT, \"src/mcp/task-server.ts\");\nexport const DECISION_SERVER = resolve(PROJECT_ROOT, \"src/mcp/decision-server.ts\");\nexport const CANONICAL_MCP_PROXY_SERVER = resolve(\n PROJECT_ROOT,\n \"src/mcp/canonical-proxy-server.ts\",\n);\n\nexport const WIKI_SERVER = resolve(PROJECT_ROOT, \"src/mcp/wiki-server.ts\");\n\nexport const TOKEN_STATS_SERVER = resolve(PROJECT_ROOT, \"src/mcp/token-stats-server.ts\");\n\nexport const COMPACTION_LOG_SERVER = resolve(PROJECT_ROOT, \"src/mcp/compaction-log-server.ts\");\n\nexport const SYSTEM_HEALTH_SERVER = resolve(PROJECT_ROOT, \"src/mcp/system-health-server.ts\");\n\nexport const AGENT_HEALTH_SERVER = resolve(PROJECT_ROOT, \"src/mcp/agent-health-server.ts\");\n\nexport const VAULT_SERVER = resolve(PROJECT_ROOT, \"src/mcp/vault-server.ts\");\n\nexport const BG_BASH_BASE_PORT = parsePortEnv(process.env.BG_BASH_BASE_PORT, 9700);\nexport const BG_BASH_MAX_PORT = parsePortEnv(process.env.BG_BASH_MAX_PORT, 9799);\n\nfunction safeWorkspaceSegment(value: string, fallback: string): string {\n return safeRuntimePathSegment(value, fallback);\n}\n\n/** Resolve the shared filesystem workspace for an API topic. */\nexport function resolveTopicWorkspaceDir(topicId: string): string {\n return join(TOPIC_WORKSPACE_DIR, safeWorkspaceSegment(topicId, \"topic\"));\n}\n\nexport function isProductionEnv(): boolean {\n return process.env.NODE_ENV === \"production\";\n}\n\nfunction loadOrCreateLocalSecret(\n envKey: string,\n filename: string,\n options: { persistEnvValue?: boolean } = {},\n): string {\n const envValue = envText(envKey);\n const secretFile = resolve(SECRETS_DIR, filename);\n mkdirSync(dirname(secretFile), { recursive: true });\n if (envValue) {\n if (options.persistEnvValue) {\n writeFileSync(secretFile, `${envValue}\\n`, { mode: 0o600 });\n chmodSync(secretFile, 0o600);\n }\n return envValue;\n }\n\n if (existsSync(secretFile)) {\n const stored = readFileSync(secretFile, \"utf-8\").trim();\n if (stored) {\n chmodSync(secretFile, 0o600);\n return stored;\n }\n }\n\n const secret = randomBytes(32).toString(\"base64url\");\n try {\n writeFileSync(secretFile, `${secret}\\n`, { mode: 0o600, flag: \"wx\" });\n return secret;\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") throw error;\n const stored = readFileSync(secretFile, \"utf-8\").trim();\n if (!stored) throw new Error(`Secret file exists but is empty: ${secretFile}`);\n chmodSync(secretFile, 0o600);\n return stored;\n }\n}\n\nexport const RUNTIME_MCP_SECRET = loadOrCreateLocalSecret(\n \"RUNTIME_MCP_SECRET\",\n \"runtime-mcp-secret\",\n);\n/** Local bearer token for the loopback node-control API. */\nexport const NODE_CONTROL_TOKEN = loadOrCreateLocalSecret(\n \"NEGOTIUM_CONTROL_TOKEN\",\n \"node-control-token\",\n);\n/**\n * Stable identity of this node's store, minted once and kept across restarts.\n *\n * Deliberately not a secret: it is published in the gateway health response so a\n * host can tell \"the node I recorded a mapping against\" apart from \"whatever is\n * answering on that port today\". Without it, pointing a host at a different node\n * makes every existing mapping look like a topic whose owner withdrew it, and the\n * same topics come back as duplicate rooms. It shares the secrets directory only\n * because that is already where per-install state that must survive restarts\n * lives; the stricter file mode costs nothing.\n */\nexport const NODE_ID = loadOrCreateLocalSecret(\"NEGOTIUM_NODE_ID\", \"node-id\");\nexport const VAULT_MASTER_KEY = loadOrCreateLocalSecret(\n \"NEGOTIUM_VAULT_MASTER_KEY\",\n \"vault-master-key\",\n { persistEnvValue: true },\n);\n// Agent/tool subprocesses inherit process.env. Keep the loaded key in this\n// process only so `env`/`ps` inside an agent workspace cannot reveal it.\ndelete process.env.NEGOTIUM_VAULT_MASTER_KEY;\n\n/** The node's single open port: runtime MCP endpoint + node API. */\nexport const NEGOTIUM_PORT = parseInt(process.env.NEGOTIUM_PORT || \"7777\", 10);\nexport const hostname = process.env.HOSTNAME || \"127.0.0.1\";\n\n// Persistent state (survives restarts, long-lived)\nexport const DATA_DIR = resolveLocalStateDir(\"NEGOTIUM_DATA_DIR\", \"data\");\nexport const LOG_DIR = resolveLocalStateDir(\"NEGOTIUM_LOG_DIR\", \"logs\");\nexport const UPLOADS_DIR = resolve(DATA_DIR, \"uploads\");\nexport const VAULT_DIR = resolve(DATA_DIR, \"vault\");\n// SESSIONS_DB_PATH env override lets tests point the DB singleton at a temp file.\nexport const SESSIONS_DB = process.env.SESSIONS_DB_PATH\n ? resolve(process.env.SESSIONS_DB_PATH)\n : resolve(DATA_DIR, \"sessions.db\");\nexport const DEBUG_FILE = resolve(DATA_DIR, \"debug-users.json\");\nexport const USERS_LOG_DIR = resolve(DATA_DIR, \"users\");\n\n// Runtime IPC queues (transient, safe to clear on restart)\n/** @deprecated Use the runtime layout name; NEGOTIUM_RUN_DIR remains a compatibility override. */\nexport const RUN_DIR = resolveLocalStateDir(\"NEGOTIUM_RUN_DIR\", \"runtime\");\nexport const RUNTIME_DIR = RUN_DIR;\nexport const PROGRESS_DIR = resolve(RUN_DIR, \"progress\");\nexport const DM_CMD_DIR = resolve(RUN_DIR, \"dm-commands\");\nexport const DM_RESP_DIR = resolve(RUN_DIR, \"dm-responses\");\nexport const SESSION_INBOX_DIR = resolve(RUN_DIR, \"session-inbox\");\nexport const SESSION_ASKS_DIR = resolve(RUN_DIR, \"session-asks\");\n/**\n * Passed to `bash-rs` as `BASHRS_SPILL_ROOT`. Each job gets a subdirectory\n * here (`{bash_id}/meta.json`, `result.json`, `stdout.log`, `stderr.log`) —\n * see bash-rs-mcp's `journal.rs`. `runtime/bashrs-completions.ts` watches\n * this directory and turns `result.json` into a session-inbox `tell`. The\n * retired TypeScript server used to write one directly instead.\n */\nexport const BASHRS_SPILL_ROOT = resolve(RUN_DIR, \"bashrs\");\nexport const PLAYWRIGHT_BASE_PORT = parsePortEnv(process.env.PLAYWRIGHT_BASE_PORT, 9100);\nexport const PLAYWRIGHT_MAX_PORT = parsePortEnv(process.env.PLAYWRIGHT_MAX_PORT, 9499);\nexport const PLAYWRIGHT_PORTS_DIR = resolve(RUN_DIR, \"playwright-ports\");\nmkdirSync(STATE_DIR, { recursive: true });\nmkdirSync(DATA_DIR, { recursive: true });\nmkdirSync(UPLOADS_DIR, { recursive: true });\nmkdirSync(VAULT_DIR, { recursive: true, mode: 0o700 });\nmkdirSync(LOG_DIR, { recursive: true });\nmkdirSync(PROGRESS_DIR, { recursive: true });\nmkdirSync(DM_CMD_DIR, { recursive: true });\nmkdirSync(DM_RESP_DIR, { recursive: true });\nmkdirSync(SESSION_INBOX_DIR, { recursive: true });\nmkdirSync(SESSION_ASKS_DIR, { recursive: true });\nmkdirSync(PLAYWRIGHT_PORTS_DIR, { recursive: true });\nmkdirSync(WORKSPACE_DIR, { recursive: true });\nmkdirSync(TOPIC_WORKSPACE_DIR, { recursive: true });\nmkdirSync(SHARED_WIKI_DIR, { recursive: true });\nmkdirSync(CRON_WORKSPACE_DIR, { recursive: true });\nmkdirSync(DM_WORKSPACE_DIR, { recursive: true });\nmkdirSync(SESSION_WORKSPACE_DIR, { recursive: true });\nmkdirSync(BROWSER_DIR, { recursive: true });\nmkdirSync(BROWSER_PROFILES_DIR, { recursive: true });\nmkdirSync(BINARIES_DIR, { recursive: true });\nmkdirSync(SECRETS_DIR, { recursive: true, mode: 0o700 });\n\n/** Stale threshold for active-query state files (crash recovery) */\nexport const ACTIVE_QUERY_STALE_MS = 10 * 60 * 1000; // 10 minutes\n\nexport const AGENTS_PROMPTS_DIR = resolve(PROJECT_ROOT, \"src/prompts/agents\");\nexport const RESOURCES_DIR = resolve(PROJECT_ROOT, \"src/resources\");\n\n/** Returns process.env without CLAUDECODE, to prevent nested claude-code detection in subprocesses. */\nexport function getCleanEnv(): NodeJS.ProcessEnv {\n const env = { ...process.env };\n delete env.CLAUDECODE;\n return env;\n}\n\n/**\n * How an absolute path can start.\n *\n * A POSIX root (`/`) is not the only form once the runtime also runs on\n * Windows, where the same tag carries `C:\\…` or a `\\\\server\\share` UNC path.\n * Anchoring on `/` alone silently dropped every Windows attachment: the tag\n * stayed in the visible text and no file was ever sent.\n */\nconst ABSOLUTE_PATH_PREFIX = String.raw`(?:[A-Za-z]:[\\\\/]|\\\\\\\\|\\/)`;\n\nexport const FILE_EXTENSIONS_REGEX = new RegExp(\n `(?:${ABSOLUTE_PATH_PREFIX}[^\\\\s\"'<>|*?[\\\\]]+\\\\.(?:png|jpg|jpeg|gif|webp|svg|pdf|csv|xlsx|xls|json|txt|md|html|zip|py|js|ts|tsx|jsx|css|xml|yaml|yml|docx|pptx))`,\n \"gi\",\n);\n\nexport const FILE_TAG_REGEX = new RegExp(`\\\\[FILE:(${ABSOLUTE_PATH_PREFIX}[^\\\\]]+)\\\\]`, \"gi\");\n\n// Canonical Claude model IDs — update here when Anthropic releases new versions\nexport const MODEL_SONNET = \"claude-sonnet-5\";\nexport const MODEL_OPUS = \"claude-opus-5-5\";\nexport const MODEL_HAIKU = \"claude-haiku-4-5-20251001\";\nexport const MODEL_FABLE = \"claude-fable-5-1\"; // Mythos-class, Fable 5.1 announced 2026-09-01 (was claude-fable-5)\n\n// DeepSeek V4 (released 2026-04-24). API is OpenAI-compatible at\n// https://api.deepseek.com/v1/chat/completions; thinking mode is enabled via\n// `extra_body.thinking.type` + `reasoning_effort`. Legacy `deepseek-chat` /\n// `deepseek-reasoner` are deprecated 2026-07-24.\nexport const MODEL_DEEPSEEK_V4_PRO = \"deepseek-v4-pro\";\nexport const MODEL_DEEPSEEK_V4_FLASH = \"deepseek-v4-flash\";\n\n// Single agent + model default for the whole node. DEFAULT_* remains a legacy alias.\nexport const FALLBACK_AGENT: AgentKind = resolveFallbackAgent(process.env);\nexport const FALLBACK_MODEL = envText(\"FALLBACK_MODEL\") ?? envText(\"DEFAULT_MODEL\");\n\n/**\n * Effort used when a caller (or an archiver assignment) names a model but no\n * effort. Deliberately one fixed node-wide value instead of each agent's\n * registry default: a model-derived agent switch would otherwise move effort —\n * and therefore cost — without anyone asking for it.\n */\nexport const DEFAULT_TOPIC_EFFORT: EffortLevel = ((): EffortLevel => {\n const raw = envText(\"NEGOTIUM_DEFAULT_EFFORT\")?.toLowerCase();\n return raw && (EFFORT_VALUES as readonly string[]).includes(raw)\n ? (raw as EffortLevel)\n : \"medium\";\n})();\n\n// ── External tool binaries + media pipeline env ───────────────────\n// (src/media/* 에서 사용. 미설정 시 fallback 의미는 기존 그대로:\n// FFMPEG_BIN은 text-extractor에서 필수(undefined면 spawn 시점 실패),\n// video.ts에서는 PATH의 ffmpeg/ffprobe로 fallback.)\nexport const FFMPEG_BIN = envText(\"FFMPEG_BIN\");\nexport const FFPROBE_BIN = envText(\"FFPROBE_BIN\");\nexport const PYTHON_BIN = envText(\"PYTHON_BIN\") ?? \"python3\";\nexport const FASTER_WHISPER_WRAPPER =\n envText(\"FASTER_WHISPER_WRAPPER\") ?? resolve(PROJECT_ROOT, \"scripts/faster-whisper-wrapper.py\");\nexport const WHISPER_MODEL = envText(\"WHISPER_MODEL_FILE\") ?? \"turbo\";\nexport const TESSERACT_BIN = envText(\"TESSERACT_BIN\") ?? \"tesseract\";\nexport const PDFTOTEXT_BIN = envText(\"PDFTOTEXT_BIN\") ?? \"pdftotext\";\n\n// ── Whisper backend selection ──────────────────────────────────────\n// Different hosts want different whisper runtimes: Apple Silicon Macs get\n// real acceleration from mlx-whisper (see clawgram's transcribe.ts, which\n// this mirrors), while Linux/Ubuntu hubs have no MLX and use the CPU-friendly\n// faster-whisper wrapper instead. Both CLIs accept the same\n// `INPUT --model M --language L --output-dir D --output-format txt` shape\n// and write `{basename}.txt`, so extractFromAudio only needs to pick which\n// binary/args to use — no per-backend parsing logic.\nexport type WhisperBackend = \"faster-whisper\" | \"mlx-whisper\";\nfunction resolveWhisperBackend(): WhisperBackend {\n return envText(\"WHISPER_BACKEND\") === \"mlx-whisper\" ? \"mlx-whisper\" : \"faster-whisper\";\n}\nexport const WHISPER_BACKEND = resolveWhisperBackend();\n/** Path to the `mlx_whisper` CLI (only used when WHISPER_BACKEND=mlx-whisper). */\nexport const MLX_WHISPER_BIN = envText(\"MLX_WHISPER_BIN\");\n/** mlx-whisper model id, e.g. \"mlx-community/whisper-large-v3-turbo\".\n *\n * Deliberately does NOT fall back to WHISPER_MODEL: that env defaults to the\n * bare alias \"turbo\", which faster-whisper's own model-name normalization\n * accepts but mlx_whisper's `--model` does not — it expects a full\n * HuggingFace repo id and otherwise fails hard with a 401\n * RepositoryNotFoundError trying to resolve \"turbo\" as a repo. Confirmed by\n * actually running mlx_whisper with each value while wiring up the\n * mlx-whisper hub in production. */\nexport const WHISPER_MODEL_MLX =\n envText(\"WHISPER_MODEL_MLX\") ?? \"mlx-community/whisper-large-v3-turbo\";\n\n// Max tell_session relay depth from origin user. ask_session forks reset to\n// depth=0, so this only caps tell_session chains. Override via MAX_TELL_DEPTH\n// (positive int); defaults to 20 when unset or invalid.\nconst _envMaxTellDepth = Number.parseInt(process.env.MAX_TELL_DEPTH ?? \"\", 10);\nexport const MAX_TELL_DEPTH =\n Number.isInteger(_envMaxTellDepth) && _envMaxTellDepth > 0 ? _envMaxTellDepth : 20;\n\n/** Codex CLI auth file. 호출 시점에 env를 읽는다 — 테스트가 런타임에\n * NEGOTIUM_CODEX_AUTH_FILE을 바꾸므로 모듈 로드 상수로 만들면 안 된다. */\nexport function codexAuthFilePath(): string {\n return (\n process.env.NEGOTIUM_CODEX_AUTH_FILE ||\n join(process.env.CODEX_HOME || join(homedir(), \".codex\"), \"auth.json\")\n );\n}\n\n// System defaults moved to per-agent registries\n// (`src/agents/{claude,codex}-registry.ts`). Read via\n// `getRegistry(agent).defaultModel` / `.defaultEffort`.\n\n// MCP server builders -> src/platform/mcp-config.ts\n",
|
|
17
|
-
"import type { HostMcpServerSpec } from \"#runtime-gateway\";\n\nexport interface TokenUsage {\n /** Aggregate billable input across every model call made during this turn. */\n inputTokens: number;\n outputTokens: number;\n cacheCreationInputTokens?: number;\n cacheReadInputTokens?: number;\n /** Provider-reported query cost when available. */\n costUsd?: number;\n /** Tokens occupied by the latest model call, not aggregate turn spend. */\n contextTokens?: number;\n /** Provider-reported context window for the latest model call. */\n contextWindow?: number;\n}\n\n/** Agent identifier — one of the supported AI provider backends. */\nexport type AgentKind = \"maestro\" | \"claude\" | \"codex\";\n\nexport const SUPPORTED_AGENTS: readonly AgentKind[] = [\"maestro\", \"claude\", \"codex\"] as const;\n\nexport function isAgentKind(value: unknown): value is AgentKind {\n return typeof value === \"string\" && (SUPPORTED_AGENTS as readonly string[]).includes(value);\n}\n\n/**\n * Per-agent supported reasoning efforts. Single source of truth for both the\n * `EffortLevel` type and each registry's `validEfforts` runtime list — the\n * registries import these directly so adding a value in one place\n * propagates to validation, footer rendering, and zod enums.\n *\n * Claude SDK rejects 'minimal'; Codex SDK rejects 'max'. The two sets\n * intersect on low/medium/high/xhigh. Maestro (TS port) currently piggybacks\n * on the Anthropic provider, so its efforts mirror the Claude set; this can\n * narrow per-provider once Phase 5 lands.\n *\n * 'minimal' removed from codex: Codex API rejects it when default tools\n * (image_gen, web_search) are active, making agent sessions unusable.\n */\nexport const CLAUDE_EFFORT_VALUES = [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const;\nexport const CODEX_EFFORT_VALUES = [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const;\nexport const MAESTRO_EFFORT_VALUES = [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const;\n\nexport type EffortLevel =\n | (typeof CLAUDE_EFFORT_VALUES)[number]\n | (typeof CODEX_EFFORT_VALUES)[number]\n | (typeof MAESTRO_EFFORT_VALUES)[number];\n\n/**\n * Runtime iteration list (used by zod enums and any callers that need to\n * loop over every accepted value). Manually ordered for readability; the\n * `satisfies` check fails the build if an entry here isn't covered by the\n * per-agent unions above.\n */\nexport const EFFORT_VALUES = [\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\",\n] as const satisfies readonly EffortLevel[];\n\n/**\n * Normalized events yielded by any agent provider (claudeProvider, codexProvider).\n * The handler/event-processor consumes these without caring which backend produced them.\n *\n * `user_message` is the lone \"into-the-log\" variant — no provider yields it.\n * The query handler writes it directly to the conversation log right before\n * `runAgent()` starts, so cross-agent rollout reconstruction can pair every\n * assistant turn with the user prompt that triggered it. Consumers that only\n * react to provider output (e.g. processAgentEvent) can safely ignore it.\n */\n/**\n * Wire-safe projection of one task, carried by the `tasks` UnifiedEvent.\n *\n * This is also the on-disk shape of Otium's shared task store, so claude,\n * codex, and maestro render the same live panel from the same source of truth.\n */\nexport interface TaskSnapshot {\n id: string;\n subject: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n /** Task ids this one is blocked by; omitted when empty. */\n blockedBy?: string[];\n /** Present-continuous label for spinners, when set. */\n activeForm?: string;\n /** Owner / agent name for multi-agent runs, when set. */\n owner?: string;\n}\n\n/** One topic-scoped decision and its incoming causal edges. */\nexport interface DecisionSnapshot {\n id: string;\n action: string;\n reasoning: string;\n agent: AgentKind;\n model?: string;\n status: \"proposed\" | \"accepted\" | \"executed\" | \"rejected\" | \"superseded\";\n /** Upstream decision ids. Each entry forms a directed upstream -> this edge. */\n causedBy?: string[];\n timestamp: number;\n}\n\nexport type UnifiedEvent =\n | {\n type: \"user_message\";\n content: string;\n synthetic?: \"compaction\";\n /** Total ordered user submissions represented by one preempting provider turn. */\n consecutiveBatchSize?: number;\n /** Zero-based position within the ordered preemption batch. */\n consecutiveBatchIndex?: number;\n }\n | { type: \"session\"; sessionId: string }\n | {\n type: \"tool_use\";\n name: string;\n input: Record<string, unknown>;\n /** Provider-assigned id so the client can match tool_use→tool_result pairs. */\n toolUseId?: string;\n }\n | { type: \"tool_progress\"; toolName: string; elapsed: number }\n | { type: \"tool_use_summary\"; summary: string }\n // Provider reasoning/thinking summary text (Codex `reasoning` items; Claude\n // extended-thinking). Surfaced so background runs (cron/archiver) show the\n // agent's thought process, not just tool calls.\n | { type: \"reasoning\"; content: string }\n // Full task-list snapshot (replace, not delta) from Otium's shared task\n // store. Provider-native task/todo stores are not authoritative.\n | { type: \"tasks\"; tasks: TaskSnapshot[] }\n | {\n type: \"tool_result\";\n toolUseId: string;\n content: string;\n /** True when the tool call failed; absent/false means success. */\n isError?: boolean;\n metadata?: {\n truncatedForModel: boolean;\n originalBytes: number;\n returnedBytes: number;\n omittedBytes?: number;\n outputPath?: string;\n };\n }\n | { type: \"text_delta\"; content: string }\n | { type: \"text\"; content: string }\n | { type: \"result\"; content: string; stopReason: string; usage?: TokenUsage }\n | { type: \"file\"; path: string; source: string; origin: \"tag\" | \"extension\" }\n | {\n type: \"error\";\n content: string;\n usage?: TokenUsage;\n code?: \"budget_exceeded\";\n }\n | { type: \"status\"; content: string };\n\nexport interface AgentInputAttachment {\n id: string;\n type: \"image\" | \"file\" | \"audio\";\n filename: string;\n mimeType: string;\n sizeBytes: number;\n path: string;\n}\n\n/** Worker-side runtime tools proxy user-facing state back to the canonical\n * hub topic identified here. */\nexport interface PeerRuntimeBridgeContext {\n hubCellId: string;\n hostTopicId: string;\n hostQueryId: string;\n canSpawnSubagents: boolean;\n}\n\nexport interface AgentQueryOptions {\n agent: AgentKind;\n prompt: string;\n attachments?: AgentInputAttachment[];\n sessionId?: string | null;\n cwd: string;\n systemPrompt: string;\n /**\n * Trusted system-level instructions for this Maestro invocation only.\n * Maestro projects them onto the invocation-start user message without\n * persisting them in session history or compaction. Omitted for Claude/Codex.\n */\n ephemeralSystemPrompt?: string;\n userId?: string;\n /** Product actor identity when execution uses a canonical node principal. */\n actorUserId?: string;\n /** Credential namespace when it differs from the execution principal. */\n vaultUserId?: string;\n session?: string;\n playwrightPort?: number;\n playwrightCapability?: string;\n bgBashPort?: number;\n sessionType?: \"dm\" | \"forum\" | \"ephemeral\" | \"manager\" | \"cron\";\n /** API topic id (REST/WS world). Carries per-query topic context for MCP servers. */\n topicId?: string;\n /** Direct parent topic id when this query runs inside a subagent room. */\n subagentParentTopicId?: string;\n /** API query id for the currently running turn. Used by runtime MCP tools. */\n queryId?: string;\n /** Optional wiki-memory topic id. Derived topics use their root origin here\n * while other per-topic MCP servers keep `topicId` bound to the live room. */\n wikiTopicId?: string;\n /** Whether self-config MCP may enqueue an automatic continue turn after set_* changes. */\n autoContinue?: boolean;\n /** Expose Otium-only visual panel tools for this turn. Default-deny. */\n visualTools?: boolean;\n /** Expose adapter-backed file-delivery tools for this turn. Default-deny. */\n fileDeliveryTools?: boolean;\n /** Validated host MCPs resolved from durable manager-room grants at execution time. */\n hostMcpServers?: Record<string, HostMcpServerSpec>;\n /**\n * Thread this turn answers inside, so the turn's runtime MCP can default\n * `thread_read` to it. Turn-scoped, unlike the session, which is per topic.\n */\n threadRootId?: string;\n abortController?: AbortController;\n model?: string;\n /** Provider-side hard budget when the selected SDK supports one. */\n maxBudgetUsd?: number;\n depth?: number;\n agents?: Record<\n string,\n {\n description: string;\n prompt: string;\n model?: string;\n tools?: string[];\n maxTurns?: number;\n effort?: EffortLevel | number;\n }\n >;\n effort?: EffortLevel;\n /**\n * Per-API-call `max_tokens` ceiling on the assistant's output. Wired\n * through to the underlying provider request body for every agent\n * (claude/codex/maestro). Omit to inherit each provider SDK's per-model\n * default — for maestro that's the v0.1.21+ `getNativeMaxOutputTokens`\n * catalog (deepseek-pro=64K, kimi-k3=64K, kimi-k2.7-code=32K).\n *\n * Pass an explicit number when a specific topic / surface needs a tighter\n * latency cap or a higher ceiling for long-form generation (legal\n * report writing, multi-K Write/Edit file bodies). Pre-0.1.21 maestro\n * builds silently clamped at 4096 and truncated outputs mid-string;\n * setting this field is now the supported way to lift that ceiling.\n */\n maxTokens?: number;\n /**\n * v0.1.22+: Claude-Code-style deferred tool catalog + `ToolSearch` built-in.\n *\n * Wired straight through to `maestro-agent-sdk`'s\n * `AgentQueryOptions.enableToolSearch`. When `true`, the maestro provider\n * registers every MCP tool as deferred — schemas stay off the wire until\n * the model promotes them via `ToolSearch(\"select:Name1,Name2\")` or\n * `ToolSearch(\"keyword\")`. Active set persists across resume.\n *\n * Otium's maestro provider supplies `true` when the caller leaves this\n * option unset, because most forum turns carry enough MCP surface for the\n * reminder-token savings to outweigh the first-use `ToolSearch` round-trip.\n * Callers can still pass `false` per call when a narrow surface or\n * latency-sensitive workflow is better served by eager MCP schemas.\n *\n * No-op for claude / codex agents — they have their own deferred-tool\n * machinery owned by their respective SDKs.\n */\n enableToolSearch?: boolean;\n /**\n * Bounded tool results with the full output kept on disk.\n *\n * Wired to `maestro-agent-sdk`'s `AgentQueryOptions.toolResultTruncation`.\n * The SDK caps a string tool result, writes the untruncated bytes to a file,\n * and splices an opaque `maestro://tool-output/<id>` reference into the text\n * that the `ReadToolOutput` tool can page back through.\n *\n * The maestro provider enables it by default. Left unset, every tool result\n * — a whole-file `Read`, a wide `Grep`, a `WebFetch` of a large page —\n * entered the context at full size, and the `\"ReadToolOutput\"` entry in the\n * provider's builtin list was dead, because the SDK only registers that tool\n * when truncation is on with `saveFullOutput`.\n *\n * Pass an explicit object to tune the budget, or `{ enabled: false }` for a\n * call whose tool results must arrive whole.\n *\n * No-op for claude / codex agents — their SDKs do their own truncation.\n */\n toolResultTruncation?: {\n enabled?: boolean;\n maxBytes?: number;\n headBytes?: number;\n tailBytes?: number;\n saveFullOutput?: boolean;\n outputDir?: string;\n retentionDays?: number;\n ignoreTools?: string[];\n };\n /**\n * Claude-Code-compatible exact tool denylist. Maestro v0.1.42+ hides these\n * tools from provider schemas / ToolSearch and blocks dispatch if a stale\n * call still arrives. Claude maps this to its SDK option. Codex does not\n * support this name-based list; its provider-native multi-agent tool family\n * is disabled separately through the Codex feature config.\n */\n disallowedTools?: readonly string[];\n /**\n * Hard provider tool policy for auxiliary model calls.\n *\n * `\"none\"` removes MCP and provider-native tools before the request is\n * dispatched. `\"compaction-log\"` keeps provider-native tools disabled and\n * exposes only the host-scoped immutable log reader. Use these for untrusted\n * transcript transforms; reacting to tool events after dispatch is not a\n * security boundary.\n */\n toolPolicy?: \"none\" | \"compaction-log\";\n mcpEnabled?: string[] | null;\n peerBridge?: PeerRuntimeBridgeContext;\n mcpExtra?: Record<string, unknown>;\n /**\n * true for silent fork runs generating ask_session replies — restricts session-comm\n * outbound tools (ask/tell/abort) so the forked session can only produce text\n */\n silent?: boolean;\n}\n\n/** State file written to data/users/{userId}/active-queries/{topicId}.json while a query is running. */\nexport interface QueryState {\n topicId?: string;\n topicName?: string;\n task?: string; // first 100 chars of prompt, newlines normalized\n since: string; // ISO timestamp\n}\n",
|
|
17
|
+
"import type { HostMcpServerSpec } from \"#runtime-gateway\";\n\nexport interface TokenUsage {\n /** Aggregate billable input across every model call made during this turn. */\n inputTokens: number;\n outputTokens: number;\n cacheCreationInputTokens?: number;\n cacheReadInputTokens?: number;\n /** Provider-reported query cost when available. */\n costUsd?: number;\n /** Tokens occupied by the latest model call, not aggregate turn spend. */\n contextTokens?: number;\n /** Provider-reported context window for the latest model call. */\n contextWindow?: number;\n}\n\n/** Agent identifier — one of the supported AI provider backends. */\nexport type AgentKind = \"maestro\" | \"claude\" | \"codex\";\n\nexport const SUPPORTED_AGENTS: readonly AgentKind[] = [\"maestro\", \"claude\", \"codex\"] as const;\n\nexport function isAgentKind(value: unknown): value is AgentKind {\n return typeof value === \"string\" && (SUPPORTED_AGENTS as readonly string[]).includes(value);\n}\n\n/**\n * Per-agent supported reasoning efforts. Single source of truth for both the\n * `EffortLevel` type and each registry's `validEfforts` runtime list — the\n * registries import these directly so adding a value in one place\n * propagates to validation, footer rendering, and zod enums.\n *\n * Claude SDK rejects 'minimal'; Codex SDK rejects 'max'. The two sets\n * intersect on low/medium/high/xhigh. Maestro (TS port) currently piggybacks\n * on the Anthropic provider, so its efforts mirror the Claude set; this can\n * narrow per-provider once Phase 5 lands.\n *\n * 'minimal' removed from codex: Codex API rejects it when default tools\n * (image_gen, web_search) are active, making agent sessions unusable.\n */\nexport const CLAUDE_EFFORT_VALUES = [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const;\nexport const CODEX_EFFORT_VALUES = [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const;\nexport const MAESTRO_EFFORT_VALUES = [\"low\", \"medium\", \"high\", \"xhigh\", \"max\"] as const;\n\nexport type EffortLevel =\n | (typeof CLAUDE_EFFORT_VALUES)[number]\n | (typeof CODEX_EFFORT_VALUES)[number]\n | (typeof MAESTRO_EFFORT_VALUES)[number];\n\n/**\n * Runtime iteration list (used by zod enums and any callers that need to\n * loop over every accepted value). Manually ordered for readability; the\n * `satisfies` check fails the build if an entry here isn't covered by the\n * per-agent unions above.\n */\nexport const EFFORT_VALUES = [\n \"low\",\n \"medium\",\n \"high\",\n \"xhigh\",\n \"max\",\n] as const satisfies readonly EffortLevel[];\n\n/**\n * Normalized events yielded by any agent provider (claudeProvider, codexProvider).\n * The handler/event-processor consumes these without caring which backend produced them.\n *\n * `user_message` is the lone \"into-the-log\" variant — no provider yields it.\n * The query handler writes it directly to the conversation log right before\n * `runAgent()` starts, so cross-agent rollout reconstruction can pair every\n * assistant turn with the user prompt that triggered it. Consumers that only\n * react to provider output (e.g. processAgentEvent) can safely ignore it.\n */\n/**\n * Wire-safe projection of one task, carried by the `tasks` UnifiedEvent.\n *\n * This is also the on-disk shape of Otium's shared task store, so claude,\n * codex, and maestro render the same live panel from the same source of truth.\n */\nexport interface TaskSnapshot {\n id: string;\n subject: string;\n status: \"pending\" | \"in_progress\" | \"completed\";\n /** Task ids this one is blocked by; omitted when empty. */\n blockedBy?: string[];\n /** Present-continuous label for spinners, when set. */\n activeForm?: string;\n /** Owner / agent name for multi-agent runs, when set. */\n owner?: string;\n}\n\n/** One topic-scoped decision and its incoming causal edges. */\nexport interface DecisionSnapshot {\n id: string;\n action: string;\n reasoning: string;\n agent: AgentKind;\n model?: string;\n status: \"proposed\" | \"accepted\" | \"executed\" | \"rejected\" | \"superseded\";\n /** Upstream decision ids. Each entry forms a directed upstream -> this edge. */\n causedBy?: string[];\n timestamp: number;\n}\n\nexport type UnifiedEvent =\n | {\n type: \"user_message\";\n content: string;\n synthetic?: \"compaction\";\n /** Total ordered user submissions represented by one preempting provider turn. */\n consecutiveBatchSize?: number;\n /** Zero-based position within the ordered preemption batch. */\n consecutiveBatchIndex?: number;\n }\n | { type: \"session\"; sessionId: string }\n | {\n type: \"tool_use\";\n name: string;\n input: Record<string, unknown>;\n /** Provider-assigned id so the client can match tool_use→tool_result pairs. */\n toolUseId?: string;\n }\n | { type: \"tool_progress\"; toolName: string; elapsed: number }\n | { type: \"tool_use_summary\"; summary: string }\n // Provider reasoning/thinking summary text (Codex `reasoning` items; Claude\n // extended-thinking). Surfaced so background runs (cron/archiver) show the\n // agent's thought process, not just tool calls.\n | { type: \"reasoning\"; content: string }\n // Full task-list snapshot (replace, not delta) from Otium's shared task\n // store. Provider-native task/todo stores are not authoritative.\n | { type: \"tasks\"; tasks: TaskSnapshot[] }\n | {\n type: \"tool_result\";\n toolUseId: string;\n content: string;\n /** True when the tool call failed; absent/false means success. */\n isError?: boolean;\n metadata?: {\n truncatedForModel: boolean;\n originalBytes: number;\n returnedBytes: number;\n omittedBytes?: number;\n outputPath?: string;\n };\n }\n | { type: \"text_delta\"; content: string }\n | { type: \"text\"; content: string }\n | { type: \"result\"; content: string; stopReason: string; usage?: TokenUsage }\n | { type: \"file\"; path: string; source: string; origin: \"tag\" | \"extension\" }\n | {\n type: \"error\";\n content: string;\n usage?: TokenUsage;\n code?: \"budget_exceeded\";\n }\n | { type: \"status\"; content: string };\n\nexport interface AgentInputAttachment {\n id: string;\n type: \"image\" | \"file\" | \"audio\";\n filename: string;\n mimeType: string;\n sizeBytes: number;\n path: string;\n}\n\n/** Worker-side runtime tools proxy user-facing state back to the canonical\n * hub topic identified here. */\nexport interface PeerRuntimeBridgeContext {\n hubCellId: string;\n hostTopicId: string;\n hostQueryId: string;\n canSpawnSubagents: boolean;\n}\n\n/**\n * The rooms the turn's human actor may reach on this node, asserted by the\n * hub that owns per-person membership for the `otium` surface.\n *\n * Node topic ids, valid on the node the turn was submitted to. `visible` is\n * every room the actor participates in (owner or member); `owned` is the\n * subset the actor owns. The assertion rides the signed per-turn MCP token,\n * so the agent can never widen it. With one, it is authoritative: reachable is\n * exactly the current room plus `visible`, owned exactly `owned` — the node\n * adds nothing of its own, not even subagent lineage. A turn without one\n * (older hub, or a turn no person started: cron, self-schedule, a subagent's\n * spawn/report/follow-up) is fail-closed to the current room plus the node's\n * own subagent lineage (direct parent, `grant_subagent_tell` targets, own\n * child workers).\n */\nexport interface ActorTopicScope {\n visibleNodeTopicIds: string[];\n ownedNodeTopicIds: string[];\n}\n\n/**\n * Hub-issued authority for the `node/topic` branches of `session-comm` on\n * the `otium` surface, one per turn. `capability` is an opaque bearer\n * (`rsc1.<payload>.<hmac>`) only the hub can verify: it binds the person who\n * spoke, the origin room and this turn's request id, and the hub re-checks\n * the person's membership of the origin and target rooms on every call. The\n * node holds no key, so it can neither mint nor verify one; the only thing it\n * reads out of the payload is `e` (expiry), base64-decoded untrusted to order\n * grants when several requests fold into one turn. `hubUrl` is where the\n * node presents it (`/api/v1/session-comm/remote/*`). Absent — an older hub,\n * a hub with the feature off, or a turn no person started (cron,\n * self-schedule, subagent lineage) — remote session-comm stays fail-closed.\n */\nexport interface RemoteSessionGrant {\n hubUrl: string;\n capability: string;\n}\n\nexport interface AgentQueryOptions {\n agent: AgentKind;\n prompt: string;\n attachments?: AgentInputAttachment[];\n sessionId?: string | null;\n cwd: string;\n systemPrompt: string;\n /**\n * Trusted system-level instructions for this Maestro invocation only.\n * Maestro projects them onto the invocation-start user message without\n * persisting them in session history or compaction. Omitted for Claude/Codex.\n */\n ephemeralSystemPrompt?: string;\n userId?: string;\n /** Product actor identity when execution uses a canonical node principal. */\n actorUserId?: string;\n /** Hub-asserted rooms the product actor may reach; absent means fail-closed. */\n actorTopicScope?: ActorTopicScope;\n /** Hub-issued per-turn authority for remote (`node/topic`) session-comm. */\n remoteSession?: RemoteSessionGrant;\n /** Credential namespace when it differs from the execution principal. */\n vaultUserId?: string;\n session?: string;\n playwrightPort?: number;\n playwrightCapability?: string;\n bgBashPort?: number;\n sessionType?: \"dm\" | \"forum\" | \"ephemeral\" | \"manager\" | \"cron\";\n /** API topic id (REST/WS world). Carries per-query topic context for MCP servers. */\n topicId?: string;\n /** Direct parent topic id when this query runs inside a subagent room. */\n subagentParentTopicId?: string;\n /** API query id for the currently running turn. Used by runtime MCP tools. */\n queryId?: string;\n /** Optional wiki-memory topic id. Derived topics use their root origin here\n * while other per-topic MCP servers keep `topicId` bound to the live room. */\n wikiTopicId?: string;\n /** Whether self-config MCP may enqueue an automatic continue turn after set_* changes. */\n autoContinue?: boolean;\n /** Expose Otium-only visual panel tools for this turn. Default-deny. */\n visualTools?: boolean;\n /** Expose adapter-backed file-delivery tools for this turn. Default-deny. */\n fileDeliveryTools?: boolean;\n /** Validated host MCPs resolved from durable manager-room grants at execution time. */\n hostMcpServers?: Record<string, HostMcpServerSpec>;\n /**\n * Thread this turn answers inside, so the turn's runtime MCP can default\n * `thread_read` to it. Turn-scoped, unlike the session, which is per topic.\n */\n threadRootId?: string;\n abortController?: AbortController;\n model?: string;\n /** Provider-side hard budget when the selected SDK supports one. */\n maxBudgetUsd?: number;\n depth?: number;\n agents?: Record<\n string,\n {\n description: string;\n prompt: string;\n model?: string;\n tools?: string[];\n maxTurns?: number;\n effort?: EffortLevel | number;\n }\n >;\n effort?: EffortLevel;\n /**\n * Per-API-call `max_tokens` ceiling on the assistant's output. Wired\n * through to the underlying provider request body for every agent\n * (claude/codex/maestro). Omit to inherit each provider SDK's per-model\n * default — for maestro that's the v0.1.21+ `getNativeMaxOutputTokens`\n * catalog (deepseek-pro=64K, kimi-k3=64K, kimi-k2.7-code=32K).\n *\n * Pass an explicit number when a specific topic / surface needs a tighter\n * latency cap or a higher ceiling for long-form generation (legal\n * report writing, multi-K Write/Edit file bodies). Pre-0.1.21 maestro\n * builds silently clamped at 4096 and truncated outputs mid-string;\n * setting this field is now the supported way to lift that ceiling.\n */\n maxTokens?: number;\n /**\n * v0.1.22+: Claude-Code-style deferred tool catalog + `ToolSearch` built-in.\n *\n * Wired straight through to `maestro-agent-sdk`'s\n * `AgentQueryOptions.enableToolSearch`. When `true`, the maestro provider\n * registers every MCP tool as deferred — schemas stay off the wire until\n * the model promotes them via `ToolSearch(\"select:Name1,Name2\")` or\n * `ToolSearch(\"keyword\")`. Active set persists across resume.\n *\n * Otium's maestro provider supplies `true` when the caller leaves this\n * option unset, because most forum turns carry enough MCP surface for the\n * reminder-token savings to outweigh the first-use `ToolSearch` round-trip.\n * Callers can still pass `false` per call when a narrow surface or\n * latency-sensitive workflow is better served by eager MCP schemas.\n *\n * No-op for claude / codex agents — they have their own deferred-tool\n * machinery owned by their respective SDKs.\n */\n enableToolSearch?: boolean;\n /**\n * Bounded tool results with the full output kept on disk.\n *\n * Wired to `maestro-agent-sdk`'s `AgentQueryOptions.toolResultTruncation`.\n * The SDK caps a string tool result, writes the untruncated bytes to a file,\n * and splices an opaque `maestro://tool-output/<id>` reference into the text\n * that the `ReadToolOutput` tool can page back through.\n *\n * The maestro provider enables it by default. Left unset, every tool result\n * — a whole-file `Read`, a wide `Grep`, a `WebFetch` of a large page —\n * entered the context at full size, and the `\"ReadToolOutput\"` entry in the\n * provider's builtin list was dead, because the SDK only registers that tool\n * when truncation is on with `saveFullOutput`.\n *\n * Pass an explicit object to tune the budget, or `{ enabled: false }` for a\n * call whose tool results must arrive whole.\n *\n * No-op for claude / codex agents — their SDKs do their own truncation.\n */\n toolResultTruncation?: {\n enabled?: boolean;\n maxBytes?: number;\n headBytes?: number;\n tailBytes?: number;\n saveFullOutput?: boolean;\n outputDir?: string;\n retentionDays?: number;\n ignoreTools?: string[];\n };\n /**\n * Claude-Code-compatible exact tool denylist. Maestro v0.1.42+ hides these\n * tools from provider schemas / ToolSearch and blocks dispatch if a stale\n * call still arrives. Claude maps this to its SDK option. Codex does not\n * support this name-based list; its provider-native multi-agent tool family\n * is disabled separately through the Codex feature config.\n */\n disallowedTools?: readonly string[];\n /**\n * Hard provider tool policy for auxiliary model calls.\n *\n * `\"none\"` removes MCP and provider-native tools before the request is\n * dispatched. `\"compaction-log\"` keeps provider-native tools disabled and\n * exposes only the host-scoped immutable log reader. Use these for untrusted\n * transcript transforms; reacting to tool events after dispatch is not a\n * security boundary.\n */\n toolPolicy?: \"none\" | \"compaction-log\";\n mcpEnabled?: string[] | null;\n peerBridge?: PeerRuntimeBridgeContext;\n mcpExtra?: Record<string, unknown>;\n /**\n * true for silent fork runs generating ask_session replies — restricts session-comm\n * outbound tools (ask/tell/abort) so the forked session can only produce text\n */\n silent?: boolean;\n}\n\n/** State file written to data/users/{userId}/active-queries/{topicId}.json while a query is running. */\nexport interface QueryState {\n topicId?: string;\n topicName?: string;\n task?: string; // first 100 chars of prompt, newlines normalized\n since: string; // ISO timestamp\n}\n",
|
|
18
18
|
"import { resolve } from \"node:path\";\nimport { type AgentKind, isAgentKind } from \"#types\";\n\nexport type RuntimeEnvironment = Readonly<Record<string, string | undefined>>;\n\nexport function readEnvText(env: RuntimeEnvironment, key: string): string | undefined {\n const value = env[key]?.trim();\n return value || undefined;\n}\n\n/**\n * Node-wide default agent (FALLBACK_AGENT env, legacy DEFAULT_AGENT, else\n * \"claude\"). Lives in this side-effect-free leaf module — not #platform/config\n * — so storage layers that only need this one value (e.g. the forum/api-topics\n * repositories, re-exported through the public storage facade) don't drag in\n * config.ts's eager `mkdirSync` of every node data directory just by\n * importing it.\n */\nexport function resolveFallbackAgent(env: RuntimeEnvironment = process.env): AgentKind {\n const value = readEnvText(env, \"FALLBACK_AGENT\") ?? readEnvText(env, \"DEFAULT_AGENT\");\n return isAgentKind(value) ? value : \"claude\";\n}\n\nexport function parseRuntimePort(value: string | undefined, fallback: number): number {\n if (!value) return fallback;\n const port = Number.parseInt(value, 10);\n return Number.isInteger(port) && port > 0 && port <= 65_535 ? port : fallback;\n}\n\nexport function resolveRuntimeStateDir(options: {\n env: RuntimeEnvironment;\n envKey: string;\n fallbackRoot: string;\n fallbackName: string;\n}): string {\n const configured = readEnvText(options.env, options.envKey);\n return configured ? resolve(configured) : resolve(options.fallbackRoot, options.fallbackName);\n}\n\nexport function safeRuntimePathSegment(value: string, fallback: string, maxLength = 160): string {\n const cleaned = value\n .trim()\n .replace(/[^A-Za-z0-9._-]/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, maxLength);\n return cleaned || fallback;\n}\n",
|
|
19
19
|
"import { mkdirSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { dirname, join, resolve } from \"node:path\";\nimport { closeDatabase, Database } from \"#storage/sqlite\";\nimport type { StorageDatabase, StorageHostConfig } from \"#storage/storage-contract\";\n\nexport type {\n StorageDatabase,\n StorageDatabaseAdapter,\n StorageDatabaseInput,\n StorageHostConfig,\n StorageHostOptions,\n StorageStatement,\n StorageTransaction,\n} from \"#storage/storage-contract\";\n\ntype InternalStorageDatabase = InstanceType<typeof Database>;\ntype OwnedStorageDatabase = InternalStorageDatabase & { close(): void };\n\ninterface StorageHostFrame {\n active: boolean;\n patch: Readonly<StorageHostConfig>;\n}\n\ntype StorageSchemaInitializer = (database: InternalStorageDatabase) => void;\ninterface RegisteredSchemaInitializer {\n initialize: StorageSchemaInitializer;\n priority: number;\n}\n\ninterface StorageHostState {\n configuredHost: Readonly<StorageHostConfig>;\n fallbackDatabase: OwnedStorageDatabase | null;\n fallbackDatabasePath: string | null;\n frames: StorageHostFrame[];\n schemaInitializers: RegisteredSchemaInitializer[];\n initializedSchemas: WeakMap<InternalStorageDatabase, Set<StorageSchemaInitializer>>;\n initializingDatabases: WeakSet<InternalStorageDatabase>;\n}\n\n/**\n * Which database an embedded Negotium uses is a property of the *process*, so\n * the state deciding it is keyed off a registered symbol rather than held in\n * module scope.\n *\n * Module scope would be equivalent only if every caller resolved to the same\n * module instance, and in the published package they do not:\n * `build-negotium-package.ts` emits most public entrypoints as independent\n * graphs, so each gets a private copy of this file. A host that configured\n * `negotium/storage` therefore left `negotium/agent-helpers` unconfigured, and\n * that copy opened a second database under the default state directory — with\n * no error, since both files are valid and writable.\n *\n * Merging those entrypoints into one graph looks tidier and does cut ~3MB from\n * `dist`, but Bun 1.2.15 (the pinned version) emits a shared chunk's export\n * list twice and the result is invalid ESM — \"Cannot export a duplicate name\".\n * That is bundler output with no counterpart in this source, so there is\n * nothing to de-duplicate here; it is fixed in Bun 1.3.14. Until the floor\n * moves, correctness cannot depend on how the package is chunked.\n */\nconst STORAGE_HOST_STATE = Symbol.for(\"negotium.storage-host.state.v1\");\n\nfunction storageState(): StorageHostState {\n const holder = globalThis as typeof globalThis & { [STORAGE_HOST_STATE]?: StorageHostState };\n const existing = holder[STORAGE_HOST_STATE];\n if (existing) return existing;\n const created: StorageHostState = {\n configuredHost: {},\n fallbackDatabase: null,\n fallbackDatabasePath: null,\n frames: [],\n schemaInitializers: [],\n initializedSchemas: new WeakMap(),\n initializingDatabases: new WeakSet(),\n };\n Object.defineProperty(holder, STORAGE_HOST_STATE, {\n value: created,\n enumerable: false,\n writable: false,\n configurable: false,\n });\n return created;\n}\n\nfunction envPath(name: string, fallback: string): string {\n const value = process.env[name]?.trim();\n return resolve(value || fallback);\n}\n\nfunction defaultStateDir(): string {\n return envPath(\"NEGOTIUM_STATE_DIR\", join(homedir(), \".negotium\"));\n}\n\nfunction defaultDataDir(): string {\n return envPath(\"NEGOTIUM_DATA_DIR\", join(defaultStateDir(), \"data\"));\n}\n\nfunction defaultLogDir(): string {\n return envPath(\"NEGOTIUM_LOG_DIR\", join(defaultStateDir(), \"logs\"));\n}\n\nfunction defaultWorkspaceDir(): string {\n return envPath(\"NEGOTIUM_WORKSPACE_DIR\", join(defaultStateDir(), \"workspace\"));\n}\n\nfunction defaultSessionAsksDir(): string {\n const runDir = envPath(\"NEGOTIUM_RUN_DIR\", join(defaultStateDir(), \"runtime\"));\n return join(runDir, \"session-asks\");\n}\n\nfunction defaultSessionsDatabasePath(): string {\n return envPath(\"SESSIONS_DB_PATH\", join(resolveStorageDataDir(), \"sessions.db\"));\n}\n\nconst SQLITE_INIT_RETRY_MS = 25;\nconst SQLITE_INIT_TIMEOUT_MS = 5_000;\nconst SQLITE_INIT_SLEEP = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));\n\nfunction isSqliteBusy(error: unknown): boolean {\n const message = error instanceof Error ? error.message : String(error);\n return /database is (?:locked|busy)|SQLITE_(?:BUSY|LOCKED)/i.test(message);\n}\n\nfunction execWithBusyRetry(\n database: InternalStorageDatabase,\n sql: string,\n timeoutMs = SQLITE_INIT_TIMEOUT_MS,\n): void {\n const deadline = Date.now() + timeoutMs;\n while (true) {\n try {\n database.exec(sql);\n return;\n } catch (error) {\n if (!isSqliteBusy(error) || Date.now() >= deadline) throw error;\n Atomics.wait(SQLITE_INIT_SLEEP, 0, 0, Math.min(SQLITE_INIT_RETRY_MS, deadline - Date.now()));\n }\n }\n}\n\nexport function initializeDatabase(database: InternalStorageDatabase): void {\n // busy_timeout FIRST. Switching to WAL needs a brief exclusive lock, so when\n // two processes open the same database at the same moment — the node daemon\n // and an adapter, or two MCP servers — one of them hits SQLITE_BUSY. With the\n // timeout configured afterwards there was no retry budget in effect for the\n // statement that needed it most. Older Bun releases can still surface\n // SQLITE_BUSY immediately for journal_mode, so keep a bounded host-level\n // retry around that one exclusive transition as well.\n database.exec(\"PRAGMA busy_timeout = 5000\");\n execWithBusyRetry(database, \"PRAGMA journal_mode = WAL\");\n database.exec(\"PRAGMA foreign_keys = ON\");\n database.exec(\"PRAGMA wal_autocheckpoint = 1000\");\n try {\n database.exec(\"PRAGMA wal_checkpoint(TRUNCATE)\");\n } catch {\n // Non-fatal; a concurrent writer may hold the WAL briefly.\n }\n}\n\nfunction defaultDatabase(): InternalStorageDatabase {\n const path = defaultSessionsDatabasePath();\n const state = storageState();\n if (state.fallbackDatabase && state.fallbackDatabasePath === path) return state.fallbackDatabase;\n if (state.fallbackDatabase) closeDatabase(state.fallbackDatabase);\n mkdirSync(dirname(path), { recursive: true });\n state.fallbackDatabase = new Database(path, { create: true }) as unknown as OwnedStorageDatabase;\n state.fallbackDatabasePath = path;\n initializeDatabase(state.fallbackDatabase);\n return state.fallbackDatabase;\n}\n\nexport function resolveStorageDatabase(): InternalStorageDatabase {\n return (storageState().configuredHost.database ?? defaultDatabase()) as InternalStorageDatabase;\n}\n\n/**\n * The host-injected connection, or null when Negotium owns its own store.\n *\n * Unlike {@link resolveStorageDatabase} this never opens anything. Callers that\n * manage their own short-lived connections need to know whether a host store\n * exists *before* deciding to open one by path: opening it anyway is how an\n * embedded Negotium ends up writing half its state into the host's database and\n * the other half into `~/.negotium`, with nothing reporting an error.\n */\nexport function configuredStorageDatabase(): InternalStorageDatabase | null {\n return (storageState().configuredHost.database as InternalStorageDatabase | undefined) ?? null;\n}\n\nexport function resolveStorageDataDir(): string {\n return storageState().configuredHost.dataDir ?? defaultDataDir();\n}\n\nexport function resolveStorageLogDir(): string {\n return storageState().configuredHost.logDir ?? defaultLogDir();\n}\n\nexport function resolveStorageSessionAsksDir(): string {\n return storageState().configuredHost.sessionAsksDir ?? defaultSessionAsksDir();\n}\n\nexport function resolveStorageWorkspaceDir(): string {\n return storageState().configuredHost.workspaceDir ?? defaultWorkspaceDir();\n}\n\nexport function resolveStorageSharedWikiDir(): string {\n return storageState().configuredHost.sharedWikiDir ?? join(resolveStorageWorkspaceDir(), \"wiki\");\n}\n\nexport function resolveStorageUsersLogDir(): string {\n return storageState().configuredHost.usersLogDir ?? join(resolveStorageDataDir(), \"users\");\n}\n\nconst STORAGE_PATH_KEYS = [\n \"dataDir\",\n \"logDir\",\n \"sessionAsksDir\",\n \"workspaceDir\",\n \"sharedWikiDir\",\n \"usersLogDir\",\n] as const;\n\nfunction normalizeStorageHostPatch(options: StorageHostConfig): Readonly<StorageHostConfig> {\n const patch: StorageHostConfig = {};\n if (options.database !== undefined) {\n options.database.exec(\"PRAGMA foreign_keys = ON\");\n patch.database = options.database;\n }\n for (const key of STORAGE_PATH_KEYS) {\n const value = options[key];\n if (value === undefined) continue;\n if (!value.trim()) throw new TypeError(`${key} must not be empty`);\n patch[key] = resolve(value);\n }\n return Object.freeze(patch);\n}\n\nfunction refreshConfiguredHost(): void {\n const state = storageState();\n state.configuredHost = Object.freeze(\n Object.assign({}, ...state.frames.filter((frame) => frame.active).map((frame) => frame.patch)),\n );\n}\n\n/**\n * Configure the process-local storage boundary for an embedding host.\n *\n * Resolution is lazy: importing `negotium/storage` never opens a database or\n * touches a filesystem path. The returned disposer restores the exact prior\n * host, which keeps tests and nested embeddings isolated.\n */\nexport function configureStorageHost(options: StorageHostConfig): () => void {\n const frame: StorageHostFrame = { active: true, patch: normalizeStorageHostPatch(options) };\n storageState().frames.push(frame);\n refreshConfiguredHost();\n return () => {\n if (!frame.active) return;\n frame.active = false;\n const frames = storageState().frames;\n const index = frames.indexOf(frame);\n if (index >= 0) frames.splice(index, 1);\n refreshConfiguredHost();\n };\n}\n\n/** Remove every configured host layer and restore standalone fallbacks. */\nexport function resetStorageHost(): void {\n const frames = storageState().frames;\n for (const frame of frames) frame.active = false;\n frames.length = 0;\n refreshConfiguredHost();\n}\n\n/** Close only Negotium's fallback connection. Injected connections are borrowed. */\nexport function closeStorageDatabase(): void {\n const state = storageState();\n if (!state.fallbackDatabase) return;\n closeDatabase(state.fallbackDatabase);\n state.fallbackDatabase = null;\n state.fallbackDatabasePath = null;\n}\n\nexport function registerStorageSchemaInitializer(\n initialize: StorageSchemaInitializer,\n priority = 100,\n): void {\n const initializers = storageState().schemaInitializers;\n initializers.push({ initialize, priority });\n initializers.sort((a, b) => a.priority - b.priority);\n}\n\nexport function ensureStorageSchemas(\n database: InternalStorageDatabase = resolveStorageDatabase(),\n): void {\n const state = storageState();\n if (state.initializingDatabases.has(database)) return;\n let initialized = state.initializedSchemas.get(database);\n if (!initialized) {\n initialized = new Set();\n state.initializedSchemas.set(database, initialized);\n }\n state.initializingDatabases.add(database);\n try {\n for (const entry of state.schemaInitializers) {\n if (initialized.has(entry.initialize)) continue;\n // Mark first so a migration that calls through the public db proxy does\n // not recursively invoke itself. Remove on failure so the next call can retry.\n initialized.add(entry.initialize);\n try {\n entry.initialize(database);\n } catch (error) {\n initialized.delete(entry.initialize);\n throw error;\n }\n }\n } finally {\n state.initializingDatabases.delete(database);\n }\n}\n\n/** Stable proxy identity used by legacy imports and embedding hosts. */\nexport const internalStorageDatabase = new Proxy({} as InternalStorageDatabase, {\n get(_target, property) {\n const database = resolveStorageDatabase();\n ensureStorageSchemas(database);\n const value = Reflect.get(database as object, property, database);\n return typeof value === \"function\" ? value.bind(database) : value;\n },\n set(_target, property, value) {\n const database = resolveStorageDatabase();\n ensureStorageSchemas(database);\n return Reflect.set(database as object, property, value, database);\n },\n});\n\n/** Structurally typed view intended for embedding hosts. */\nexport const storageDatabase = internalStorageDatabase as unknown as StorageDatabase;\n",
|
|
20
20
|
"// Runtime-adaptive SQLite façade.\n//\n// Why this exists: Otium normally runs on bun and uses `bun:sqlite` (native,\n// fast, zero-build). But the `codex` agent's MCP servers must run on pure node\n// — codex 0.135's rmcp stdio client can't handshake with bun-spawned servers\n// (see serverLaunch in platform/mcp-config.ts). `bun:sqlite` doesn't exist\n// under node, so any MCP server that touches the DB would crash on import.\n//\n// This module picks the backend at runtime and re-exports it as `Database`:\n// - under bun → the real `bun:sqlite` Database (unchanged behavior)\n// - under node → a thin shim over node:sqlite's DatabaseSync exposing the\n// exact subset Otium uses: new Database(path, {readonly?,create?}),\n// .query()/.prepare() → statement, .exec(), .run(), .transaction(fn),\n// .close(); statements support positional `?` params via .run()/.get()/\n// .all(). (No named-object params / .values()/.iterate()/.as() are used in\n// this codebase, so the shim intentionally omits them.)\n//\n// The TYPE of the exported `Database` is bun:sqlite's own class type (imported\n// type-only, so it's erased at runtime and never resolved under node). That\n// keeps every existing call site — including generic `.query<Row, Params>()`\n// usages — type-checking exactly as before with zero changes.\n//\n// The bun branch uses a dynamic import so node never resolves the `bun:sqlite`\n// specifier (dead code under node), and the node branch's `node:sqlite` import\n// never runs under bun.\n\nimport type { Database as BunDatabase } from \"bun:sqlite\";\n\n// `mock.module()` can replace the Bun global while a test graph is being\n// evaluated. The runtime version flag is stable and is also the documented\n// cross-runtime discriminator.\nconst isBun = typeof process.versions.bun === \"string\";\n\ntype DatabaseCtor = typeof BunDatabase;\n\nlet Database: DatabaseCtor;\n\nif (isBun) {\n ({ Database } = await import(\"bun:sqlite\"));\n} else {\n // node:sqlite has no type declarations under our tsconfig (`types: bun-types`\n // only), and `bun-types` doesn't ship them — so type the dynamic import\n // locally and use a `string` specifier to keep tsc from trying to resolve it.\n type NodeStatement = {\n run(...params: unknown[]): unknown;\n get(...params: unknown[]): unknown;\n all(...params: unknown[]): unknown[];\n };\n type NodeDatabaseSync = {\n prepare(sql: string): NodeStatement;\n exec(sql: string): void;\n close(): void;\n };\n type NodeSqliteModule = {\n DatabaseSync: new (path: string, options?: { readOnly?: boolean }) => NodeDatabaseSync;\n };\n // Keep Bun's resolver from eagerly resolving the Node-only builtin even\n // though this branch is dead there (notably under `bun test` + mock.module).\n const nodeSqliteSpecifier = [\"node\", \"sqlite\"].join(\":\");\n const { DatabaseSync } = (await import(nodeSqliteSpecifier)) as NodeSqliteModule;\n\n type DbOptions = { readonly?: boolean; create?: boolean };\n\n class NodeDatabase {\n #db: NodeDatabaseSync;\n\n constructor(path: string, options: DbOptions = {}) {\n // node:sqlite opens read-write and creates-if-missing by default, which\n // matches bun's `{create:true}`. For readonly we must not create.\n this.#db = options.readonly\n ? new DatabaseSync(path, { readOnly: true })\n : new DatabaseSync(path);\n }\n\n query(sql: string) {\n return this.#db.prepare(sql);\n }\n\n prepare(sql: string) {\n return this.#db.prepare(sql);\n }\n\n exec(sql: string): void {\n this.#db.exec(sql);\n }\n\n run(sql: string, ...params: unknown[]) {\n return this.#db.prepare(sql).run(...params);\n }\n\n transaction<Args extends unknown[], R>(fn: (...args: Args) => R): (...args: Args) => R {\n // bun's db.transaction(fn) returns a function that runs fn inside a\n // transaction when called. node:sqlite has no helper, so emulate with\n // BEGIN/COMMIT/ROLLBACK. It also exposes deferred/immediate/exclusive\n // variants; mirror those so Node-launched MCP servers can share call\n // sites with Bun.\n const run =\n (begin: \"BEGIN\" | \"BEGIN DEFERRED\" | \"BEGIN IMMEDIATE\" | \"BEGIN EXCLUSIVE\") =>\n (...args: Args): R => {\n this.#db.exec(begin);\n try {\n const result = fn(...args);\n this.#db.exec(\"COMMIT\");\n return result;\n } catch (err) {\n this.#db.exec(\"ROLLBACK\");\n throw err;\n }\n };\n const tx = run(\"BEGIN\") as ((...args: Args) => R) & {\n deferred: (...args: Args) => R;\n immediate: (...args: Args) => R;\n exclusive: (...args: Args) => R;\n };\n tx.deferred = run(\"BEGIN DEFERRED\");\n tx.immediate = run(\"BEGIN IMMEDIATE\");\n tx.exclusive = run(\"BEGIN EXCLUSIVE\");\n return tx;\n }\n\n close(): void {\n this.#db.close();\n }\n }\n\n Database = NodeDatabase as unknown as DatabaseCtor;\n}\n\nexport { Database };\n\n/**\n * Close a database and release its underlying file handle.\n *\n * `bun:sqlite`'s no-argument `close()` does not finalize prepared statements\n * that are still reachable, so the file stays open. POSIX hides this — the\n * path can be unlinked while a handle remains — but Windows refuses to delete\n * or rename the file at all (`EBUSY`), which is how the leak surfaces. Passing\n * `true` finalizes eagerly and drops the handle.\n *\n * The node shim's `close()` ignores the argument, so one call covers both\n * backends; the fallback only matters for a backend that rejects the extra\n * parameter outright.\n */\nexport function closeDatabase(database: { close(throwOnError?: boolean): void }): void {\n try {\n database.close(true);\n } catch {\n try {\n database.close();\n } catch {\n // Already closed: nothing left to release.\n }\n }\n}\n",
|
package/dist/storage.js
CHANGED
|
@@ -1261,6 +1261,7 @@ __export(exports_api_topics, {
|
|
|
1261
1261
|
normalizeAiMode: () => normalizeAiMode,
|
|
1262
1262
|
listTopics: () => listTopics,
|
|
1263
1263
|
listSubagentTellTargetIds: () => listSubagentTellTargetIds,
|
|
1264
|
+
listSubagentChildTopicIds: () => listSubagentChildTopicIds,
|
|
1264
1265
|
isTopicVisible: () => isTopicVisible,
|
|
1265
1266
|
isSurfaceScopeRequired: () => isSurfaceScopeRequired,
|
|
1266
1267
|
inferTopicKind: () => inferTopicKind,
|
|
@@ -1593,6 +1594,9 @@ function initializeApiTopicsSchema() {
|
|
|
1593
1594
|
if (!tableColumns2("api_topics").has("subagent_report_mode")) {
|
|
1594
1595
|
db.exec("ALTER TABLE api_topics ADD COLUMN subagent_report_mode TEXT NOT NULL DEFAULT 'auto'");
|
|
1595
1596
|
}
|
|
1597
|
+
if (!tableColumns2("api_topics").has("derived_by_user_id")) {
|
|
1598
|
+
db.exec("ALTER TABLE api_topics ADD COLUMN derived_by_user_id TEXT");
|
|
1599
|
+
}
|
|
1596
1600
|
if (!tableColumns2("api_topics").has("memory_topic_id")) {
|
|
1597
1601
|
db.exec("ALTER TABLE api_topics ADD COLUMN memory_topic_id TEXT");
|
|
1598
1602
|
}
|
|
@@ -1731,6 +1735,7 @@ function rowToDto2(r, participants = getTopicParticipants(r.id), tellTargets) {
|
|
|
1731
1735
|
memoryTopicId: r.memory_topic_id ?? undefined,
|
|
1732
1736
|
memoryKey: r.memory_key ?? undefined,
|
|
1733
1737
|
isFork: r.is_fork !== 0,
|
|
1738
|
+
...r.derived_by_user_id ? { derivedByUserId: r.derived_by_user_id } : {},
|
|
1734
1739
|
...r.is_subagent !== 0 ? { isSubagent: true } : {},
|
|
1735
1740
|
...r.is_subagent !== 0 ? {
|
|
1736
1741
|
subagentTellTargetIds: tellTargets ? tellTargets.get(r.id) ?? [] : listSubagentTellTargetIds(r.id)
|
|
@@ -1825,8 +1830,8 @@ function upsertTopic(t) {
|
|
|
1825
1830
|
db.query(`INSERT INTO api_topics
|
|
1826
1831
|
(id,title,kind,description,agent,base_model,base_effort,response_policy,
|
|
1827
1832
|
created_at,last_message_at,parent_topic_id,memory_topic_id,memory_key,is_fork,is_subagent,visibility,surface,
|
|
1828
|
-
surface_scope,subagent_report_mode)
|
|
1829
|
-
VALUES (
|
|
1833
|
+
surface_scope,subagent_report_mode,derived_by_user_id)
|
|
1834
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
|
1830
1835
|
ON CONFLICT(id) DO UPDATE SET
|
|
1831
1836
|
title = excluded.title,
|
|
1832
1837
|
kind = excluded.kind,
|
|
@@ -1848,7 +1853,10 @@ function upsertTopic(t) {
|
|
|
1848
1853
|
-- assignment: an update may fill in a scope that was unknown when the
|
|
1849
1854
|
-- room was created, but may never move a room to another workspace.
|
|
1850
1855
|
surface_scope = COALESCE(api_topics.surface_scope, excluded.surface_scope),
|
|
1851
|
-
subagent_report_mode = excluded.subagent_report_mode
|
|
1856
|
+
subagent_report_mode = excluded.subagent_report_mode,
|
|
1857
|
+
-- Who asked for the derive is a fact about creation; a later update
|
|
1858
|
+
-- that omits it must not erase it.
|
|
1859
|
+
derived_by_user_id = COALESCE(excluded.derived_by_user_id, api_topics.derived_by_user_id)`).run(t.id, t.title, normalized.kind, t.description ?? null, normalized.agent ?? null, t.defaultModel ?? null, t.defaultEffort ?? null, normalized.aiMode, t.createdAt, t.lastMessageAt ?? null, t.parentTopicId ?? null, t.memoryTopicId ?? null, t.memoryKey ?? null, t.isFork ? 1 : 0, t.isSubagent ? 1 : 0, normalizeTopicVisibility(t.visibility), normalizeTopicSurface(t.surface ?? defaultTopicSurface()), surfaceScopeForWrite(t), t.subagentReportMode ?? "auto", t.derivedByUserId ?? null);
|
|
1852
1860
|
db.query("DELETE FROM topic_members WHERE topic_id = ?").run(t.id);
|
|
1853
1861
|
for (const participant of t.participants) {
|
|
1854
1862
|
db.query("INSERT INTO topic_members (topic_id,user_id,role) VALUES (?,?,?)").run(t.id, participant.userId, participant.role);
|
|
@@ -2044,6 +2052,9 @@ function grantSubagentTellTarget(subagentTopicId, targetTopicId, grantedByTopicI
|
|
|
2044
2052
|
function revokeSubagentTellTarget(subagentTopicId, targetTopicId) {
|
|
2045
2053
|
return db.query("DELETE FROM subagent_tell_grants WHERE subagent_topic_id = ? AND target_topic_id = ?").run(subagentTopicId, targetTopicId).changes > 0;
|
|
2046
2054
|
}
|
|
2055
|
+
function listSubagentChildTopicIds(parentTopicId) {
|
|
2056
|
+
return db.query("SELECT id FROM api_topics WHERE parent_topic_id = ? AND is_subagent = 1 ORDER BY created_at").all(parentTopicId).map((row) => row.id);
|
|
2057
|
+
}
|
|
2047
2058
|
function reparentTopicChildren(deletedTopicId, replacementParentTopicId) {
|
|
2048
2059
|
const rows = db.query("SELECT id FROM api_topics WHERE parent_topic_id = ?").all(deletedTopicId);
|
|
2049
2060
|
if (rows.length === 0)
|
|
@@ -4503,6 +4514,7 @@ export {
|
|
|
4503
4514
|
listThreadMessages,
|
|
4504
4515
|
listThreadMessageRows,
|
|
4505
4516
|
listSubagentTellTargetIds,
|
|
4517
|
+
listSubagentChildTopicIds,
|
|
4506
4518
|
listRuntimeProcessLeases,
|
|
4507
4519
|
listPendingAsksForCaller,
|
|
4508
4520
|
listApiMessagesByKind,
|
|
@@ -4608,4 +4620,4 @@ export {
|
|
|
4608
4620
|
ConversationLogDivergedError
|
|
4609
4621
|
};
|
|
4610
4622
|
|
|
4611
|
-
//# debugId=
|
|
4623
|
+
//# debugId=5DCDC85ECAD4D37764756E2164756E21
|