@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
@@ -6,6 +6,7 @@ import { OmpRpcClient, OmpRpcResponseError } from "./client.js";
6
6
  import { defaultOmpRuntimeConfig, OMP_RUNTIME_CONFIG_SCHEMA, parseOmpRuntimeConfig } from "./config.js";
7
7
  import { OmpHostToolBridge } from "./host-tools.js";
8
8
  import { emptyOmpHistoryPage, inspectOmpHistory, readOmpHistory } from "./history.js";
9
+ import { historyReconciliationDigest, } from "../../agent-runtime/history.js";
9
10
  import { OMP_MODEL_OPTIONS_SCHEMA, OMP_MODEL_PROVIDER_ID, OMP_REASONING_VALUES, parseOmpReasoning, readOmpModelCatalog, setOmpModel, } from "./models.js";
10
11
  import { buildOmpProcessEnvironment, diagnoseOmpRuntime, disposeOmpSessionPaths, prepareOmpSessionPaths, resetOmpNativeSession, resolveOmpCommand, } from "./process.js";
11
12
  import { OmpResourceDelivery } from "./resource-delivery.js";
@@ -14,6 +15,32 @@ import { OmpRpcTurnController } from "./turn.js";
14
15
  export { OMP_ADAPTER_ID } from "./thread.js";
15
16
  export const OMP_RUNTIME_PROTOCOL_NAME = "omp-rpc";
16
17
  export const OMP_RUNTIME_SUPPORTED_RANGE = "2";
