@pasko70/pibo 2.5.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (104) 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 +307 -31
  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/models.js +48 -1
  10. package/dist/agent-runtimes/codex-native/process.js +15 -1
  11. package/dist/agent-runtimes/codex-native/thread.js +75 -31
  12. package/dist/agent-runtimes/codex-native/turn.js +30 -0
  13. package/dist/agent-runtimes/history-proof.js +8 -0
  14. package/dist/agent-runtimes/omp/adapter.js +61 -14
  15. package/dist/agent-runtimes/omp/history.js +189 -34
  16. package/dist/agent-runtimes/omp/thread.js +82 -36
  17. package/dist/agent-runtimes/pi/adapter.js +30 -3
  18. package/dist/agent-runtimes/pi/history.js +66 -5
  19. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  20. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  21. package/dist/apps/chat/agent-store.js +3 -0
  22. package/dist/apps/chat/chat-api-routes.js +1 -1
  23. package/dist/apps/chat/chat-files.js +130 -9
  24. package/dist/apps/chat/chat-request-normalizers.js +6 -0
  25. package/dist/apps/chat/chat-settings-routes.js +27 -2
  26. package/dist/apps/chat/chat-speech.js +100 -0
  27. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  28. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  29. package/dist/apps/chat/trace-v2.js +210 -8
  30. package/dist/apps/chat/trace.js +3 -0
  31. package/dist/apps/chat/web-app.js +108 -3
  32. package/dist/apps/chat-ui/assets/{dist-GS8xUdfg.js → dist-BBDMeU5-.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-BpVSf6ZV.js → dist-BDIATCQt.js} +1 -1
  34. package/dist/apps/chat-ui/assets/{dist-CRv7fzi3.js → dist-JnW4v8UE.js} +1 -1
  35. package/dist/apps/chat-ui/assets/{dist-CbBXbGVi.js → dist-lRiLP9rr.js} +1 -1
  36. package/dist/apps/chat-ui/assets/{dist-HC0xQ92C.js → dist-mqCviI-c.js} +1 -1
  37. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  38. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  39. package/dist/apps/chat-ui/index.html +2 -2
  40. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  41. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  42. package/dist/apps/chat-vscode-web/index.html +2 -2
  43. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  44. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
  45. package/dist/cli.js +49 -5
  46. package/dist/config/config.js +59 -8
  47. package/dist/core/preview-server-settings.js +20 -0
  48. package/dist/core/profiles.js +1 -0
  49. package/dist/core/session-router.js +476 -44
  50. package/dist/core/user-settings.js +10 -0
  51. package/dist/data/payload-store.js +57 -1
  52. package/dist/data/schema.js +18 -1
  53. package/dist/debug/trace.js +5 -0
  54. package/dist/gateway/client.js +368 -65
  55. package/dist/gateway/protocol.js +2 -0
  56. package/dist/gateway/server.js +8 -0
  57. package/dist/gateway/web.js +2 -0
  58. package/dist/index.js +1 -1
  59. package/dist/loops/store.js +6 -4
  60. package/dist/mcp/config.js +89 -17
  61. package/dist/mcp/daemon-client.js +473 -153
  62. package/dist/mcp/daemon.js +401 -37
  63. package/dist/plugins/builtin.js +2 -2
  64. package/dist/plugins/codex-native.js +2 -0
  65. package/dist/plugins/registry.js +260 -1
  66. package/dist/previews/cli.js +345 -0
  67. package/dist/previews/config.js +61 -0
  68. package/dist/previews/manager.js +644 -0
  69. package/dist/previews/network.js +198 -0
  70. package/dist/previews/plugin.js +11 -0
  71. package/dist/previews/process-supervisor.js +38 -0
  72. package/dist/previews/proxy.js +387 -0
  73. package/dist/previews/store.js +750 -0
  74. package/dist/previews/types.js +1 -0
  75. package/dist/previews/web-app.js +446 -0
  76. package/dist/runs/registry.js +22 -0
  77. package/dist/runs/tools.js +46 -7
  78. package/dist/session-ui/terminalRows.js +187 -13
  79. package/dist/sessions/pibo-data-store.js +37 -0
  80. package/dist/sessions/runtime-binding-persistence.js +34 -0
  81. package/dist/sessions/runtime-binding.js +6 -0
  82. package/dist/sessions/sqlite-store.js +37 -0
  83. package/dist/shared/trace-engine.js +12 -64
  84. package/dist/shared/trace-event-projection.js +16 -0
  85. package/dist/shared/trace-history.js +684 -184
  86. package/dist/shared/trace-limits.js +7 -0
  87. package/dist/shared/trace-live-patch.js +61 -0
  88. package/dist/shared/trace-tool-identity.js +43 -0
  89. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  90. package/dist/speech/openai-codex.js +407 -0
  91. package/dist/speech/types.js +8 -0
  92. package/dist/subagents/context.js +5 -3
  93. package/dist/subagents/observations.js +94 -6
  94. package/dist/subagents/tool.js +103 -20
  95. package/dist/tools/codex-image-artifacts.js +24 -0
  96. package/dist/tools/codex-image-generation.js +4 -22
  97. package/dist/web/channel.js +88 -4
  98. package/dist/web/http.js +2 -1
  99. package/npm-shrinkwrap.json +2 -2
  100. package/package.json +1 -1
  101. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  102. package/dist/apps/chat-ui/assets/index-DT5jCQBP.js +0 -228
  103. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  104. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.1.vsix +0 -0
