@pasko70/pibo 2.0.0 → 2.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,108 @@
1
+ import { OMP_ADAPTER_ID } from "./thread.js";
2
+ import { OMP_ADAPTER_VERSION } from "./thread.js";
3
+ function isRecord(value) {
4
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+ export function inspectOmpHistory(input, runtimeInstanceId) {
7
+ const binding = input.binding;
8
+ return {
9
+ runtimeInstanceId,
10
+ adapterId: OMP_ADAPTER_ID,
11
+ bindingState: binding.state,
12
+ available: binding.state === "bound" && Boolean(binding.nativeSessionId),
13
+ ...(binding.locator ? { locator: binding.locator } : {}),
14
+ version: OMP_ADAPTER_VERSION,
15
+ diagnostics: [],
16
+ };
17
+ }
18
+ function messageEntryRole(message) {
19
+ if (isRecord(message)) {
20
+ if (typeof message.role === "string") {
21
+ const role = message.role;
22
+ if (role === "user" || role === "assistant" || role === "tool" || role === "system")
23
+ return role;
24
+ }
25
+ }
26
+ return "system";
27
+ }
28
+ function messageText(message) {
29
+ if (typeof message === "string")
30
+ return message;
31
+ if (!isRecord(message))
32
+ return "";
33
+ if (typeof message.text === "string")
34
+ return message.text;
35
+ if (typeof message.content === "string")
36
+ return message.content;
37
+ if (Array.isArray(message.content)) {
38
+ const parts = [];
39
+ for (const part of message.content) {
40
+ if (isRecord(part) && typeof part.text === "string")
41
+ parts.push(part.text);
42
+ }
43
+ return parts.join("\n");
44
+ }
45
+ return "";
46
+ }
47
+ function toHistoryEntry(message, sequence, binding) {
48
+ const role = messageEntryRole(message);
49
+ const content = messageText(message);
50
+ return {
51
+ id: `${binding.nativeSessionId ?? "omp"}-${sequence}`,
52
+ type: "message",
53
+ source: "native",
54
+ createdAt: isRecord(message) && typeof message.timestamp === "string"
55
+ ? message.timestamp
56
+ : new Date().toISOString(),
57
+ sequence,
58
+ role,
59
+ content: content || "[empty message]",
60
+ };
61
+ }
62
+ export async function readOmpHistory(client, input, runtimeInstanceId, binding) {
63
+ try {
64
+ const result = await client.request({
65
+ type: "get_messages_page",
66
+ ...(input.cursor ? { cursor: input.cursor } : {}),
67
+ ...(input.limit ? { limit: input.limit } : {}),
68
+ }, "get_messages_page");
69
+ const data = result["data"];
70
+ if (!isRecord(data) || !Array.isArray(data.messages)) {
71
+ return emptyPage(runtimeInstanceId);
72
+ }
73
+ const entries = [];
74
+ let sequence = 0;
75
+ for (const message of data.messages) {
76
+ entries.push(toHistoryEntry(message, sequence++, binding));
77
+ }
78
+ return {
79
+ runtimeInstanceId,
80
+ adapterId: OMP_ADAPTER_ID,
81
+ source: "native",
82
+ entries,
83
+ ...(typeof data.nextCursor === "string" ? { nextCursor: data.nextCursor } : {}),
84
+ hasMore: typeof data.nextCursor === "string" && data.nextCursor.length > 0,
85
+ };
86
+ }
87
+ catch {
88
+ return emptyPage(runtimeInstanceId);
89
+ }
90
+ }
91
+ function emptyPage(runtimeInstanceId) {
92
+ return {
93
+ runtimeInstanceId,
94
+ adapterId: OMP_ADAPTER_ID,
95
+ source: "native",
96
+ entries: [],
97
+ hasMore: false,
98
+ };
99
+ }
100
+ export function emptyOmpHistoryPage(runtimeInstanceId) {
101
+ return {
102
+ runtimeInstanceId,
103
+ adapterId: OMP_ADAPTER_ID,
104
+ source: "native",
105
+ entries: [],
106
+ hasMore: false,
107
+ };
108
+ }
@@ -0,0 +1,201 @@
1
+ const DEFAULT_MAX_TOOL_CALL_TIMEOUT_MS = 5 * 60_000;
2
+ // Test hook: lets unit tests exercise the abort-on-timeout path quickly.
3
+ function maxToolCallTimeoutMs() {
4
+ if (typeof process !== "undefined" && process.env?.PIBO_OMP_TOOL_TIMEOUT_MS) {
5
+ const parsed = Number(process.env.PIBO_OMP_TOOL_TIMEOUT_MS);
6
+ if (Number.isFinite(parsed) && parsed > 0)
7
+ return parsed;
8
+ }
9
+ return DEFAULT_MAX_TOOL_CALL_TIMEOUT_MS;
10
+ }
11
+ const MAX_RESULT_BYTES = 8 * 1024 * 1024;
12
+ function isRecord(value) {
13
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
14
+ }
15
+ function stringValue(value) {
16
+ return typeof value === "string" ? value : JSON.stringify(value ?? "");
17
+ }
18
+ /** Resolve the input-schema JSON Schema from a Pibo tool definition. */
19
+ function toolInputSchema(definition) {
20
+ if (isRecord(definition.inputSchema)) {
21
+ return definition.inputSchema;
22
+ }
23
+ if (isRecord(definition.parameters)) {
24
+ return definition.parameters;
25
+ }
26
+ return { type: "object", properties: {} };
27
+ }
28
+ /** Flatten a Pibo tool result's content parts into host-tool text content. */
29
+ function flattenToolResult(result) {
30
+ const parts = [];
31
+ for (const item of result.content ?? []) {
32
+ if (item.type === "text") {
33
+ parts.push(item.text);
34
+ }
35
+ else if (item.type === "image") {
36
+ parts.push("[image content]");
37
+ }
38
+ else if ("text" in item) {
39
+ parts.push(String(item.text ?? ""));
40
+ }
41
+ else {
42
+ parts.push(JSON.stringify(item));
43
+ }
44
+ }
45
+ const content = parts.join("\n");
46
+ return result.isError ? { content, isError: true } : { content };
47
+ }
48
+ /**
49
+ * Bridges Pibo's portable tool session over OMP's host-tool RPC protocol.
50
+ *
51
+ * - `install` sends Pibo's tool definitions via `set_host_tools` so OMP mounts
52
+ * them directly (matching by name).
53
+ * - `handleFrame` processes `host_tool_call` / `host_tool_cancel` frames from
54
+ * OMP, executes the underlying Pibo tool (same contract the Pi direct
55
+ * compiler uses), and replies with `host_tool_result`.
56
+ *
57
+ * OMP preserves its native tools and base prompt; Pibo adds only its own
58
+ * portable tools, and only Pibo-hosted tools are governed by Pibo.
59
+ */
60
+ export class OmpHostToolBridge {
61
+ client;
62
+ portableTools;
63
+ executionContext;
64
+ emitWarning;
65
+ tools = new Map();
66
+ activeCalls = new Map();
67
+ dispossed = false;
68
+ constructor(client, portableTools, executionContext, emitWarning) {
69
+ this.client = client;
70
+ this.portableTools = portableTools;
71
+ this.executionContext = executionContext;
72
+ this.emitWarning = emitWarning;
73
+ }
74
+ get installedNames() {
75
+ return [...this.tools.keys()];
76
+ }
77
+ /** Send the current Pibo portable tool definitions to OMP via set_host_tools. */
78
+ async install() {
79
+ if (!this.portableTools || this.dispossed) {
80
+ await this.client.request({ type: "set_host_tools", tools: [] }, "set_host_tools");
81
+ return [];
82
+ }
83
+ const definitions = this.portableTools.createDefinitions();
84
+ this.tools.clear();
85
+ const wire = [];
86
+ for (const def of definitions) {
87
+ if (typeof def.name !== "string" || def.name.length === 0 || def.portable === false)
88
+ continue;
89
+ this.tools.set(def.name, def);
90
+ wire.push({
91
+ name: def.name,
92
+ description: typeof def.description === "string" ? def.description : "",
93
+ inputSchema: toolInputSchema(def),
94
+ });
95
+ }
96
+ const result = await this.client.request({ type: "set_host_tools", tools: wire }, "set_host_tools");
97
+ const data = result["data"];
98
+ const toolNames = isRecord(data) && Array.isArray(data.toolNames)
99
+ ? data.toolNames.filter((n) => typeof n === "string")
100
+ : [];
101
+ return toolNames;
102
+ }
103
+ /**
104
+ * Handle a host tool frame from OMP. Returns true when the frame was a host
105
+ * tool call/cancel (consumed); false otherwise.
106
+ */
107
+ handleFrame(frame) {
108
+ if (!isRecord(frame))
109
+ return false;
110
+ if (frame.type === "host_tool_call") {
111
+ void this.handleToolCall(frame);
112
+ return true;
113
+ }
114
+ if (frame.type === "host_tool_cancel") {
115
+ void this.handleToolCancel(frame);
116
+ return true;
117
+ }
118
+ return false;
119
+ }
120
+ async handleToolCall(frame) {
121
+ const toolName = typeof frame.toolName === "string" ? frame.toolName : "unknown";
122
+ const toolCallId = stringValue(frame.toolCallId);
123
+ const definition = this.tools.get(toolName);
124
+ if (!definition) {
125
+ await this.client
126
+ .sendSideChannel({ type: "host_tool_result", toolCallId, result: { content: `error: unknown host tool "${toolName}"`, isError: true } }, "host_tool_result")
127
+ .catch(() => { });
128
+ return;
129
+ }
130
+ const controller = new AbortController();
131
+ this.activeCalls.set(toolCallId, controller);
132
+ let timeoutHandle;
133
+ // Forward incremental Pibo tool progress as host_tool_update when the tool
134
+ // callback provides it.
135
+ const onUpdate = definition.portable === false ? undefined : (update) => {
136
+ void this.client
137
+ .sendSideChannel({ type: "host_tool_update", toolCallId, partialResult: update }, "host_tool_update")
138
+ .catch(() => { });
139
+ };
140
+ try {
141
+ const prepared = definition.prepareInput ? definition.prepareInput(frame.arguments) : frame.arguments;
142
+ const result = await Promise.race([
143
+ definition.execute(toolCallId, prepared, controller.signal, onUpdate, this.executionContext),
144
+ new Promise((_, reject) => {
145
+ timeoutHandle = setTimeout(() => {
146
+ // Abort the underlying Pibo tool so timeout doesn't leak a
147
+ // still-running background execution.
148
+ controller.abort();
149
+ reject(new Error(`OMP host tool "${toolName}" timed out.`));
150
+ }, maxToolCallTimeoutMs());
151
+ }),
152
+ ]);
153
+ clearTimeout(timeoutHandle);
154
+ const wire = this.sanitize(flattenToolResult(result));
155
+ await this.client.sendSideChannel({ type: "host_tool_result", toolCallId, result: wire }, "host_tool_result");
156
+ }
157
+ catch (error) {
158
+ const message = error instanceof Error ? error.message : String(error);
159
+ this.emitWarning(`OMP host tool "${toolName}" failed: ${message}`);
160
+ try {
161
+ await this.client.sendSideChannel({ type: "host_tool_result", toolCallId, result: { content: `error: ${message}`, isError: true } }, "host_tool_result");
162
+ }
163
+ catch {
164
+ // best-effort; the client may already be closing
165
+ }
166
+ }
167
+ finally {
168
+ if (timeoutHandle)
169
+ clearTimeout(timeoutHandle);
170
+ this.activeCalls.delete(toolCallId);
171
+ }
172
+ }
173
+ async handleToolCancel(frame) {
174
+ const toolCallId = stringValue(frame.toolCallId);
175
+ const active = this.activeCalls.get(toolCallId);
176
+ if (active)
177
+ active.abort();
178
+ this.activeCalls.delete(toolCallId);
179
+ }
180
+ sanitize(result) {
181
+ const serialized = JSON.stringify(result.content ?? "");
182
+ if (Buffer.byteLength(serialized, "utf8") > MAX_RESULT_BYTES) {
183
+ return { content: "[result truncated by Pibo]", isError: result.isError };
184
+ }
185
+ return result;
186
+ }
187
+ /** Cancel all in-flight host tool calls (used on abort/dispose). */
188
+ async cancelAll() {
189
+ for (const controller of this.activeCalls.values())
190
+ controller.abort();
191
+ this.activeCalls.clear();
192
+ }
193
+ dispose() {
194
+ if (this.dispossed)
195
+ return;
196
+ this.dispossed = true;
197
+ for (const controller of this.activeCalls.values())
198
+ controller.abort();
199
+ this.activeCalls.clear();
200
+ }
201
+ }
@@ -0,0 +1,86 @@
1
+ /** OMP thinking levels (from ai/types.ts ThinkingLevel). */
2
+ export const OMP_REASONING_VALUES = ["none", "low", "medium", "high"];
3
+ export const OMP_MODEL_PROVIDER_ID = "omp";
4
+ export const OMP_MODEL_OPTIONS_SCHEMA = {
5
+ type: "object",
6
+ additionalProperties: false,
7
+ properties: {
8
+ provider: { type: "string" },
9
+ modelId: { type: "string" },
10
+ thinkingLevel: { type: "string", enum: [...OMP_REASONING_VALUES] },
11
+ },
12
+ };
13
+ function isRecord(value) {
14
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+ export function toAgentRuntimeModelCatalog(runtimeInstanceId, models) {
17
+ return {
18
+ runtimeInstanceId,
19
+ models: models.map(toAgentRuntimeModelInfo),
20
+ };
21
+ }
22
+ export function toAgentRuntimeModelInfo(model) {
23
+ return {
24
+ id: model.id,
25
+ provider: model.provider ?? OMP_MODEL_PROVIDER_ID,
26
+ displayName: model.name ?? model.id,
27
+ reasoningOptions: model.reasoning || model.thinking ? [...OMP_REASONING_VALUES] : undefined,
28
+ options: {
29
+ provider: model.provider ?? OMP_MODEL_PROVIDER_ID,
30
+ modelId: model.id,
31
+ },
32
+ };
33
+ }
34
+ export async function readOmpModelCatalog(client, runtimeInstanceId) {
35
+ try {
36
+ const result = await client.request({ type: "get_available_models" }, "get_available_models");
37
+ const data = result["data"];
38
+ if (data && typeof data === "object" && !Array.isArray(data) && "models" in data) {
39
+ const models = data.models;
40
+ if (Array.isArray(models)) {
41
+ const descriptors = [];
42
+ for (const item of models) {
43
+ if (!isRecord(item))
44
+ continue;
45
+ if (typeof item.id !== "string" || item.id.length === 0)
46
+ continue;
47
+ descriptors.push({
48
+ provider: typeof item.provider === "string" ? item.provider : undefined,
49
+ id: item.id,
50
+ name: typeof item.name === "string" ? item.name : undefined,
51
+ reasoning: item.reasoning === true,
52
+ thinking: item.thinking === true,
53
+ contextWindow: typeof item.contextWindow === "number" ? item.contextWindow : undefined,
54
+ });
55
+ }
56
+ return toAgentRuntimeModelCatalog(runtimeInstanceId, descriptors);
57
+ }
58
+ }
59
+ }
60
+ catch {
61
+ // Model discovery is best-effort in fixtures; empty catalog is acceptable.
62
+ }
63
+ return { runtimeInstanceId, models: [] };
64
+ }
65
+ export async function setOmpModel(client, provider, modelId) {
66
+ const result = await client.request({ type: "set_model", provider, modelId }, "set_model");
67
+ const data = result["data"];
68
+ if (isRecord(data) && typeof data.id === "string") {
69
+ return toAgentRuntimeModelInfo({
70
+ provider: typeof data.provider === "string" ? data.provider : provider,
71
+ id: data.id,
72
+ name: typeof data.name === "string" ? data.name : undefined,
73
+ });
74
+ }
75
+ return { id: modelId, provider };
76
+ }
77
+ export async function setOmpThinkingLevel(client, level) {
78
+ await client.request({ type: "set_thinking_level", level }, "set_thinking_level");
79
+ }
80
+ export function parseOmpReasoning(level) {
81
+ if (typeof level === "string" && level.length > 0) {
82
+ const normalized = OMP_REASONING_VALUES.includes(level) ? level : "medium";
83
+ return { value: normalized, availableValues: [...OMP_REASONING_VALUES], supported: true };
84
+ }
85
+ return { availableValues: [...OMP_REASONING_VALUES], supported: true };
86
+ }
@@ -0,0 +1,283 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomUUID } from "node:crypto";
3
+ import { mkdir, rm, stat, writeFile } from "node:fs/promises";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { OmpRpcClient } from "./client.js";
6
+ const MAX_VERSION_OUTPUT_BYTES = 256 * 1024;
7
+ const PRIVATE_DIRECTORY_MODE = 0o700;
8
+ const PRIVATE_FILE_MODE = 0o600;
9
+ const RESOURCE_ENVIRONMENT_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
10
+ export class OmpProcessError extends Error {
11
+ code;
12
+ constructor(code, message, options = {}) {
13
+ super(message, options);
14
+ this.code = code;
15
+ this.name = "OmpProcessError";
16
+ }
17
+ }
18
+ function safeSegment(value) {
19
+ return value.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
20
+ }
21
+ function nodeErrorCode(error) {
22
+ return error instanceof Error && "code" in error && typeof error.code === "string"
23
+ ? (error.code ?? undefined)
24
+ : undefined;
25
+ }
26
+ async function ensurePrivateDirectory(path) {
27
+ await mkdir(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
28
+ }
29
+ async function ensurePrivateConfig(path) {
30
+ await mkdir(dirname(path), { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
31
+ try {
32
+ await stat(path);
33
+ }
34
+ catch {
35
+ await writeFile(path, "", { mode: PRIVATE_FILE_MODE });
36
+ }
37
+ }
38
+ export async function prepareOmpInstancePaths(config, runtimeInstanceId) {
39
+ const root = resolve(config.homeRoot, safeSegment(runtimeInstanceId) || "orp");
40
+ return {
41
+ root,
42
+ agentDir: join(root, "agent"),
43
+ config: join(root, "agent", "config.yml"),
44
+ skills: join(root, "agent", "skills"),
45
+ context: join(root, "agent", "context"),
46
+ };
47
+ }
48
+ export async function prepareOmpSessionPaths(input) {
49
+ const instanceKey = safeSegment(input.runtimeInstanceId) || "orp";
50
+ const sessionKey = safeSegment(input.piboSessionId) || "session";
51
+ const generationKey = safeSegment(input.sessionGeneration) || "gen";
52
+ // Instance root is shared/persistent; never delete it on session dispose.
53
+ const instanceRoot = resolve(input.config.homeRoot, instanceKey);
54
+ // Config/agent state is per-session (not shared) so one session's dispose can
55
+ // never destroy another session's config or transcript. agentDir/config live
56
+ // under a per-piboSessionId directory; sessionDir (OMP's transcript store) is
57
+ // stable across generations for resume.
58
+ const sessionRoot = join(instanceRoot, "sessions", sessionKey);
59
+ const paths = {
60
+ root: join(sessionRoot, generationKey),
61
+ agentDir: join(sessionRoot, "agent"),
62
+ config: join(sessionRoot, "agent", "config.yml"),
63
+ skills: join(sessionRoot, "agent", "skills"),
64
+ context: join(sessionRoot, "agent", "context"),
65
+ piboSessionId: input.piboSessionId,
66
+ sessionGeneration: input.sessionGeneration,
67
+ // OMP transcript store: stable across generations so a resumed session
68
+ // re-attaches to the same native session files.
69
+ sessionDir: join(sessionRoot, "omp-sessions"),
70
+ };
71
+ await ensurePrivateDirectory(instanceRoot);
72
+ await ensurePrivateDirectory(paths.agentDir);
73
+ await ensurePrivateDirectory(paths.skills);
74
+ await ensurePrivateDirectory(paths.context);
75
+ await ensurePrivateDirectory(paths.sessionDir);
76
+ await ensurePrivateConfig(paths.config);
77
+ return paths;
78
+ }
79
+ /**
80
+ * Release the ephemeral session generation directory. The per-session agent
81
+ * dir, config.yml, and OMP transcript store are deliberately NOT removed — they
82
+ * belong to the Pibo Session across generations and are needed for resume.
83
+ */
84
+ export async function disposeOmpSessionPaths(paths) {
85
+ await rm(paths.root, { recursive: true, force: true });
86
+ }
87
+ /**
88
+ * Build the environment for the OMP child. The agent dir is redirected via
89
+ * `PI_CODING_AGENT_DIR` (and `PI_CONFIG_DIR`) so all OMP user-global state is
90
+ * isolated under the Pibo-owned home — the real `~/.omp` is never touched.
91
+ * Only allow-listed keys are inherited; provider API keys are passed through
92
+ * from the allowlisted key set (model auth is provider-config via models.yml +
93
+ * env).
94
+ */
95
+ export function buildOmpProcessEnvironment(input) {
96
+ const base = input.baseEnvironment ?? process.env;
97
+ // PI_CODING_AGENT_DIR is the authoritative override for ~/.omp (getAgentDir).
98
+ // We must NOT set OMP_PROFILE: it relocates getAgentDir() under
99
+ // <agentDir>/profiles/<name>, which would break the config.yml lookup (config
100
+ // is written at <agentDir>/config.yml). No PI_CONFIG_DIR either — OMP does not
101
+ // use it to locate config.
102
+ const env = {
103
+ PI_CODING_AGENT_DIR: input.paths.agentDir,
104
+ PI_NO_TITLE: "1",
105
+ };
106
+ const allowlist = new Set([...input.config.environmentAllowlist, ...input.config.apiKeyEnvironment]);
107
+ for (const [key, value] of Object.entries(base)) {
108
+ if (allowlist.has(key) && value !== undefined) {
109
+ env[key] = value;
110
+ }
111
+ }
112
+ return env;
113
+ }
114
+ async function probeOmpVersion(config, environment) {
115
+ return await new Promise((resolveProbe) => {
116
+ const command = ["--version"];
117
+ let child;
118
+ try {
119
+ child = spawn(config.bunExecutable, [config.ompEntry, ...command], {
120
+ env: environment,
121
+ stdio: ["ignore", "pipe", "pipe"],
122
+ });
123
+ }
124
+ catch (error) {
125
+ resolveProbe({ status: "failed", errorCode: nodeErrorCode(error) });
126
+ return;
127
+ }
128
+ let settled = false;
129
+ let bytes = 0;
130
+ const chunks = [];
131
+ const settle = (result) => {
132
+ if (settled)
133
+ return;
134
+ settled = true;
135
+ clearTimeout(timer);
136
+ resolveProbe(result);
137
+ };
138
+ const collect = (chunk) => {
139
+ bytes += chunk.length;
140
+ if (bytes > MAX_VERSION_OUTPUT_BYTES) {
141
+ child?.kill("SIGKILL");
142
+ settle({ status: "too_large" });
143
+ return;
144
+ }
145
+ chunks.push(Buffer.from(chunk));
146
+ };
147
+ child.stdout?.on("data", collect);
148
+ child.stderr?.on("data", collect);
149
+ child.once("error", (error) => {
150
+ settle(error.code === "ENOENT" ? { status: "missing" } : { status: "failed", errorCode: nodeErrorCode(error) });
151
+ });
152
+ child.once("close", (code) => {
153
+ if (settled)
154
+ return;
155
+ if (code !== 0) {
156
+ settle({ status: "failed", exitCode: code });
157
+ return;
158
+ }
159
+ settle({ status: "ok", output: Buffer.concat(chunks).toString("utf8") });
160
+ });
161
+ const timer = setTimeout(() => {
162
+ child?.kill("SIGKILL");
163
+ settle({ status: "timeout" });
164
+ }, config.diagnosticTimeoutMs);
165
+ });
166
+ }
167
+ export async function diagnoseOmpRuntime(config, runtimeInstanceId, options = {}) {
168
+ const diagnostics = [];
169
+ if (!config.ompEntry) {
170
+ diagnostics.push({
171
+ severity: "error",
172
+ code: "omp_entry_unset",
173
+ message: `No OMP CLI entry is configured for runtime instance "${runtimeInstanceId}".`,
174
+ path: "config.ompEntry",
175
+ });
176
+ return diagnostics;
177
+ }
178
+ let paths;
179
+ try {
180
+ paths = await prepareOmpSessionPaths({
181
+ config,
182
+ runtimeInstanceId,
183
+ piboSessionId: "runtime-diagnostics",
184
+ sessionGeneration: `version-probe-${randomUUID()}`,
185
+ });
186
+ }
187
+ catch (error) {
188
+ const errorCode = nodeErrorCode(error);
189
+ diagnostics.push({
190
+ severity: "error",
191
+ code: "omp_home_unavailable",
192
+ message: `Private OMP state is unavailable for runtime instance "${runtimeInstanceId}".`,
193
+ path: "config.homeRoot",
194
+ ...(errorCode ? { details: { errorCode } } : {}),
195
+ });
196
+ return diagnostics;
197
+ }
198
+ diagnostics.push({
199
+ severity: "info",
200
+ code: "omp_home_ready",
201
+ message: `Private OMP state is ready for runtime instance "${runtimeInstanceId}".`,
202
+ details: { scope: "configured-instance", private: true },
203
+ });
204
+ let probe;
205
+ try {
206
+ probe = await probeOmpVersion(config, buildOmpProcessEnvironment({ paths, config, baseEnvironment: options.baseEnvironment ?? process.env }));
207
+ }
208
+ finally {
209
+ await disposeOmpSessionPaths(paths);
210
+ }
211
+ if (probe.status === "missing") {
212
+ diagnostics.push({
213
+ severity: "error",
214
+ code: "omp_bun_not_found",
215
+ message: `Bun executable is not available for runtime instance "${runtimeInstanceId}".`,
216
+ path: "config.bunExecutable",
217
+ });
218
+ return diagnostics;
219
+ }
220
+ if (probe.status === "timeout") {
221
+ diagnostics.push({
222
+ severity: "error",
223
+ code: "omp_version_probe_timeout",
224
+ message: `OMP version inspection timed out for runtime instance "${runtimeInstanceId}".`,
225
+ path: "config.diagnosticTimeoutMs",
226
+ });
227
+ return diagnostics;
228
+ }
229
+ if (probe.status === "too_large") {
230
+ diagnostics.push({
231
+ severity: "error",
232
+ code: "omp_version_probe_too_large",
233
+ message: `OMP version inspection produced too much output for runtime instance "${runtimeInstanceId}".`,
234
+ });
235
+ return diagnostics;
236
+ }
237
+ if (probe.status === "failed") {
238
+ diagnostics.push({
239
+ severity: "error",
240
+ code: "omp_version_probe_failed",
241
+ message: `OMP CLI version inspection failed for runtime instance "${runtimeInstanceId}".`,
242
+ ...(probe.exitCode !== undefined || probe.errorCode
243
+ ? {
244
+ details: {
245
+ ...(probe.exitCode !== undefined ? { exitCode: probe.exitCode } : {}),
246
+ ...(probe.errorCode ? { errorCode: probe.errorCode } : {}),
247
+ },
248
+ }
249
+ : {}),
250
+ });
251
+ return diagnostics;
252
+ }
253
+ const version = probe.output.trim().split("\n").slice(-1)[0] ?? "";
254
+ diagnostics.push({
255
+ severity: "info",
256
+ code: "omp_version_ok",
257
+ message: `OMP CLI is available for runtime instance "${runtimeInstanceId}".`,
258
+ details: { version: version || "unknown", private: true },
259
+ });
260
+ return diagnostics;
261
+ }
262
+ export async function startOmpProcess(input) {
263
+ const client = new OmpRpcClient({
264
+ startupTimeoutMs: input.startupTimeoutMs,
265
+ requestTimeoutMs: input.requestTimeoutMs,
266
+ });
267
+ await client.connect(Array.from(input.ompCommand), {
268
+ cwd: input.cwd,
269
+ env: input.environment,
270
+ });
271
+ return client;
272
+ }
273
+ /**
274
+ * Build the OMP child command. ompEntry is validated as an absolute path at
275
+ * config-parse time (config.ompEntry), so the command is never resolved against
276
+ * the arbitrary session workspace.
277
+ */
278
+ export function resolveOmpCommand(config, paths) {
279
+ if (!config.ompEntry) {
280
+ throw new Error("OMP CLI entry is not configured; set config.ompEntry to an absolute CLI path.");
281
+ }
282
+ return [config.bunExecutable, config.ompEntry, "--mode", "rpc", "--session-dir", paths.sessionDir];
283
+ }