18
+ const ompCompleteHistoryProofs = new WeakMap();
19
+ const ompRpcRequestImplementation = OmpRpcClient.prototype.request;
20
+ const ompBuiltInHistoryClients = new WeakMap();
21
+ const ompBuiltInAdapterHistoryClients = new WeakMap();
22
+ function bindOmpCompleteHistoryProof(page) {
23
+ const claim = page.reconciliationProof;
24
+ if (!claim?.complete)
25
+ return page;
26
+ const entries = Object.freeze([...claim.entries]);
27
+ const fullScope = Object.freeze({ entryCount: entries.length, digest: historyReconciliationDigest(entries) });
28
+ const proof = Object.freeze({
29
+ complete: true,
30
+ ...(claim.scopeId ? { scopeId: claim.scopeId } : {}),
31
+ fullScope,
32
+ entries,
33
+ });
34
+ ompCompleteHistoryProofs.set(proof, { ...(claim.scopeId ? { scopeId: claim.scopeId } : {}), fullScope });
35
+ return { ...page, reconciliationProof: proof };
36
+ }
37
+ export function isOmpBuiltInHistoryReconciliationProof(proof) {
38
+ const bound = ompCompleteHistoryProofs.get(proof);
39
+ return proof.complete
40
+ && bound !== undefined
41
+ && proof.scopeId === bound.scopeId
42
+ && proof.fullScope === bound.fullScope;
43
+ }
17
44
  function ompCapabilities() {
18
45
  return {
19
46
  lifecycle: {
@@ -179,6 +206,7 @@ export class OmpSession {
179
206
  thread;
180
207
  hostTools;
181
208
  resourceDelivery;
209
+ forkCandidatesRequest;
182
210
  constructor(runtimeInstanceId, bundle, config, emitWarning, adapter) {
183
211
  this.runtimeInstanceId = runtimeInstanceId;
184
212
  this.bundle = bundle;
@@ -197,12 +225,11 @@ export class OmpSession {
197
225
  this.controls = {
198
226
  getCurrentSession: () => this.thread.getSessionSnapshot(this.runtimeInstanceId),
199
227
  listSessions: () => this.thread.listSessions(this.runtimeInstanceId),
200
- getForkCandidates: () => this.forkCandidates(),
228
+ getForkCandidates: () => this.getForkCandidates(),
201
229
  forkSession: async (entryId) => await this.runIdleOperation(async () => {
202
- const previous = this.thread.getSessionSnapshot(this.runtimeInstanceId);
203
230
  const result = await this.thread.forkSession(this.runtimeInstanceId, entryId);
204
231
  this.updateBinding();
205
- return { previous, current: result.current, cancelled: result.cancelled };
232
+ return result;
206
233
  }),
207
234
  getReasoning: () => parseOmpReasoning(undefined),
208
235
  setReasoning: (value) => {
@@ -245,9 +272,6 @@ export class OmpSession {
245
272
  adapterId: OMP_ADAPTER_ID,
246
273
  };
247
274
  }
248
- forkCandidates() {
249
- return this.thread.cachedForkCandidates();
250
- }
251
275
  async setOmpModel(provider, modelId) {
252
276
  const info = await setOmpModel(this.client, provider, modelId);
253
277
  return info;
@@ -315,7 +339,7 @@ export class OmpSession {
315
339
  listener(event);
316
340
  }
317
341
  async prompt(input) {
318
- this.assertActive();
342
+ this.assertIdle();
319
343
  this.operationInFlight = true;
320
344
  try {
321
345
  await this.turn.prompt(input.text);
@@ -369,6 +393,19 @@ export class OmpSession {
369
393
  if (this.disposed)
370
394
  throw new Error("OMP session is disposed.");
371
395
  }
396
+ async getForkCandidates() {
397
+ if (this.forkCandidatesRequest)
398
+ return await this.forkCandidatesRequest;
399
+ const request = this.runIdleOperation(async () => await this.thread.loadForkCandidates(this.runtimeInstanceId));
400
+ this.forkCandidatesRequest = request;
401
+ try {
402
+ return await request;
403
+ }
404
+ finally {
405
+ if (this.forkCandidatesRequest === request)
406
+ this.forkCandidatesRequest = undefined;
407
+ }
408
+ }
372
409
  async runIdleOperation(operation) {
373
410
  this.assertIdle();
374
411
  this.operationInFlight = true;
@@ -380,7 +417,7 @@ export class OmpSession {
380
417
  }
381
418
  }
382
419
  }
383
- export class OmpAgentRuntimeAdapter {
420
+ class OmpAgentRuntimeAdapter {
384
421
  instanceId;
385
422
  descriptor;
386
423
  config;
@@ -473,6 +510,10 @@ export class OmpAgentRuntimeAdapter {
473
510
  const session = new OmpSession(this.instanceId, bundle, this.parsed, (m) => {
474
511
  // Warning surfaced via session events is delivered by the turn controller.
475
512
  }, this);
513
+ const historyClient = Object.freeze({
514
+ request: ((...args) => Reflect.apply(ompRpcRequestImplementation, client, args)),
515
+ });
516
+ ompBuiltInHistoryClients.set(session, { owner: this, client: historyClient });
476
517
  session.setPiboSessionId(input.piboSession.id);
477
518
  // Wire host tools after session construction so the executor is available.
478
519
  const portableTools = input.services?.portableTools;
@@ -521,8 +562,6 @@ export class OmpAgentRuntimeAdapter {
521
562
  // (bindNativeSessionId below still sets the binding.)
522
563
  }
523
564
  }
524
- // Prime fork candidates (get_branch_messages) for the sync SPI.
525
- void threads.loadForkCandidates(this.instanceId);
526
565
  }
527
566
  catch (error) {
528
567
  await client.dispose();
@@ -537,11 +576,18 @@ export class OmpAgentRuntimeAdapter {
537
576
  }
538
577
  /** Record the live session so adapter-level reads can route to it. */
539
578
  attachLiveSession(session) {
579
+ const history = ompBuiltInHistoryClients.get(session);
580
+ if (!history || history.owner !== this) {
581
+ throw new AgentRuntimeUnavailableError(this.instanceId, "OMP history requires a session created by this built-in adapter instance.");
582
+ }
540
583
  this.live = session;
584
+ ompBuiltInAdapterHistoryClients.set(this, history.client);
541
585
  }
542
586
  detachLiveSession(session) {
543
- if (this.live === session)
544
- this.live = undefined;
587
+ if (this.live !== session)
588
+ return;
589
+ this.live = undefined;
590
+ ompBuiltInAdapterHistoryClients.delete(this);
545
591
  }
546
592
  async listModels() {
547
593
  if (this.live) {
@@ -579,8 +625,9 @@ export class OmpAgentRuntimeAdapter {
579
625
  return inspectOmpHistory(input, this.instanceId);
580
626
  }
581
627
  async readHistory(input) {
582
- if (this.live) {
583
- return await readOmpHistory(this.live.getClient(), input, this.instanceId, input.binding);
628
+ const historyClient = ompBuiltInAdapterHistoryClients.get(this);
629
+ if (historyClient) {
630
+ return bindOmpCompleteHistoryProof(await readOmpHistory(historyClient, input, this.instanceId, input.binding));
584
631
  }
585
632
  return emptyOmpHistoryPage(this.instanceId);
586
633
  }
@@ -1,5 +1,15 @@
1
- import { OMP_ADAPTER_ID } from "./thread.js";
2
- import { OMP_ADAPTER_VERSION } from "./thread.js";
1
+ import { historyReconciliationDigest } from "../../agent-runtime/history.js";
2
+ import { TRACE_RECONCILIATION_ENTRY_CAP } from "../../shared/trace-limits.js";
3
+ import { OMP_ADAPTER_ID, OMP_ADAPTER_VERSION } from "./thread.js";
4
+ const OMP_HISTORY_CURSOR_PREFIX = "omp-history:";
5
+ const OMP_HISTORY_PAGE_LIMIT = 200;
6
+ const DEFAULT_HISTORY_LIMIT = 100;
7
+ class OmpHistoryResponseError extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = "OmpHistoryResponseError";
11
+ }
12
+ }
3
13
  function isRecord(value) {
4
14
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
5
15
  }
@@ -16,15 +26,26 @@ export function inspectOmpHistory(input, runtimeInstanceId) {
16
26
  };
17
27
  }
18
28
  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
- }
29
+ if (isRecord(message) && typeof message.role === "string") {
30
+ const role = message.role;
31
+ if (role === "user" || role === "assistant" || role === "tool" || role === "system")
32
+ return role;
25
33
  }
26
34
  return "system";
27
35
  }
36
+ function messageString(message, keys) {
37
+ if (!isRecord(message))
38
+ return undefined;
39
+ for (const key of keys) {
40
+ const value = message[key];
41
+ if (typeof value === "string" && value.trim())
42
+ return value;
43
+ }
44
+ return undefined;
45
+ }
46
+ function messageEntryId(message) {
47
+ return messageString(message, ["entryId", "id", "messageId"]);
48
+ }
28
49
  function messageText(message) {
29
50
  if (typeof message === "string")
30
51
  return message;
@@ -44,48 +65,187 @@ function messageText(message) {
44
65
  }
45
66
  return "";
46
67
  }
47
- function toHistoryEntry(message, sequence, binding) {
68
+ function jsonValue(value, depth = 0) {
69
+ if (depth > 32)
70
+ return "[maximum depth reached]";
71
+ if (value === null || typeof value === "boolean" || typeof value === "string")
72
+ return value;
73
+ if (typeof value === "number")
74
+ return Number.isFinite(value) ? value : String(value);
75
+ if (Array.isArray(value))
76
+ return value.map((child) => jsonValue(child, depth + 1));
77
+ if (isRecord(value)) {
78
+ const result = {};
79
+ for (const [key, child] of Object.entries(value)) {
80
+ if (child !== undefined)
81
+ result[key] = jsonValue(child, depth + 1);
82
+ }
83
+ return result;
84
+ }
85
+ return String(value);
86
+ }
87
+ function toHistoryEntry(message, sequence, binding, historyScopeId) {
48
88
  const role = messageEntryRole(message);
49
- const content = messageText(message);
89
+ const content = messageText(message) || "[empty message]";
90
+ const nativeEntryId = messageEntryId(message);
91
+ const nativeTurnId = messageString(message, ["turnId", "nativeTurnId"]);
92
+ const nativeSessionId = binding.nativeSessionId ?? "omp";
93
+ const historyPosition = `omp:${nativeSessionId}:message:${sequence}`;
94
+ const toolCallId = role === "tool"
95
+ ? messageString(message, ["toolCallId", "callId"]) ?? `omp-tool:${nativeSessionId}:${sequence}`
96
+ : undefined;
97
+ const toolName = role === "tool" ? messageString(message, ["toolName", "name"]) ?? "OMP Tool" : undefined;
50
98
  return {
51
- id: `${binding.nativeSessionId ?? "omp"}-${sequence}`,
99
+ id: `omp:${nativeSessionId}:message:${sequence}`,
52
100
  type: "message",
53
101
  source: "native",
54
102
  createdAt: isRecord(message) && typeof message.timestamp === "string"
55
103
  ? message.timestamp
56
- : new Date().toISOString(),
104
+ : new Date(0).toISOString(),
57
105
  sequence,
106
+ historyPosition,
107
+ historyScopeId,
108
+ ...(nativeTurnId ? { nativeTurnId } : {}),
109
+ ...(nativeEntryId ? { nativeEntryId } : {}),
58
110
  role,
59
- content: content || "[empty message]",
111
+ content,
112
+ ...(toolCallId ? {
113
+ toolCallId,
114
+ toolName,
115
+ result: isRecord(message) && message.result !== undefined ? jsonValue(message.result) : content,
116
+ isError: isRecord(message) && message.isError === true,
117
+ status: isRecord(message) && message.isError === true ? "error" : "complete",
118
+ } : {}),
60
119
  };
61
120
  }
62
- export async function readOmpHistory(client, input, runtimeInstanceId, binding) {
121
+ function selectedLimit(value) {
122
+ if (value === undefined)
123
+ return DEFAULT_HISTORY_LIMIT;
124
+ if (!Number.isSafeInteger(value) || value <= 0)
125
+ throw new OmpHistoryResponseError("OMP native history limit must be a positive integer.");
126
+ return Math.min(value, OMP_HISTORY_PAGE_LIMIT);
127
+ }
128
+ function encodeCursor(cursor) {
129
+ return `${OMP_HISTORY_CURSOR_PREFIX}${Buffer.from(JSON.stringify(cursor), "utf8").toString("base64url")}`;
130
+ }
131
+ function decodeCursor(value, nativeSessionId) {
132
+ if (!value)
133
+ return undefined;
134
+ if (!value.startsWith(OMP_HISTORY_CURSOR_PREFIX))
135
+ throw new OmpHistoryResponseError("Invalid OMP native history cursor.");
63
136
  try {
137
+ const parsed = JSON.parse(Buffer.from(value.slice(OMP_HISTORY_CURSOR_PREFIX.length), "base64url").toString("utf8"));
138
+ if (parsed.v !== 1
139
+ || parsed.nativeSessionId !== nativeSessionId
140
+ || !Number.isSafeInteger(parsed.beforeIndex)
141
+ || Number(parsed.beforeIndex) < 0)
142
+ throw new Error("invalid cursor");
143
+ return parsed;
144
+ }
145
+ catch {
146
+ throw new OmpHistoryResponseError("Invalid OMP native history cursor.");
147
+ }
148
+ }
149
+ function messagesPageData(result) {
150
+ const data = isRecord(result) ? result.data : undefined;
151
+ if (!isRecord(data)
152
+ || !Array.isArray(data.messages)
153
+ || !Number.isSafeInteger(data.totalMessages)
154
+ || Number(data.totalMessages) < 0)
155
+ throw new OmpHistoryResponseError("OMP returned an invalid native history page.");
156
+ return {
157
+ messages: data.messages,
158
+ ...(typeof data.nextCursor === "string" && data.nextCursor.length > 0 ? { nextCursor: data.nextCursor } : {}),
159
+ totalMessages: Number(data.totalMessages),
160
+ };
161
+ }
162
+ async function readCompleteOmpMessages(client) {
163
+ const pages = [];
164
+ const seenCursors = new Set();
165
+ let cursor;
166
+ let expectedTotal;
167
+ let collected = 0;
168
+ let pageRequests = 0;
169
+ do {
170
+ pageRequests += 1;
171
+ if (pageRequests > TRACE_RECONCILIATION_ENTRY_CAP + 1) {
172
+ throw new OmpHistoryResponseError("OMP native history exceeded the bounded provider page sequence.");
173
+ }
64
174
  const result = await client.request({
65
175
  type: "get_messages_page",
66
- ...(input.cursor ? { cursor: input.cursor } : {}),
67
- ...(input.limit ? { limit: input.limit } : {}),
176
+ ...(cursor ? { cursor } : {}),
177
+ limit: OMP_HISTORY_PAGE_LIMIT,
68
178
  }, "get_messages_page");
69
- const data = result["data"];
70
- if (!isRecord(data) || !Array.isArray(data.messages)) {
71
- return emptyPage(runtimeInstanceId);
179
+ const page = messagesPageData(result);
180
+ expectedTotal ??= page.totalMessages;
181
+ if (page.totalMessages !== expectedTotal) {
182
+ throw new OmpHistoryResponseError("OMP native history changed while its proof was collected.");
183
+ }
184
+ if (page.messages.length > OMP_HISTORY_PAGE_LIMIT) {
185
+ throw new OmpHistoryResponseError("OMP native history exceeded the requested provider page bound.");
186
+ }
187
+ if (expectedTotal > TRACE_RECONCILIATION_ENTRY_CAP) {
188
+ throw new OmpHistoryResponseError(`OMP native history exceeds the ${TRACE_RECONCILIATION_ENTRY_CAP}-entry proof bound.`);
189
+ }
190
+ if (collected + page.messages.length > expectedTotal) {
191
+ throw new OmpHistoryResponseError("OMP native history pages exceed their declared total.");
72
192
  }
73
- const entries = [];
74
- let sequence = 0;
75
- for (const message of data.messages) {
76
- entries.push(toHistoryEntry(message, sequence++, binding));
193
+ pages.push(page.messages);
194
+ collected += page.messages.length;
195
+ if (collected === expectedTotal) {
196
+ if (page.nextCursor) {
197
+ throw new OmpHistoryResponseError("OMP native history cursor extends beyond its declared total.");
198
+ }
199
+ break;
200
+ }
201
+ if (!page.nextCursor || seenCursors.has(page.nextCursor)) {
202
+ throw new OmpHistoryResponseError("OMP native history cannot prove a complete bounded page sequence.");
203
+ }
204
+ seenCursors.add(page.nextCursor);
205
+ cursor = page.nextCursor;
206
+ } while (true);
207
+ if (collected !== expectedTotal)
208
+ throw new OmpHistoryResponseError("OMP native history proof is incomplete.");
209
+ return pages.reverse().flat();
210
+ }
211
+ export async function readOmpHistory(client, input, runtimeInstanceId, binding) {
212
+ const nativeSessionId = binding.nativeSessionId ?? "omp";
213
+ const historyScopeId = `omp:${runtimeInstanceId}:${nativeSessionId}`;
214
+ try {
215
+ const messages = await readCompleteOmpMessages(client);
216
+ const allEntries = messages.map((message, sequence) => toHistoryEntry(message, sequence, binding, historyScopeId));
217
+ const beforeTimestampMs = input.beforeTimestamp === undefined ? undefined : Date.parse(input.beforeTimestamp);
218
+ if (beforeTimestampMs !== undefined && Number.isNaN(beforeTimestampMs)) {
219
+ throw new OmpHistoryResponseError("OMP native history beforeTimestamp is invalid.");
77
220
  }
221
+ const filtered = beforeTimestampMs === undefined
222
+ ? allEntries
223
+ : allEntries.filter((entry) => Date.parse(entry.createdAt) < beforeTimestampMs);
224
+ const cursor = decodeCursor(input.cursor, nativeSessionId);
225
+ const end = Math.min(cursor?.beforeIndex ?? filtered.length, filtered.length);
226
+ const limit = selectedLimit(input.limit);
227
+ const start = Math.max(0, end - limit);
228
+ const entries = filtered.slice(start, end);
78
229
  return {
79
230
  runtimeInstanceId,
80
231
  adapterId: OMP_ADAPTER_ID,
81
232
  source: "native",
82
233
  entries,
83
- ...(typeof data.nextCursor === "string" ? { nextCursor: data.nextCursor } : {}),
84
- hasMore: typeof data.nextCursor === "string" && data.nextCursor.length > 0,
234
+ reconciliationProof: {
235
+ complete: true,
236
+ scopeId: historyScopeId,
237
+ fullScope: { entryCount: allEntries.length, digest: historyReconciliationDigest(allEntries) },
238
+ entries: allEntries,
239
+ },
240
+ orderOffset: start,
241
+ ...(start > 0 ? { nextCursor: encodeCursor({ v: 1, nativeSessionId, beforeIndex: start }) } : {}),
242
+ hasMore: start > 0,
85
243
  };
86
244
  }
87
- catch {
88
- return emptyPage(runtimeInstanceId);
245
+ catch (error) {
246
+ if (error instanceof OmpHistoryResponseError)
247
+ throw error;
248
+ throw new OmpHistoryResponseError("OMP native history could not establish a complete bounded proof.");
89
249
  }
90
250
  }
91
251
  function emptyPage(runtimeInstanceId) {
@@ -94,15 +254,10 @@ function emptyPage(runtimeInstanceId) {
94
254
  adapterId: OMP_ADAPTER_ID,
95
255
  source: "native",
96
256
  entries: [],
257
+ reconciliationProof: { complete: false, entries: [] },
97
258
  hasMore: false,
98
259
  };
99
260
  }
100
261
  export function emptyOmpHistoryPage(runtimeInstanceId) {
101
- return {
102
- runtimeInstanceId,
103
- adapterId: OMP_ADAPTER_ID,
104
- source: "native",
105
- entries: [],
106
- hasMore: false,
107
- };
262
+ return emptyPage(runtimeInstanceId);
108
263
  }
@@ -1,6 +1,13 @@
1
+ import { OmpRpcResponseError } from "./client.js";
1
2
  import { OMP_RPC_PROTOCOL_NAME, OMP_RPC_PROTOCOL_VERSION } from "./protocol-types.js";
2
3
  export const OMP_ADAPTER_ID = "orp";
3
4
  export const OMP_ADAPTER_VERSION = "1.0.0";
5
+ function isUnsupportedForkCommand(error) {
6
+ if (!(error instanceof OmpRpcResponseError))
7
+ return false;
8
+ return /unknown_command|unsupported_command|method_not_found|(?:unknown|unsupported|unrecognized|invalid)\s+(?:rpc\s+)?(?:command|method)|(?:command|method)\s+(?:is\s+)?(?:unknown|unsupported|unrecognized|not implemented|not found)/i
9
+ .test(`${error.errorCode ?? ""} ${error.error}`);
10
+ }
4
11
  /**
5
12
  * OMP session-file lifecycle controller. OMP owns its session files (JSONL
6
13
  * session transcripts) under the isolated `PI_CODING_AGENT_DIR` sessions dir.
@@ -11,7 +18,6 @@ export class OmpThreadController {
11
18
  client;
12
19
  cwd;
13
20
  snapshot;
14
- forkCandidatesCache = [];
15
21
  constructor(client, cwd, initial) {
16
22
  this.client = client;
17
23
  this.cwd = cwd;
@@ -56,49 +62,89 @@ export class OmpThreadController {
56
62
  const snapshot = this.getSessionSnapshot(runtimeInstanceId);
57
63
  return [{ ...snapshot, messageCount: this.snapshot.messageCount, name: this.snapshot.sessionName }];
58
64
  }
59
- /** Fetch branch candidates from OMP and cache them for the sync SPI. */
60
- async loadForkCandidates(runtimeInstanceId) {
65
+ /** Fetch fork candidates from current OMP RPC names with legacy branch compatibility. */
66
+ async loadForkCandidates(_runtimeInstanceId) {
67
+ let result;
61
68
  try {
62
- const result = await this.client.request({ type: "get_branch_messages" }, "get_branch_messages");
63
- const data = result["data"];
64
- if (data && typeof data === "object" && !Array.isArray(data) && "messages" in data) {
65
- const messages = data.messages;
66
- if (Array.isArray(messages)) {
67
- const candidates = [];
68
- for (const entry of messages) {
69
- if (entry && typeof entry === "object" && !Array.isArray(entry)) {
70
- const rec = entry;
71
- if (typeof rec.entryId === "string") {
72
- candidates.push({
73
- entryId: rec.entryId,
74
- text: typeof rec.text === "string" ? rec.text : "",
75
- });
76
- }
77
- }
78
- }
79
- this.forkCandidatesCache = candidates;
80
- return candidates;
81
- }
69
+ result = await this.client.request({ type: "get_fork_messages" }, "get_fork_messages");
70
+ }
71
+ catch (error) {
72
+ if (!isUnsupportedForkCommand(error))
73
+ throw error;
74
+ try {
75
+ result = await this.client.request({ type: "get_branch_messages" }, "get_branch_messages");
76
+ }
77
+ catch (legacyError) {
78
+ if (isUnsupportedForkCommand(legacyError))
79
+ return [];
80
+ throw legacyError;
82
81
  }
83
82
  }
84
- catch {
85
- // Branch candidates are optional; fall through to empty.
83
+ const data = result["data"];
84
+ if (!data || typeof data !== "object" || Array.isArray(data) || !("messages" in data))
85
+ return [];
86
+ const messages = data.messages;
87
+ if (!Array.isArray(messages))
88
+ return [];
89
+ const candidates = [];
90
+ const seenEntryIds = new Set();
91
+ for (const entry of messages) {
92
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
93
+ continue;
94
+ const record = entry;
95
+ if (typeof record.entryId !== "string" || !record.entryId.trim() || seenEntryIds.has(record.entryId))
96
+ continue;
97
+ seenEntryIds.add(record.entryId);
98
+ candidates.push({
99
+ entryId: record.entryId,
100
+ text: typeof record.text === "string" ? record.text : "",
101
+ });
86
102
  }
87
- this.forkCandidatesCache = [];
88
- return [];
89
- }
90
- /** Sync accessor for the SPI (get_available_commands via a warm-up load). */
91
- cachedForkCandidates() {
92
- return this.forkCandidatesCache;
103
+ return candidates;
93
104
  }
94
105
  async forkSession(runtimeInstanceId, entryId) {
106
+ if (!entryId.trim())
107
+ throw new Error("OMP session fork requires a native user-message id.");
108
+ const previousState = structuredClone(this.snapshot);
95
109
  const previous = structuredClone(this.getSessionSnapshot(runtimeInstanceId));
96
- const result = await this.client.request({ type: "branch", entryId }, "branch");
110
+ let result;
111
+ try {
112
+ result = await this.client.request({ type: "fork", entryId }, "fork");
113
+ }
114
+ catch (error) {
115
+ if (!isUnsupportedForkCommand(error))
116
+ throw error;
117
+ result = await this.client.request({ type: "branch", entryId }, "branch");
118
+ }
97
119
  const data = result["data"];
98
- const cancelled = Boolean(data && typeof data === "object" && !Array.isArray(data) && data.cancelled === true);
99
- await this.refresh();
100
- const current = this.getSessionSnapshot(runtimeInstanceId);
101
- return { previous, current, cancelled };
120
+ const record = data && typeof data === "object" && !Array.isArray(data) ? data : undefined;
121
+ const cancelled = record?.cancelled === true;
122
+ try {
123
+ await this.refresh();
124
+ const current = this.getSessionSnapshot(runtimeInstanceId);
125
+ if (!cancelled && current.nativeSessionId === previous.nativeSessionId) {
126
+ throw new Error("OMP session fork returned the source native session id.");
127
+ }
128
+ return {
129
+ previous,
130
+ current,
131
+ cancelled,
132
+ summaryEntryId: entryId,
133
+ ...(typeof record?.text === "string" ? { selectedText: record.text } : {}),
134
+ };
135
+ }
136
+ catch (error) {
137
+ if (!cancelled && previousState.sessionFile) {
138
+ try {
139
+ await this.client.request({ type: "switch_session", sessionPath: previousState.sessionFile }, "switch_session");
140
+ await this.refresh();
141
+ }
142
+ catch (rollbackError) {
143
+ throw new AggregateError([error, rollbackError], "OMP session fork failed and the source session could not be restored.");
144
+ }
145
+ }
146
+ throw error;
147
+ }
102
148
  }
103
149
  async switchSession(runtimeInstanceId, sessionPath) {
104
150
  const previous = structuredClone(this.getSessionSnapshot(runtimeInstanceId));
@@ -8,11 +8,36 @@ import { unsupportedAgentRuntimeCapability, } from "../../agent-runtime/capabili
8
8
  import { RoutedSession as PiRoutedSession } from "./routed-session.js";
9
9
  import { loadModelCatalog as loadPiModelCatalog, piAgentRuntimeModelCatalog, } from "./model-catalog.js";
10
10
  import { inspectPiAgentRuntimeHistory, readPiAgentRuntimeHistory, } from "./history.js";
11
+ import { historyReconciliationDigest, } from "../../agent-runtime/history.js";
11
12
  import { PiAgentRuntimeAuthController } from "./auth.js";
12
13
  import { importPortableHistoryIntoPi } from "./portable-history.js";
13
14
  import { piIntentTracingEnabled } from "./intent-tracing.js";
15
+ import { isWebSearchProviderTool } from "../../tools/web-search.js";
14
16
  const PI_ADAPTER_ID = "pi";
15
17
  export const PI_PROTOCOL_VERSION = "0.84.2";
18
+ const piCompleteHistoryProofs = new WeakMap();
19
+ function bindPiCompleteHistoryProof(page) {
20
+ const claim = page.reconciliationProof;
21
+ if (!claim?.complete)
22
+ return page;
23
+ const entries = Object.freeze([...claim.entries]);
24
+ const fullScope = Object.freeze({ entryCount: entries.length, digest: historyReconciliationDigest(entries) });
25
+ const proof = Object.freeze({
26
+ complete: true,
27
+ ...(claim.scopeId ? { scopeId: claim.scopeId } : {}),
28
+ fullScope,
29
+ entries,
30
+ });
31
+ piCompleteHistoryProofs.set(proof, { ...(claim.scopeId ? { scopeId: claim.scopeId } : {}), fullScope });
32
+ return { ...page, reconciliationProof: proof };
33
+ }
34
+ export function isPiBuiltInHistoryReconciliationProof(proof) {
35
+ const bound = piCompleteHistoryProofs.get(proof);
36
+ return proof.complete
37
+ && bound !== undefined
38
+ && proof.scopeId === bound.scopeId
39
+ && proof.fullScope === bound.fullScope;
40
+ }
16
41
  export const PI_AGENT_RUNTIME_CAPABILITIES = {
17
42
  lifecycle: {
18
43
  persistent: true,
@@ -251,7 +276,7 @@ class PiAgentRuntimeSession {
251
276
  bindingNativeSessionId;
252
277
  nativePresenceExpected;
253
278
  disposed = false;
254
- constructor(runtimeInstanceId, piboSessionId, runtime, binding, initialFastMode) {
279
+ constructor(runtimeInstanceId, piboSessionId, runtime, binding, initialFastMode, providerWebSearchEnabled) {
255
280
  this.runtimeInstanceId = runtimeInstanceId;
256
281
  this.piboSessionId = piboSessionId;
257
282
  this.runtime = runtime;
@@ -260,6 +285,8 @@ class PiAgentRuntimeSession {
260
285
  this.bindingNativeSessionId = runtime.session.sessionId;
261
286
  this.nativePresenceExpected = runtime.session.sessionManager.buildSessionContext().messages.length > 0;
262
287
  this.routed = new PiRoutedSession(piboSessionId, runtime, (event) => this.handlePiboEvent(event), PiboPluginRegistry.create(), true, undefined, initialFastMode, undefined, undefined, (state) => this.handleEngineState(state));
288
+ if (providerWebSearchEnabled)
289
+ this.routed.enableProviderWebSearchObservation();
263
290
  this.controls = this.createControls();
264
291
  this.compatibilityHandle = this.createCompatibilityHandle();
265
292
  }
@@ -517,7 +544,7 @@ class PiAgentRuntimeAdapter {
517
544
  return await inspectPiAgentRuntimeHistory(this.instanceId, input);
518
545
  }
519
546
  async readHistory(input) {
520
- return await readPiAgentRuntimeHistory(this.instanceId, input);
547
+ return bindPiCompleteHistoryProof(await readPiAgentRuntimeHistory(this.instanceId, input));
521
548
  }
522
549
  validateProfile(input) {
523
550
  const diagnostics = [];
@@ -658,7 +685,7 @@ class PiAgentRuntimeAdapter {
658
685
  && runtime.session.sessionManager.buildSessionContext().messages.length > 0,
659
686
  },
660
687
  };
661
- return new PiAgentRuntimeSession(this.instanceId, input.piboSession.id, runtime, binding, compatibility?.initialFastMode ?? false);
688
+ return new PiAgentRuntimeSession(this.instanceId, input.piboSession.id, runtime, binding, compatibility?.initialFastMode ?? false, profile.tools.some((tool) => tool.enabled !== false && isWebSearchProviderTool(tool)));
662
689
  }
663
690
  loadModelCatalog() {
664
691
  const now = Date.now();