@@ -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();
@@ -3,6 +3,8 @@ import { closeSync, existsSync, openSync, readFileSync, readSync, readdirSync, s
3
3
  import { homedir } from "node:os";
4
4
  import { join } from "node:path";
5
5
  import { parseSessionEntries, SessionManager } from "@earendil-works/pi-coding-agent";
6
+ import { historyReconciliationDigest } from "../../agent-runtime/history.js";
7
+ import { TRACE_RECONCILIATION_ENTRY_CAP } from "../../shared/trace-limits.js";
6
8
  export const PI_HISTORY_TAIL_MAX_BYTES = 2 * 1024 * 1024;
7
9
  export const PI_HISTORY_PAGE_MAX_BYTES = 512 * 1024;
8
10
  export const PI_HISTORY_SCAN_MAX_BYTES = 16 * 1024 * 1024;
@@ -76,11 +78,35 @@ export async function readPiAgentRuntimeHistory(runtimeInstanceId, input) {
76
78
  beforeTimestamp: input.beforeTimestamp ?? decoded.beforeTimestamp,
77
79
  limit: input.limit,
78
80
  });
81
+ const historyScopeId = `pi:${runtimeInstanceId}:${input.binding.nativeSessionId ?? "unbound"}`;
82
+ const completeProof = inspection.sizeBytes !== undefined && inspection.sizeBytes <= PI_HISTORY_SCAN_MAX_BYTES
83
+ ? readPiTranscriptEntriesWithPositions(inspection.locator.value, PI_HISTORY_SCAN_MAX_BYTES, TRACE_RECONCILIATION_ENTRY_CAP)
84
+ : undefined;
85
+ const proofEntries = completeProof
86
+ ? piSessionEntriesToAgentRuntimeHistoryEntries(completeProof.entries, completeProof.entryPositions, historyScopeId)
87
+ : undefined;
88
+ const proofEntriesByPosition = proofEntries
89
+ ? new Map(proofEntries.map((entry) => [entry.historyPosition, entry]))
90
+ : undefined;
91
+ const entries = proofEntriesByPosition
92
+ ? page.entryPositions.flatMap((position) => {
93
+ const entry = proofEntriesByPosition.get(position);
94
+ return entry ? [entry] : [];
95
+ })
96
+ : piSessionEntriesToAgentRuntimeHistoryEntries(page.entries, page.entryPositions, historyScopeId);
79
97
  return {
80
98
  runtimeInstanceId,
81
99
  adapterId: "pi",
82
100
  source: "native",
83
- entries: piSessionEntriesToAgentRuntimeHistoryEntries(page.entries),
101
+ entries,
102
+ reconciliationProof: completeProof && proofEntries
103
+ ? {
104
+ complete: true,
105
+ scopeId: historyScopeId,
106
+ fullScope: { entryCount: proofEntries.length, digest: historyReconciliationDigest(proofEntries) },
107
+ entries: proofEntries,
108
+ }
109
+ : { complete: false, scopeId: historyScopeId, entries },
84
110
  orderOffset: page.startByte,
85
111
  nextCursor: page.hasOlder && page.nextBeforeByte !== undefined
86
112
  ? encodePiHistoryCursor({ beforeByte: page.nextBeforeByte, beforeTimestamp: input.beforeTimestamp ?? decoded.beforeTimestamp })
@@ -89,11 +115,12 @@ export async function readPiAgentRuntimeHistory(runtimeInstanceId, input) {
89
115
  inspection,
90
116
  };
91
117
  }
