@pasko70/pibo 2.5.2 → 3.0.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.
Files changed (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +464 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -1,11 +1,13 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { piboHomePath } from "./pibo-home.js";
4
+ import { sanitizePreviewServerSettings } from "./preview-server-settings.js";
4
5
  import { sanitizeTelemetryRetentionSettings } from "./telemetry-retention-settings.js";
5
6
  import { sanitizeTelemetryStaleThresholdSettings } from "./telemetry-staleness.js";
6
7
  export const DEFAULT_USER_TIMEZONE = "UTC";
7
8
  export const DEFAULT_WEB_ANNOTATIONS_TOGGLE_SHORTCUT = "Alt+Shift+A";
8
9
  export const DEFAULT_TRANSCRIPTION_PROVIDER_ID = "openai-chatgpt";
10
+ export const DEFAULT_SPEECH_PROVIDER_ID = "openai-codex";
9
11
  const LEGACY_OPENAI_TRANSCRIPTION_PROVIDER_ID = "openai";
10
12
  export const DEFAULT_PIBO_USER_SETTINGS_PATH = "user-settings.json";
11
13
  export function loadPiboUserSettings() {
@@ -56,6 +58,12 @@ export function sanitizeTranscriptionSettings(value) {
56
58
  : providerId ?? DEFAULT_TRANSCRIPTION_PROVIDER_ID,
57
59
  };
58
60
  }
61
+ export function sanitizeSpeechSettings(value) {
62
+ const raw = value && typeof value === "object" && !Array.isArray(value)
63
+ ? value
64
+ : {};
65
+ return { providerId: sanitizeTranscriptionProviderId(raw.providerId) ?? DEFAULT_SPEECH_PROVIDER_ID };
66
+ }
59
67
  export function sanitizeTimezone(value) {
60
68
  if (typeof value !== "string")
61
69
  return undefined;
@@ -78,6 +86,8 @@ function sanitizeUserSettings(value) {
78
86
  timezone: sanitizeTimezone(raw.timezone) ?? DEFAULT_USER_TIMEZONE,
79
87
  shortcuts: sanitizeShortcutSettings(raw.shortcuts),
80
88
  transcription: sanitizeTranscriptionSettings(raw.transcription),
89
+ speech: sanitizeSpeechSettings(raw.speech),
90
+ previewServers: sanitizePreviewServerSettings(raw.previewServers),
81
91
  telemetryStaleThresholds: sanitizeTelemetryStaleThresholdSettings(raw.telemetryStaleThresholds),
82
92
  telemetryRetention: sanitizeTelemetryRetentionSettings(raw.telemetryRetention),
83
93
  };
@@ -1,6 +1,6 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { gunzipSync, gzipSync } from "node:zlib";
3
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { closeSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, readSync, renameSync, rmSync, writeFileSync } from "node:fs";
4
4
  import { dirname, extname, join, resolve } from "node:path";
5
5
  import { piboHomePath } from "../core/pibo-home.js";
6
6
  const MAX_SYNC_PAYLOAD_GZIP_BYTES = 512 * 1024;
@@ -78,11 +78,67 @@ export class PayloadStore {
78
78
  readPayloadJson(id) {
79
79
  return JSON.parse(this.readPayloadText(id));
80
80
  }
81
+ readPayloadBytesBounded(id, maxBytes) {
82
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
83
+ throw new RangeError("maxBytes must be a positive safe integer");
84
+ const payload = this.getPayload(id);
85
+ if (!payload)
86
+ throw new Error(`Unknown payload \"${id}\"`);
87
+ if (!payload.storagePath)
88
+ throw new Error(`Payload \"${id}\" has no storage path`);
89
+ if (payload.byteSize > maxBytes)
90
+ throw new RangeError(`Payload \"${id}\" exceeds the ${maxBytes}-byte read limit`);
91
+ const absolutePath = this.rootDir === ":memory:" ? payload.storagePath : join(this.rootDir, payload.storagePath);
92
+ const storedBytes = readFileBounded(absolutePath, maxBytes + 64 * 1024);
93
+ let bytes;
94
+ if (payload.encoding === "gzip")
95
+ bytes = gunzipSync(storedBytes, { maxOutputLength: maxBytes });
96
+ else if (payload.encoding === "identity")
97
+ bytes = storedBytes;
98
+ else
99
+ throw new Error(`Unsupported payload encoding \"${payload.encoding}\"`);
100
+ if (bytes.byteLength !== payload.byteSize)
101
+ throw new Error(`Payload \"${id}\" byte length does not match its metadata`);
102
+ const sha256 = createHash("sha256").update(bytes).digest("hex");
103
+ if (sha256 !== payload.sha256)
104
+ throw new Error(`Payload \"${id}\" checksum does not match its metadata`);
105
+ return bytes;
106
+ }
107
+ readPayloadJsonBounded(id, maxBytes) {
108
+ return JSON.parse(Buffer.from(this.readPayloadBytesBounded(id, maxBytes)).toString("utf8"));
109
+ }
81
110
  findBySha256(sha256) {
82
111
  const row = this.db.prepare("SELECT * FROM payloads WHERE sha256 = ?").get(sha256);
83
112
  return row ? payloadFromRow(row) : undefined;
84
113
  }
85
114
  }
115
+ function readFileBounded(path, maxBytes) {
116
+ const descriptor = openSync(path, "r");
117
+ try {
118
+ const stats = fstatSync(descriptor);
119
+ if (!stats.isFile())
120
+ throw new Error(`Payload path is not a file: ${path}`);
121
+ if (stats.size > maxBytes)
122
+ throw new RangeError(`Stored payload exceeds the ${maxBytes}-byte read limit`);
123
+ const bytes = Buffer.alloc(stats.size);
124
+ let offset = 0;
125
+ while (offset < bytes.byteLength) {
126
+ const read = readSync(descriptor, bytes, offset, bytes.byteLength - offset, offset);
127
+ if (read === 0)
128
+ break;
129
+ offset += read;
130
+ }
131
+ if (offset !== bytes.byteLength)
132
+ throw new Error(`Stored payload changed while it was being read: ${path}`);
133
+ const extra = Buffer.alloc(1);
134
+ if (readSync(descriptor, extra, 0, 1, offset) !== 0)
135
+ throw new RangeError(`Stored payload exceeds the ${maxBytes}-byte read limit`);
136
+ return bytes;
137
+ }
138
+ finally {
139
+ closeSync(descriptor);
140
+ }
141
+ }
86
142
  function defaultContentType(value) {
87
143
  if (typeof value === "string")
88
144
  return "text/plain; charset=utf-8";
@@ -1,4 +1,4 @@
1
- export const PIBO_DATA_SCHEMA_VERSION = 5;
1
+ export const PIBO_DATA_SCHEMA_VERSION = 6;
2
2
  export function applyPiboDataSchema(db) {
3
3
  const previousVersion = Number(db.prepare("PRAGMA user_version").get()?.user_version ?? 0);
4
4
  const existingSessionCount = db.prepare("SELECT COUNT(*) AS count FROM sqlite_schema WHERE type = 'table' AND name = 'sessions'").get();
@@ -423,6 +423,23 @@ export function applyPiboDataSchema(db) {
423
423
  ON event_log(session_id, session_sequence DESC, stream_id DESC);
424
424
  CREATE INDEX IF NOT EXISTS idx_event_log_session_type_sequence_stream
425
425
  ON event_log(session_id, type, session_sequence ASC, stream_id ASC);
426
+ CREATE INDEX IF NOT EXISTS idx_event_log_session_tool_event_sequence_stream
427
+ ON event_log(session_id, tool_call_id, event_id, session_sequence ASC, stream_id ASC)
428
+ WHERE tool_call_id IS NOT NULL
429
+ AND type IN ('tool_execution_started', 'tool_execution_updated', 'tool_execution_finished');
430
+ CREATE INDEX IF NOT EXISTS idx_event_log_session_tool_sequence_stream
431
+ ON event_log(session_id, tool_call_id, session_sequence ASC, stream_id ASC)
432
+ WHERE tool_call_id IS NOT NULL
433
+ AND type IN ('tool_execution_started', 'tool_execution_updated', 'tool_execution_finished');
434
+ CREATE INDEX IF NOT EXISTS idx_event_log_session_attribute_tool_event_sequence_stream
435
+ ON event_log(
436
+ session_id,
437
+ json_extract(attributes_json, '$.toolCallId'),
438
+ event_id,
439
+ session_sequence ASC,
440
+ stream_id ASC
441
+ )
442
+ WHERE type IN ('tool_execution_started', 'tool_execution_updated', 'tool_execution_finished');
426
443
  CREATE INDEX IF NOT EXISTS idx_event_log_room_stream
427
444
  ON event_log(room_id, stream_id);
428
445
  CREATE INDEX IF NOT EXISTS idx_event_log_topic_stream
@@ -46,10 +46,14 @@ export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
46
46
  const productHistory = readProductHistoryEntries(chatDb, payloadStore, piboSessionId);
47
47
  let nativeHistoryEntries = [];
48
48
  let nativeHistoryInspection;
49
+ let nativeHistoryReconciliationProof;
49
50
  if (options.nativeHistory || (events.length === 0 && productHistory.length === 0)) {
50
51
  const native = await readDebugNativeHistory(binding, session.workspace ?? process.cwd(), adapterIssues);
51
52
  nativeHistoryEntries = native?.entries ?? [];
52
53
  nativeHistoryInspection = native?.inspection;
54
+ nativeHistoryReconciliationProof = native
55
+ ? native.reconciliationProof ?? { complete: false, entries: native.entries }
56
+ : undefined;
53
57
  }
54
58
  const historyEntries = nativeHistoryEntries.length ? nativeHistoryEntries : productHistory;
55
59
  const historySource = nativeHistoryEntries.length
@@ -62,6 +66,7 @@ export async function inspectDebugTrace(piboSessionId, stores, options = {}) {
62
66
  events,
63
67
  historyEntries,
64
68
  historyInspection: nativeHistoryInspection,
69
+ historyReconciliationProof: nativeHistoryEntries.length ? nativeHistoryReconciliationProof : undefined,
65
70
  status: sessionStatus.status,
66
71
  });
67
72
  const rows = flattenTraceNodes(view.nodes);
@@ -4,6 +4,44 @@ import readline from "node:readline/promises";
4
4
  import { stdin as input, stdout as output } from "node:process";
5
5
  import { DEFAULT_GATEWAY_HOST, DEFAULT_GATEWAY_PORT, encodeFrame, } from "./protocol.js";
6
6
  import { parsePiboThinkingLevel } from "../core/thinking.js";
7
+ const DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS = 5_000;
8
+ const DEFAULT_TERMINAL_TIMEOUT_MS = 120_000;
9
+ export class GatewayClientExpectedError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "GatewayClientExpectedError";
13
+ }
14
+ }
15
+ export function isGatewayClientExpectedError(error) {
16
+ return error instanceof GatewayClientExpectedError;
17
+ }
18
+ function errorMessage(error) {
19
+ return error instanceof Error ? error.message : String(error);
20
+ }
21
+ export function parseGatewayClientMessage(value) {
22
+ const text = value.trim();
23
+ if (!text)
24
+ return { ok: false, error: "Message text is required" };
25
+ const command = /^\/(queue|steer)(?:\s+|$)/.exec(text);
26
+ if (!command)
27
+ return { ok: true, text };
28
+ const delivery = command[1];
29
+ const message = text.slice(command[0].length).trim();
30
+ return message
31
+ ? { ok: true, text: message, delivery }
32
+ : { ok: false, error: `Usage: /${delivery} <message>` };
33
+ }
34
+ export function createGatewayClientRequestFrame(event) {
35
+ const id = randomUUID();
36
+ return { type: "req", id, event: { ...event, id: event.id ?? id } };
37
+ }
38
+ export function createGatewayClientSubscriptionFrame(piboSessionId) {
39
+ return {
40
+ type: "subscribe",
41
+ id: randomUUID(),
42
+ subscription: { type: "session", piboSessionId },
43
+ };
44
+ }
7
45
  function parseJsonLine(line) {
8
46
  try {
9
47
  return JSON.parse(line);
@@ -16,9 +54,22 @@ function writeFrame(socket, frame) {
16
54
  socket.write(encodeFrame(frame));
17
55
  }
18
56
  function printResponse(frame) {
19
- if (frame.ok)
57
+ if (!frame.ok) {
58
+ console.error(`\nerror: ${frame.error?.message ?? "request failed"}`);
59
+ return;
60
+ }
61
+ if (!frame.payload || typeof frame.payload !== "object")
20
62
  return;
21
- console.error(`\nerror: ${frame.error?.message ?? "request failed"}`);
63
+ const payload = frame.payload;
64
+ if (payload.type !== "message_steered" || typeof payload.activeEventId !== "string" || payload.activeEventId.length === 0)
65
+ return;
66
+ console.error(`steer: delivered to active turn ${payload.activeEventId}`);
67
+ }
68
+ function printGatewayClientHelp() {
69
+ console.error("messages queue by default; /queue <message> queues explicitly; /steer <message> steers the active turn");
70
+ console.error("commands: /status, /clear, /abort, /thinking [level], /thinking-show, /goal [command], /help, /quit");
71
+ console.error("piped EOF waits for acknowledgements and terminal completion of every accepted message while assistant output keeps streaming");
72
+ console.error("security: raw gateway TCP is unauthenticated and unencrypted; use remote hosts only on trusted networks or through a secure tunnel");
22
73
  }
23
74
  function isSessionStatus(value) {
24
75
  if (!value || typeof value !== "object")
@@ -109,26 +160,237 @@ function printEvent(frame, piboSessionId, state) {
109
160
  output.write("assistant> ");
110
161
  }
111
162
  }
163
+ function isExpectedSubscriptionResponse(frame, piboSessionId) {
164
+ if (!frame.payload || typeof frame.payload !== "object")
165
+ return false;
166
+ const payload = frame.payload;
167
+ return payload.subscription?.type === "session" && payload.subscription.piboSessionId === piboSessionId;
168
+ }
169
+ function steeringTargetEventId(frame) {
170
+ if (!frame.payload || typeof frame.payload !== "object")
171
+ return undefined;
172
+ const payload = frame.payload;
173
+ return payload.type === "message_steered" && typeof payload.activeEventId === "string" && payload.activeEventId.length > 0
174
+ ? payload.activeEventId
175
+ : undefined;
176
+ }
177
+ function handleGatewayClientLine(value, sendRequest, piboSessionId, renderState) {
178
+ const text = value.trim();
179
+ if (!text)
180
+ return "continue";
181
+ if (text === "/quit" || text === "/exit")
182
+ return "quit";
183
+ if (text === "/help") {
184
+ printGatewayClientHelp();
185
+ return "continue";
186
+ }
187
+ if (text === "/thinking-show") {
188
+ renderState.showThinking = !renderState.showThinking;
189
+ console.error(`thinking display: ${renderState.showThinking ? "on" : "off"}`);
190
+ return "continue";
191
+ }
192
+ if (text === "/thinking" || text.startsWith("/thinking ")) {
193
+ const level = text.slice("/thinking".length).trim();
194
+ let params;
195
+ try {
196
+ params = level ? { level: parsePiboThinkingLevel(level) } : undefined;
197
+ }
198
+ catch (error) {
199
+ console.error(errorMessage(error));
200
+ return "error";
201
+ }
202
+ sendRequest(params === undefined
203
+ ? { type: "execution", piboSessionId, action: "thinking" }
204
+ : { type: "execution", piboSessionId, action: "thinking", params });
205
+ return "continue";
206
+ }
207
+ if (text === "/status" || text === "/clear" || text === "/abort") {
208
+ const action = text === "/status" ? "status" : text === "/clear" ? "clear_queue" : "abort";
209
+ sendRequest({ type: "execution", piboSessionId, action });
210
+ return "continue";
211
+ }
212
+ if (text === "/goal" || text.startsWith("/goal ")) {
213
+ sendRequest({
214
+ type: "execution",
215
+ piboSessionId,
216
+ action: "goal",
217
+ params: { command: text.slice("/goal".length).trim() },
218
+ });
219
+ return "continue";
220
+ }
221
+ const message = parseGatewayClientMessage(text);
222
+ if (!message.ok) {
223
+ console.error(message.error);
224
+ return "error";
225
+ }
226
+ sendRequest({
227
+ type: "message",
228
+ piboSessionId,
229
+ text: message.text,
230
+ source: "user",
231
+ ...(message.delivery ? { delivery: message.delivery } : {}),
232
+ });
233
+ return "continue";
234
+ }
112
235
  export async function runGatewayClient(options = {}) {
236
+ const interactive = input.isTTY === true && output.isTTY === true;
113
237
  const host = options.host ?? DEFAULT_GATEWAY_HOST;
114
238
  const port = options.port ?? DEFAULT_GATEWAY_PORT;
115
- const piboSessionId = options.piboSessionId ?? "default";
239
+ const piboSessionId = (options.piboSessionId ?? "default").trim();
240
+ const acknowledgementTimeoutMs = options.acknowledgementTimeoutMs ?? DEFAULT_ACKNOWLEDGEMENT_TIMEOUT_MS;
241
+ const terminalTimeoutMs = options.terminalTimeoutMs ?? DEFAULT_TERMINAL_TIMEOUT_MS;
242
+ if (!piboSessionId)
243
+ throw new GatewayClientExpectedError("Pibo Session ID must not be empty");
116
244
  const socket = connect({ host, port });
117
245
  socket.setEncoding("utf-8");
118
- await new Promise((resolve, reject) => {
119
- socket.once("connect", resolve);
120
- socket.once("error", reject);
121
- });
122
- console.error(`connected to pibo gateway at ${host}:${port}`);
123
- console.error(`session: ${piboSessionId}`);
124
- console.error("type a message, /status, /clear, /abort, /thinking [level], /thinking-show, or /quit");
246
+ try {
247
+ await new Promise((resolve, reject) => {
248
+ const onError = (error) => reject(error);
249
+ socket.once("error", onError);
250
+ socket.once("connect", () => {
251
+ socket.off("error", onError);
252
+ resolve();
253
+ });
254
+ });
255
+ }
256
+ catch (error) {
257
+ socket.destroy();
258
+ throw new GatewayClientExpectedError(`Could not connect to gateway at ${host}:${port}: ${errorMessage(error)}`);
259
+ }
125
260
  let buffer = "";
261
+ let closing = false;
262
+ let inputEnded = false;
263
+ let connectionFailure;
264
+ let rl;
265
+ let pendingWaiter;
266
+ let pipedInputFailed = false;
267
+ let completedNormally = false;
268
+ const pendingRequests = new Map();
269
+ const terminalOutcomes = new Map();
126
270
  const renderState = {
127
271
  visibleIncomingEventIds: new Set(),
128
272
  sawAssistantDelta: false,
129
273
  showThinking: false,
130
274
  sawThinkingDelta: false,
131
275
  };
276
+ const pendingCount = (kind) => {
277
+ let count = 0;
278
+ for (const request of pendingRequests.values()) {
279
+ if (kind === "acknowledgement" ? !request.acknowledged : request.acknowledged && request.terminalEventId !== undefined) {
280
+ count += 1;
281
+ }
282
+ }
283
+ return count;
284
+ };
285
+ const finishPendingWaiter = (error) => {
286
+ const waiter = pendingWaiter;
287
+ if (!waiter)
288
+ return;
289
+ pendingWaiter = undefined;
290
+ clearTimeout(waiter.timeout);
291
+ if (error)
292
+ waiter.reject(error);
293
+ else
294
+ waiter.resolve();
295
+ };
296
+ const notifyPendingWaiter = () => {
297
+ if (pendingWaiter && pendingCount(pendingWaiter.kind) === 0)
298
+ finishPendingWaiter();
299
+ };
300
+ const waitForPending = async (kind, timeoutMs) => {
301
+ if (connectionFailure)
302
+ throw connectionFailure;
303
+ const count = pendingCount(kind);
304
+ if (count === 0)
305
+ return;
306
+ await new Promise((resolve, reject) => {
307
+ const label = kind === "acknowledgement" ? "gateway acknowledgement" : "terminal message completion";
308
+ pendingWaiter = {
309
+ kind,
310
+ resolve,
311
+ reject,
312
+ timeout: setTimeout(() => {
313
+ finishPendingWaiter(new GatewayClientExpectedError(`Timed out waiting for ${pendingCount(kind)} ${label}(s)`));
314
+ }, timeoutMs),
315
+ };
316
+ });
317
+ };
318
+ const finishRequestTerminal = (request) => {
319
+ if (!request.terminalEventId)
320
+ return;
321
+ const outcome = terminalOutcomes.get(request.terminalEventId);
322
+ if (!outcome)
323
+ return;
324
+ if (!outcome.ok && !interactive)
325
+ pipedInputFailed = true;
326
+ pendingRequests.delete(request.frameId);
327
+ notifyPendingWaiter();
328
+ };
329
+ const recordTerminalOutcome = (event) => {
330
+ if (interactive || (event.type !== "message_finished" && event.type !== "session_error") || !event.eventId)
331
+ return;
332
+ const outcome = event.type === "message_finished"
333
+ ? { ok: true }
334
+ : { ok: false, error: event.error };
335
+ terminalOutcomes.set(event.eventId, outcome);
336
+ for (const request of [...pendingRequests.values()]) {
337
+ if (request.terminalEventId === event.eventId)
338
+ finishRequestTerminal(request);
339
+ }
340
+ };
341
+ const sendRequest = (event) => {
342
+ const frame = createGatewayClientRequestFrame(event);
343
+ pendingRequests.set(frame.id, {
344
+ frameId: frame.id,
345
+ event: frame.event,
346
+ acknowledged: false,
347
+ terminalEventId: !interactive && frame.event.type === "message" && frame.event.delivery !== "steer"
348
+ ? frame.event.id
349
+ : undefined,
350
+ });
351
+ writeFrame(socket, frame);
352
+ };
353
+ const subscriptionFrame = createGatewayClientSubscriptionFrame(piboSessionId);
354
+ let subscribed = false;
355
+ let settleSubscription;
356
+ let subscriptionTimeout;
357
+ const subscription = new Promise((resolve, reject) => {
358
+ settleSubscription = (error) => error ? reject(error) : resolve();
359
+ });
360
+ const finishSubscription = (error) => {
361
+ const settle = settleSubscription;
362
+ if (!settle)
363
+ return;
364
+ settleSubscription = undefined;
365
+ if (subscriptionTimeout)
366
+ clearTimeout(subscriptionTimeout);
367
+ settle(error);
368
+ };
369
+ subscriptionTimeout = setTimeout(() => {
370
+ finishSubscription(new GatewayClientExpectedError(`Timed out subscribing to Pibo Session "${piboSessionId}"`));
371
+ }, acknowledgementTimeoutMs);
372
+ const failConnection = (error) => {
373
+ if (!subscribed) {
374
+ finishSubscription(error);
375
+ return;
376
+ }
377
+ if (closing || connectionFailure || (!interactive && inputEnded && pendingRequests.size === 0))
378
+ return;
379
+ connectionFailure = error;
380
+ finishPendingWaiter(error);
381
+ rl?.close();
382
+ };
383
+ socket.on("error", (error) => {
384
+ failConnection(new GatewayClientExpectedError(`Gateway connection error: ${errorMessage(error)}`));
385
+ });
386
+ socket.on("close", () => {
387
+ const acknowledgements = pendingCount("acknowledgement");
388
+ const terminals = pendingCount("terminal");
389
+ const suffix = acknowledgements > 0 || terminals > 0
390
+ ? ` before ${acknowledgements} acknowledgement(s) and ${terminals} terminal completion(s)`
391
+ : "";
392
+ failConnection(new GatewayClientExpectedError(`Gateway connection closed${suffix}`));
393
+ });
132
394
  socket.on("data", (chunk) => {
133
395
  buffer += chunk;
134
396
  let newlineIndex = buffer.indexOf("\n");
@@ -136,73 +398,114 @@ export async function runGatewayClient(options = {}) {
136
398
  const line = buffer.slice(0, newlineIndex).trim();
137
399
  buffer = buffer.slice(newlineIndex + 1);
138
400
  const frame = line ? parseJsonLine(line) : undefined;
139
- if (frame?.type === "res") {
401
+ if (frame?.type === "res" && frame.id === subscriptionFrame.id) {
402
+ if (!frame.ok) {
403
+ finishSubscription(new GatewayClientExpectedError(frame.error?.message ?? "Gateway rejected the session subscription"));
404
+ }
405
+ else if (!isExpectedSubscriptionResponse(frame, piboSessionId)) {
406
+ finishSubscription(new GatewayClientExpectedError("Gateway returned a mismatched session subscription"));
407
+ }
408
+ else {
409
+ subscribed = true;
410
+ finishSubscription();
411
+ }
412
+ }
413
+ else if (subscribed && frame?.type === "res") {
140
414
  printResponse(frame);
415
+ const request = pendingRequests.get(frame.id);
416
+ if (request) {
417
+ request.acknowledged = true;
418
+ if (!frame.ok) {
419
+ if (!interactive)
420
+ pipedInputFailed = true;
421
+ pendingRequests.delete(frame.id);
422
+ }
423
+ else if (request.event.type === "message") {
424
+ if (request.event.delivery === "steer") {
425
+ const activeEventId = steeringTargetEventId(frame);
426
+ if (!activeEventId) {
427
+ console.error("\nerror: gateway steering acknowledgement did not identify the active turn");
428
+ if (!interactive)
429
+ pipedInputFailed = true;
430
+ pendingRequests.delete(frame.id);
431
+ }
432
+ else if (interactive) {
433
+ pendingRequests.delete(frame.id);
434
+ }
435
+ else {
436
+ request.terminalEventId = activeEventId;
437
+ finishRequestTerminal(request);
438
+ }
439
+ }
440
+ else if (interactive) {
441
+ pendingRequests.delete(frame.id);
442
+ }
443
+ else {
444
+ finishRequestTerminal(request);
445
+ }
446
+ }
447
+ else {
448
+ pendingRequests.delete(frame.id);
449
+ }
450
+ notifyPendingWaiter();
451
+ }
141
452
  }
142
- else if (frame?.type === "event") {
453
+ else if (subscribed && frame?.type === "event") {
143
454
  printEvent(frame, piboSessionId, renderState);
455
+ recordTerminalOutcome(frame.payload);
144
456
  }
145
457
  newlineIndex = buffer.indexOf("\n");
146
458
  }
147
459
  });
148
- const rl = readline.createInterface({ input, output });
460
+ writeFrame(socket, subscriptionFrame);
149
461
  try {
150
- while (!socket.destroyed) {
151
- const text = (await rl.question("you> ")).trim();
152
- if (!text)
153
- continue;
154
- if (text === "/quit" || text === "/exit")
462
+ await subscription;
463
+ }
464
+ catch (error) {
465
+ closing = true;
466
+ socket.destroy();
467
+ throw error;
468
+ }
469
+ if (connectionFailure || socket.destroyed) {
470
+ closing = true;
471
+ socket.destroy();
472
+ throw connectionFailure ?? new GatewayClientExpectedError("Gateway connection closed");
473
+ }
474
+ console.error(`connected to pibo gateway at ${host}:${port}`);
475
+ console.error(`session: ${piboSessionId}`);
476
+ printGatewayClientHelp();
477
+ rl = readline.createInterface({ input, output, terminal: interactive });
478
+ if (interactive) {
479
+ rl.setPrompt("you> ");
480
+ rl.prompt();
481
+ }
482
+ try {
483
+ for await (const line of rl) {
484
+ const result = handleGatewayClientLine(line, sendRequest, piboSessionId, renderState);
485
+ if (result === "error" && !interactive)
486
+ pipedInputFailed = true;
487
+ if (result === "quit")
155
488
  break;
156
- if (text === "/thinking-show") {
157
- renderState.showThinking = !renderState.showThinking;
158
- console.error(`thinking display: ${renderState.showThinking ? "on" : "off"}`);
159
- continue;
160
- }
161
- if (text === "/thinking" || text.startsWith("/thinking ")) {
162
- const level = text.slice("/thinking".length).trim();
163
- let params;
164
- try {
165
- params = level ? { level: parsePiboThinkingLevel(level) } : undefined;
166
- }
167
- catch (error) {
168
- console.error(error instanceof Error ? error.message : String(error));
169
- continue;
170
- }
171
- writeFrame(socket, {
172
- type: "req",
173
- id: randomUUID(),
174
- event: params === undefined
175
- ? { type: "execution", piboSessionId, action: "thinking" }
176
- : { type: "execution", piboSessionId, action: "thinking", params },
177
- });
178
- continue;
179
- }
180
- if (text === "/status" || text === "/clear" || text === "/abort") {
181
- const action = text === "/status" ? "status" : text === "/clear" ? "clear_queue" : "abort";
182
- writeFrame(socket, {
183
- type: "req",
184
- id: randomUUID(),
185
- event: { type: "execution", piboSessionId, action },
186
- });
187
- continue;
188
- }
189
- if (text === "/goal" || text.startsWith("/goal ")) {
190
- writeFrame(socket, {
191
- type: "req",
192
- id: randomUUID(),
193
- event: { type: "execution", piboSessionId, action: "goal", params: { command: text.slice("/goal".length).trim() } },
194
- });
195
- continue;
196
- }
197
- writeFrame(socket, {
198
- type: "req",
199
- id: randomUUID(),
200
- event: { type: "message", piboSessionId, text, source: "user" },
201
- });
489
+ if (interactive && !socket.destroyed)
490
+ rl.prompt();
491
+ }
492
+ inputEnded = true;
493
+ if (!interactive) {
494
+ await waitForPending("acknowledgement", acknowledgementTimeoutMs);
495
+ await waitForPending("terminal", terminalTimeoutMs);
202
496
  }
497
+ completedNormally = true;
203
498
  }
204
499
  finally {
500
+ closing = true;
205
501
  rl.close();
206
- socket.end();
502
+ if (completedNormally)
503
+ socket.end();
504
+ else
505
+ socket.destroy();
207
506
  }
507
+ if (connectionFailure)
508
+ throw connectionFailure;
509
+ if (!interactive && pipedInputFailed)
510
+ process.exitCode = 1;
208
511
  }
@@ -16,6 +16,8 @@ function isJsonValue(value) {
16
16
  return false;
17
17
  }
18
18
  export function isGatewayRequestFrame(value) {
19
+ // Keep this unversioned wire validator compatible with existing clients. Stricter
20
+ // message text, delivery, and identifier validation belongs at client boundaries.
19
21
  if (!value || typeof value !== "object")
20
22
  return false;
21
23
  const frame = value;