92
- export function piSessionEntriesToAgentRuntimeHistoryEntries(entries) {
118
+ export function piSessionEntriesToAgentRuntimeHistoryEntries(entries, historyPositions = [], historyScopeId) {
93
119
  const normalized = [];
94
120
  let nativeTurnId;
95
121
  for (let sequence = 0; sequence < entries.length; sequence += 1) {
96
122
  const entry = entries[sequence];
123
+ const historyPosition = historyPositions[sequence];
97
124
  if (entry.type === "session_info" && entry.name) {
98
125
  normalized.push({
99
126
  id: `pi:${entry.id}`,
@@ -101,6 +128,8 @@ export function piSessionEntriesToAgentRuntimeHistoryEntries(entries) {
101
128
  source: "native",
102
129
  createdAt: entry.timestamp,
103
130
  sequence,
131
+ ...(historyPosition ? { historyPosition } : {}),
132
+ ...(historyScopeId ? { historyScopeId } : {}),
104
133
  nativeEntryId: entry.id,
105
134
  name: entry.name,
106
135
  });
@@ -117,6 +146,8 @@ export function piSessionEntriesToAgentRuntimeHistoryEntries(entries) {
117
146
  source: "native",
118
147
  createdAt: entry.timestamp,
119
148
  sequence,
149
+ ...(historyPosition ? { historyPosition } : {}),
150
+ ...(historyScopeId ? { historyScopeId } : {}),
120
151
  nativeTurnId,
121
152
  nativeEntryId: entry.id,
122
153
  role: "user",
@@ -132,6 +163,8 @@ export function piSessionEntriesToAgentRuntimeHistoryEntries(entries) {
132
163
  source: "native",
133
164
  createdAt: entry.timestamp,
134
165
  sequence,
166
+ ...(historyPosition ? { historyPosition } : {}),
167
+ ...(historyScopeId ? { historyScopeId } : {}),
135
168
  nativeTurnId,
136
169
  nativeEntryId: entry.id,
137
170
  role: "assistant",
@@ -153,6 +186,8 @@ export function piSessionEntriesToAgentRuntimeHistoryEntries(entries) {
153
186
  source: "native",
154
187
  createdAt: entry.timestamp,
155
188
  sequence,
189
+ ...(historyPosition ? { historyPosition } : {}),
190
+ ...(historyScopeId ? { historyScopeId } : {}),
156
191
  nativeTurnId,
157
192
  nativeEntryId: entry.id,
158
193
  role: "tool",
@@ -228,7 +263,7 @@ export function readPiTranscriptTailEntries(path, maxBytes = PI_HISTORY_TAIL_MAX
228
263
  }
229
264
  export function readPiTranscriptHistoryPage(path, input = {}) {
230
265
  if (!existsSync(path))
231
- return { entries: [], hasOlder: false, scannedBytes: 0, startByte: 0, endByte: 0 };
266
+ return { entries: [], entryPositions: [], hasOlder: false, scannedBytes: 0, startByte: 0, endByte: 0 };
232
267
  const stats = statSync(path);
233
268
  const fileSize = Math.max(0, stats.size);
234
269
  const initialEnd = input.beforeByte === undefined
@@ -261,7 +296,7 @@ export function readPiTranscriptHistoryPage(path, input = {}) {
261
296
  const entry = parsed[entryIndex];
262
297
  if (beforeTime !== undefined && entryTimestampMs(entry) >= beforeTime)
263
298
  continue;
264
- entries.push({ entry, startByte: record.startByte });
299
+ entries.push({ entry, startByte: record.startByte, parsedIndex: entryIndex });
265
300
  if (entries.length >= limit)
266
301
  break;
267
302
  }
@@ -271,8 +306,10 @@ export function readPiTranscriptHistoryPage(path, input = {}) {
271
306
  closeSync(fd);
272
307
  }
273
308
  const nextBeforeByte = cursorEnd > 0 ? cursorEnd : undefined;
309
+ const orderedEntries = entries.reverse();
274
310
  return {
275
- entries: entries.reverse().map((item) => item.entry),
311
+ entries: orderedEntries.map((item) => item.entry),
312
+ entryPositions: orderedEntries.map((item) => `pi-byte:${item.startByte}:${item.parsedIndex}`),
276
313
  nextBeforeByte,
277
314
  hasOlder: nextBeforeByte !== undefined,
278
315
  scannedBytes,
@@ -280,6 +317,30 @@ export function readPiTranscriptHistoryPage(path, input = {}) {
280
317
  endByte: initialEnd,
281
318
  };
282
319
  }
320
+ function readPiTranscriptEntriesWithPositions(path, maxBytes, maxEntries) {
321
+ if (statSync(path).size > maxBytes)
322
+ return undefined;
323
+ const content = readFileSync(path);
324
+ if (content.length > maxBytes)
325
+ return undefined;
326
+ const entries = [];
327
+ const entryPositions = [];
328
+ let lineStart = 0;
329
+ for (let cursor = 0; cursor <= content.length; cursor += 1) {
330
+ if (cursor < content.length && content[cursor] !== 0x0a)
331
+ continue;
332
+ const line = content.subarray(lineStart, cursor).toString("utf8");
333
+ const parsed = parseTranscriptLine(line);
334
+ for (let parsedIndex = 0; parsedIndex < parsed.length; parsedIndex += 1) {
335
+ if (entries.length >= maxEntries)
336
+ return undefined;
337
+ entries.push(parsed[parsedIndex]);
338
+ entryPositions.push(`pi-byte:${lineStart}:${parsedIndex}`);
339
+ }
340
+ lineStart = cursor + 1;
341
+ }
342
+ return { entries, entryPositions };
343
+ }
283
344
  function normalizePiAssistantContent(content) {
284
345
  if (typeof content === "string")
285
346
  return [{ type: "text", text: content }];