@hachej/boring-agent 0.1.110 → 0.2.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 (156) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +262 -51
  3. package/dist/agents.d.ts +75 -0
  4. package/dist/agents.js +91 -0
  5. package/dist/approval.d.ts +71 -0
  6. package/dist/approval.js +126 -0
  7. package/dist/artifacts.d.ts +53 -0
  8. package/dist/artifacts.js +129 -0
  9. package/dist/ask-user.d.ts +40 -0
  10. package/dist/ask-user.js +134 -0
  11. package/dist/attachment.d.ts +5 -0
  12. package/dist/attachment.js +4 -0
  13. package/dist/browser-task.d.ts +32 -0
  14. package/dist/browser-task.js +119 -0
  15. package/dist/chat-transport.d.ts +80 -0
  16. package/dist/chat-transport.js +306 -0
  17. package/dist/contracts.d.ts +40 -0
  18. package/dist/contracts.js +1 -0
  19. package/dist/conversations.d.ts +136 -0
  20. package/dist/conversations.js +366 -0
  21. package/dist/definitions.d.ts +37 -0
  22. package/dist/definitions.js +99 -0
  23. package/dist/delivery-input.d.ts +35 -0
  24. package/dist/delivery-input.js +56 -0
  25. package/dist/delivery.d.ts +33 -0
  26. package/dist/delivery.js +154 -0
  27. package/dist/durable-wait.d.ts +23 -0
  28. package/dist/durable-wait.js +79 -0
  29. package/dist/file-convert.d.ts +46 -0
  30. package/dist/file-convert.js +159 -0
  31. package/dist/file-guard.d.ts +50 -0
  32. package/dist/file-guard.js +171 -0
  33. package/dist/file-types.d.ts +17 -0
  34. package/dist/file-types.js +95 -0
  35. package/dist/gateway-provider.d.ts +24 -0
  36. package/dist/gateway-provider.js +42 -0
  37. package/dist/git.d.ts +34 -0
  38. package/dist/git.js +79 -0
  39. package/dist/harness-pool.d.ts +38 -0
  40. package/dist/harness-pool.js +134 -0
  41. package/dist/index.d.ts +4 -0
  42. package/dist/index.js +1 -0
  43. package/dist/json-schema-tool.d.ts +25 -0
  44. package/dist/json-schema-tool.js +19 -0
  45. package/dist/memory/optchat-core.d.ts +118 -0
  46. package/dist/memory/optchat-core.js +317 -0
  47. package/dist/memory/optchat.d.ts +156 -0
  48. package/dist/memory/optchat.js +432 -0
  49. package/dist/mentions.d.ts +56 -0
  50. package/dist/mentions.js +140 -0
  51. package/dist/metering.d.ts +183 -0
  52. package/dist/metering.js +443 -0
  53. package/dist/model-gateway.d.ts +86 -0
  54. package/dist/model-gateway.js +241 -0
  55. package/dist/native-version.d.ts +9 -0
  56. package/dist/native-version.js +7 -0
  57. package/dist/native.d.ts +4 -0
  58. package/dist/native.js +1 -0
  59. package/dist/presentation.d.ts +17 -0
  60. package/dist/presentation.js +39 -0
  61. package/dist/projection-format.d.ts +68 -0
  62. package/dist/projection-format.js +118 -0
  63. package/dist/projection-window.d.ts +9 -0
  64. package/dist/projection-window.js +93 -0
  65. package/dist/projection.d.ts +22 -0
  66. package/dist/projection.js +153 -0
  67. package/dist/question-response.d.ts +20 -0
  68. package/dist/question-response.js +75 -0
  69. package/dist/questions.d.ts +74 -0
  70. package/dist/questions.js +199 -0
  71. package/dist/self-evolving.d.ts +55 -0
  72. package/dist/self-evolving.js +239 -0
  73. package/dist/skills.d.ts +15 -0
  74. package/dist/skills.js +51 -0
  75. package/dist/tool-evidence.d.ts +14 -0
  76. package/dist/tool-evidence.js +54 -0
  77. package/dist/validation.d.ts +38 -0
  78. package/dist/validation.js +68 -0
  79. package/dist/workspaces.d.ts +107 -0
  80. package/dist/workspaces.js +212 -0
  81. package/package.json +145 -150
  82. package/dist/DebugDrawer-JNA43SVR.js +0 -8
  83. package/dist/admin-DORVQfLn.d.ts +0 -340
  84. package/dist/chunk-2BGLZZ7T.js +0 -10
  85. package/dist/chunk-42D6KAQV.js +0 -2882
  86. package/dist/chunk-4XOW57KJ.js +0 -343
  87. package/dist/chunk-72KT6VWV.js +0 -1338
  88. package/dist/chunk-7XAOVFSN.js +0 -28
  89. package/dist/chunk-KJIEMSQP.js +0 -198
  90. package/dist/chunk-KN72XWP7.js +0 -394
  91. package/dist/chunk-N6THTH7P.js +0 -110
  92. package/dist/chunk-V3H5A3BR.js +0 -22501
  93. package/dist/chunk-YGRQ4Q5P.js +0 -20
  94. package/dist/chunk-YTUZFKB3.js +0 -78
  95. package/dist/core/index.d.ts +0 -28
  96. package/dist/core/index.js +0 -617
  97. package/dist/createHarness-DOLOHA62.js +0 -18
  98. package/dist/eval/index.d.ts +0 -247
  99. package/dist/eval/index.js +0 -541
  100. package/dist/file-search-BUgMSKZE.d.ts +0 -289
  101. package/dist/front/artifacts.d.ts +0 -34
  102. package/dist/front/artifacts.js +0 -21
  103. package/dist/front/index.d.ts +0 -1082
  104. package/dist/front/index.js +0 -12912
  105. package/dist/front/styles.css +0 -5380
  106. package/dist/harness-qHlMJhZo.d.ts +0 -276
  107. package/dist/middleware-CaAEXpi5.d.ts +0 -115
  108. package/dist/piChatEvent-DpE0lIb9.d.ts +0 -450
  109. package/dist/piChatSessionService-CBct-hmz.d.ts +0 -86
  110. package/dist/sandbox-BMhut3kh.d.ts +0 -317
  111. package/dist/server/agent-host/testing/compositionRouteProof.d.ts +0 -8
  112. package/dist/server/agent-host/testing/compositionRouteProof.js +0 -78
  113. package/dist/server/agent-host/testing/gatewayConformance.d.ts +0 -43
  114. package/dist/server/agent-host/testing/gatewayConformance.js +0 -605
  115. package/dist/server/index.d.ts +0 -2278
  116. package/dist/server/index.js +0 -274
  117. package/dist/server/pi-session-readability.d.ts +0 -12
  118. package/dist/server/pi-session-readability.js +0 -23
  119. package/dist/server/worker/index.d.ts +0 -68
  120. package/dist/server/worker/index.js +0 -377
  121. package/dist/share-entry-CxS6UJ_L.d.ts +0 -330
  122. package/dist/shared/index.d.ts +0 -2111
  123. package/dist/shared/index.js +0 -262
  124. package/dist/types-DcWoQMn8.d.ts +0 -1377
  125. package/docs/ACCESSIBILITY.md +0 -52
  126. package/docs/AGENT_GATEWAY_V0.md +0 -212
  127. package/docs/AGENT_PACKAGES.md +0 -90
  128. package/docs/API.md +0 -123
  129. package/docs/AUTHORING.md +0 -68
  130. package/docs/CSP.md +0 -41
  131. package/docs/ERROR_CODES.md +0 -229
  132. package/docs/KNOWN_LIMITATIONS.md +0 -100
  133. package/docs/MIGRATION.md +0 -50
  134. package/docs/PERFORMANCE.md +0 -68
  135. package/docs/PLUGINS.md +0 -138
  136. package/docs/README.md +0 -98
  137. package/docs/RISKS-MULTI-TAB.md +0 -46
  138. package/docs/SESSION_IDENTITY.md +0 -91
  139. package/docs/STYLING.md +0 -73
  140. package/docs/UI-SHADCN.md +0 -56
  141. package/docs/VERCEL_COSTS.md +0 -103
  142. package/docs/plans/archive/AGENT_EVAL_FRAMEWORK.md +0 -466
  143. package/docs/plans/archive/agent-package-spec.md +0 -1777
  144. package/docs/plans/archive/harness-followup-capabilities.md +0 -440
  145. package/docs/plans/archive/harness-tool-ui-capabilities.md +0 -144
  146. package/docs/plans/archive/pi-followup-history-projection.md +0 -229
  147. package/docs/plans/archive/pi-tools-migration.md +0 -1061
  148. package/docs/plans/archive/reviews/pi-followup-history-codex-review.md +0 -11
  149. package/docs/plans/archive/reviews/pi-followup-history-gpt-review.md +0 -64
  150. package/docs/plans/archive/reviews/pi-followup-history-opus-review.md +0 -43
  151. package/docs/plans/archive/reviews/pi-followup-history-xai-review.md +0 -43
  152. package/docs/plans/archive/vercel-base-snapshot-template-plan.md +0 -527
  153. package/docs/plans/archive/vercel-persistent-sandbox-adapter.md +0 -553
  154. package/docs/runtime-provisioning.md +0 -157
  155. package/docs/runtime.md +0 -296
  156. package/docs/tools.md +0 -102
@@ -1,2882 +0,0 @@
1
- import {
2
- SAFE_NATIVE_SESSION_ID,
3
- compareSessionOrder,
4
- noopTelemetry,
5
- safeCapture
6
- } from "./chunk-7XAOVFSN.js";
7
- import {
8
- ErrorCode
9
- } from "./chunk-KJIEMSQP.js";
10
-
11
- // src/server/harness/pi-coding-agent/createHarness.ts
12
- import { AsyncLocalStorage } from "node:async_hooks";
13
- import {
14
- createAgentSession,
15
- SessionManager as SessionManager2,
16
- DefaultResourceLoader,
17
- getAgentDir as getAgentDir2,
18
- loadSkills
19
- } from "@mariozechner/pi-coding-agent";
20
- import { createLogger } from "@hachej/boring-bash/server";
21
-
22
- // src/server/harness/pi-coding-agent/tool-adapter.ts
23
- var BORING_TOOL_ERROR_MARKER = "__boringToolError";
24
- function markToolResultErrorDetails(details) {
25
- return details && typeof details === "object" && !Array.isArray(details) ? { ...details, [BORING_TOOL_ERROR_MARKER]: true } : { [BORING_TOOL_ERROR_MARKER]: true, details };
26
- }
27
- function unmarkToolResultErrorDetails(details) {
28
- if (!details || typeof details !== "object" || Array.isArray(details)) return { isMarked: false, details };
29
- const record = { ...details };
30
- if (record[BORING_TOOL_ERROR_MARKER] !== true) return { isMarked: false, details };
31
- delete record[BORING_TOOL_ERROR_MARKER];
32
- if (Object.keys(record).length === 1 && "details" in record) return { isMarked: true, details: record.details };
33
- return { isMarked: true, details: record };
34
- }
35
- function toolTelemetryProperties(toolName, sessionId, status, startedAt, result) {
36
- const properties = {
37
- toolName,
38
- status,
39
- durationMs: Date.now() - startedAt
40
- };
41
- if (sessionId) properties.sessionId = sessionId;
42
- const errorCode = result?.details?.code;
43
- if (status === "error") {
44
- properties.errorCode = ErrorCode.safeParse(errorCode).success ? errorCode : ErrorCode.enum.TOOL_EXECUTION_ERROR;
45
- }
46
- return properties;
47
- }
48
- function adaptToolForPi(tool, sessionId, telemetry = noopTelemetry, getRunContext) {
49
- return {
50
- name: tool.name,
51
- label: tool.name,
52
- description: tool.description,
53
- parameters: tool.parameters,
54
- promptSnippet: tool.promptSnippet ?? tool.description,
55
- async execute(toolCallId, params, signal, onUpdate, _ctx) {
56
- const startedAt = Date.now();
57
- let emittedFailure = false;
58
- try {
59
- const runContext = getRunContext?.();
60
- const result = await tool.execute(params, {
61
- toolCallId,
62
- abortSignal: signal ?? new AbortController().signal,
63
- onUpdate: onUpdate ? (partial) => onUpdate({ content: [{ type: "text", text: partial }], details: void 0 }) : void 0,
64
- sessionId,
65
- userId: runContext?.userId,
66
- userEmail: runContext?.userEmail,
67
- userEmailVerified: runContext?.userEmailVerified,
68
- workspaceId: runContext?.workspaceId,
69
- requestId: runContext?.requestId
70
- });
71
- safeCapture(telemetry, {
72
- name: result.isError ? "agent.tool.failed" : "agent.tool.completed",
73
- properties: toolTelemetryProperties(
74
- tool.name,
75
- sessionId,
76
- result.isError ? "error" : "ok",
77
- startedAt,
78
- result
79
- )
80
- });
81
- if (result.isError) {
82
- emittedFailure = true;
83
- return {
84
- content: result.content,
85
- details: markToolResultErrorDetails(result.details)
86
- };
87
- }
88
- return {
89
- content: result.content,
90
- details: result.details
91
- };
92
- } catch (error) {
93
- if (!emittedFailure) {
94
- safeCapture(telemetry, {
95
- name: "agent.tool.failed",
96
- properties: toolTelemetryProperties(tool.name, sessionId, "error", startedAt)
97
- });
98
- }
99
- throw error;
100
- }
101
- }
102
- };
103
- }
104
- function adaptToolsForPi(tools, sessionId, telemetry, getRunContext) {
105
- return tools.map((tool) => adaptToolForPi(tool, sessionId, telemetry, getRunContext));
106
- }
107
-
108
- // src/server/harness/pi-coding-agent/piFollowUpQueueCompat.ts
109
- function createPiFollowUpQueueCompat() {
110
- let queue = [];
111
- const seenNonces = /* @__PURE__ */ new Set();
112
- function record(text, options) {
113
- const nonce = options?.clientNonce;
114
- if (nonce && seenNonces.has(nonce)) return false;
115
- queue.push({
116
- text,
117
- displayText: options?.displayText ?? text,
118
- clientNonce: options?.clientNonce,
119
- clientSeq: options?.clientSeq
120
- });
121
- if (nonce) seenNonces.add(nonce);
122
- return true;
123
- }
124
- function rollback(text, options) {
125
- const index = queue.findIndex((item) => item.text === text && item.clientNonce === options?.clientNonce && item.clientSeq === options?.clientSeq);
126
- if (index >= 0) queue.splice(index, 1);
127
- if (options?.clientNonce) seenNonces.delete(options.clientNonce);
128
- }
129
- function clear(piSession, options) {
130
- syncWithPi(piSession);
131
- const removed = removeNativeFollowUp(options);
132
- if (!options?.clientNonce && options?.clientSeq === void 0) {
133
- removePiQueuedFollowUp(piSession);
134
- return;
135
- }
136
- for (const item of removed) removePiQueuedFollowUp(piSession, item.request.text, item.textOrdinal);
137
- }
138
- function syncWithPi(piSession) {
139
- if (!queue.length) return;
140
- const piTexts = readPiQueuedFollowUpTexts(piSession);
141
- if (piTexts.length === 0) {
142
- queue = [];
143
- return;
144
- }
145
- if (piTexts.length >= queue.length) return;
146
- const remaining = [];
147
- let searchEnd = queue.length;
148
- for (let index = piTexts.length - 1; index >= 0; index -= 1) {
149
- const text = piTexts[index];
150
- let matchIndex = -1;
151
- for (let queueIndex = searchEnd - 1; queueIndex >= 0; queueIndex -= 1) {
152
- const item = queue[queueIndex];
153
- if (item.text === text || item.displayText === text) {
154
- matchIndex = queueIndex;
155
- break;
156
- }
157
- }
158
- if (matchIndex < 0) break;
159
- remaining.unshift(queue[matchIndex]);
160
- searchEnd = matchIndex;
161
- }
162
- if (remaining.length !== piTexts.length) return;
163
- queue = remaining;
164
- }
165
- function removeNativeFollowUp(options) {
166
- if (!queue.length) {
167
- if (!hasFollowUpSelector(options)) seenNonces.clear();
168
- return [];
169
- }
170
- const removed = [];
171
- const next = [];
172
- const textCounts = /* @__PURE__ */ new Map();
173
- for (const request of queue) {
174
- const textOrdinal = textCounts.get(request.text) ?? 0;
175
- textCounts.set(request.text, textOrdinal + 1);
176
- if (matchesFollowUpSelector(request, options)) removed.push({ request, textOrdinal });
177
- else next.push(request);
178
- }
179
- queue = next;
180
- if (queue.length === 0) seenNonces.clear();
181
- return removed;
182
- }
183
- return { record, rollback, clear };
184
- }
185
- function removePiQueuedFollowUp(piSession, text, textOrdinal = 0) {
186
- const queue = piQueueAccess(piSession);
187
- if (!text) {
188
- queue.clearAll();
189
- return;
190
- }
191
- if (queue.queuedMessages) {
192
- removeFirstMatchingOrdinal(queue.queuedMessages, (message) => userMessageText(message) === text, textOrdinal);
193
- }
194
- if (queue.followUpMessages) {
195
- removeFirstMatchingOrdinal(queue.followUpMessages, (message) => message === text, textOrdinal);
196
- }
197
- queue.emitUpdate();
198
- }
199
- function readPiQueuedFollowUpTexts(piSession) {
200
- const queue = piQueueAccess(piSession);
201
- if (queue.queuedMessages) {
202
- return queue.queuedMessages.map(userMessageText).filter((text) => text.length > 0);
203
- }
204
- if (queue.followUpMessages) return [...queue.followUpMessages];
205
- return [];
206
- }
207
- function piQueueAccess(piSession) {
208
- const session = piSession;
209
- const followUpMessages = Array.isArray(session._followUpMessages) ? session._followUpMessages : void 0;
210
- const queuedMessages = Array.isArray(session.agent?.followUpQueue?.messages) ? session.agent.followUpQueue.messages : void 0;
211
- return {
212
- followUpMessages,
213
- queuedMessages,
214
- clearAll() {
215
- session.agent?.clearFollowUpQueue?.();
216
- if (followUpMessages) followUpMessages.length = 0;
217
- session._emitQueueUpdate?.();
218
- },
219
- emitUpdate() {
220
- session._emitQueueUpdate?.();
221
- }
222
- };
223
- }
224
- function userMessageText(message) {
225
- const content = message.content;
226
- if (!Array.isArray(content)) return "";
227
- return content.map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").join("");
228
- }
229
- function removeFirstMatchingOrdinal(items, matches, ordinal) {
230
- let seen = 0;
231
- const index = items.findIndex((item) => {
232
- if (!matches(item)) return false;
233
- if (seen++ !== ordinal) return false;
234
- return true;
235
- });
236
- if (index >= 0) items.splice(index, 1);
237
- }
238
- function hasFollowUpSelector(options) {
239
- return Boolean(options?.clientNonce) || options?.clientSeq !== void 0;
240
- }
241
- function matchesFollowUpSelector(item, options) {
242
- if (!hasFollowUpSelector(options)) return true;
243
- if (options?.clientNonce) return item.clientNonce === options.clientNonce;
244
- return options?.clientSeq !== void 0 && item.clientSeq === options.clientSeq;
245
- }
246
-
247
- // src/server/pi-chat/PiAgentSessionAdapter.ts
248
- var followUpQueuesBySession = /* @__PURE__ */ new WeakMap();
249
- function followUpQueueFor(session) {
250
- const existing = followUpQueuesBySession.get(session);
251
- if (existing) return existing;
252
- const created = createPiFollowUpQueueCompat();
253
- followUpQueuesBySession.set(session, created);
254
- return created;
255
- }
256
- function normalizePromptInput(input) {
257
- if (typeof input === "string") return { text: input };
258
- return input;
259
- }
260
- var cancellationStateBySession = /* @__PURE__ */ new WeakMap();
261
- function cancellationStateFor(session) {
262
- const existing = cancellationStateBySession.get(session);
263
- if (existing) return existing;
264
- const created = { abortRequested: false };
265
- cancellationStateBySession.set(session, created);
266
- return created;
267
- }
268
- function createPiAgentSessionAdapter(session, options = {}) {
269
- const followUpQueue = followUpQueueFor(session);
270
- const cancellationState = cancellationStateFor(session);
271
- const adapter = {
272
- readSnapshot() {
273
- return {
274
- state: session.state,
275
- messages: session.messages,
276
- isStreaming: session.isStreaming,
277
- isRetrying: session.isRetrying,
278
- retryAttempt: session.retryAttempt,
279
- pendingMessageCount: session.pendingMessageCount,
280
- steeringMessages: session.getSteeringMessages(),
281
- followUpMessages: session.getFollowUpMessages(),
282
- followUpMode: session.followUpMode,
283
- sessionId: options.sessionId ?? session.sessionId,
284
- sessionName: session.sessionName
285
- };
286
- },
287
- currentModel() {
288
- const model = session.model;
289
- return typeof model?.provider === "string" && typeof model?.id === "string" ? { provider: model.provider, id: model.id } : void 0;
290
- },
291
- subscribe(listener) {
292
- return session.subscribe((event) => {
293
- if (event.type === "agent_start") cancellationState.abortRequested = false;
294
- if (!cancellationState.abortRequested) {
295
- listener(event);
296
- return;
297
- }
298
- if (event.type === "message_end" && event.message.role === "assistant") {
299
- listener({ ...event, message: { ...event.message, stopReason: "aborted" } });
300
- return;
301
- }
302
- if (event.type === "agent_end") {
303
- const messages = event.messages.map(
304
- (message, index) => index === event.messages.length - 1 && message.role === "assistant" ? { ...message, stopReason: "aborted" } : message
305
- );
306
- listener({ ...event, messages });
307
- return;
308
- }
309
- listener(event);
310
- });
311
- },
312
- async prompt(input) {
313
- const { text, options: options2 } = normalizePromptInput(input);
314
- await session.prompt(text, options2);
315
- },
316
- async followUp(text, followUpOptions) {
317
- const accepted = followUpQueue.record(text, followUpOptions);
318
- if (!accepted) return;
319
- try {
320
- await session.followUp(text);
321
- } catch (error) {
322
- followUpQueue.rollback(text, followUpOptions);
323
- throw error;
324
- }
325
- },
326
- clearFollowUp(selector) {
327
- followUpQueue.clear(session, selector);
328
- },
329
- async abort() {
330
- cancellationState.abortRequested = true;
331
- await session.abort();
332
- }
333
- };
334
- if (typeof session.abortRetry === "function") {
335
- adapter.abortRetry = () => session.abortRetry?.();
336
- }
337
- if (options.continueQueuedFollowUp) {
338
- adapter.continueQueuedFollowUp = options.continueQueuedFollowUp;
339
- }
340
- return adapter;
341
- }
342
-
343
- // src/server/harness/pi-coding-agent/sessions.ts
344
- import { randomUUID as randomUUID3 } from "node:crypto";
345
- import {
346
- readdir as readdir2,
347
- readFile as readFile3,
348
- stat as fsStat2,
349
- rm as rm2,
350
- mkdir as mkdir2,
351
- writeFile as writeFile2,
352
- appendFile as appendFile2,
353
- open as open3
354
- } from "node:fs/promises";
355
- import { closeSync, openSync, readFileSync, readSync, readdirSync, writeFileSync } from "node:fs";
356
- import { join as join2, basename, resolve } from "node:path";
357
- import { homedir } from "node:os";
358
-
359
- // src/server/config/env.ts
360
- function getEnv(name) {
361
- return process.env[name];
362
- }
363
-
364
- // src/server/harness/pi-coding-agent/sessions.ts
365
- import {
366
- parseSessionEntries,
367
- CURRENT_SESSION_VERSION as CURRENT_SESSION_VERSION2
368
- } from "@mariozechner/pi-coding-agent";
369
-
370
- // src/server/harness/pi-coding-agent/nativeSessionRename.ts
371
- import { randomUUID } from "node:crypto";
372
- import { appendFile, readFile, stat as fsStat, utimes } from "node:fs/promises";
373
- import {
374
- CURRENT_SESSION_VERSION,
375
- SessionManager
376
- } from "@mariozechner/pi-coding-agent";
377
-
378
- // src/server/harness/pi-coding-agent/sessionJsonlPrefix.ts
379
- import { open } from "node:fs/promises";
380
- var SUMMARY_PREFIX_BYTES = 64 * 1024;
381
- async function readJsonlPrefix(filepath, maxBytes = SUMMARY_PREFIX_BYTES) {
382
- const handle = await open(filepath, "r");
383
- try {
384
- const buffer = Buffer.alloc(maxBytes);
385
- const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
386
- return completeJsonlPrefix(buffer.subarray(0, bytesRead).toString("utf-8"), bytesRead === maxBytes);
387
- } finally {
388
- await handle.close();
389
- }
390
- }
391
- function parseJsonlPrefixEntries(content) {
392
- const entries = [];
393
- for (const line of content.split("\n")) {
394
- if (!line.trim()) continue;
395
- try {
396
- entries.push(JSON.parse(line));
397
- } catch {
398
- }
399
- }
400
- return entries;
401
- }
402
- function completeJsonlPrefix(content, reachedLimit) {
403
- if (!reachedLimit) return content;
404
- const lastNewline = content.lastIndexOf("\n");
405
- return lastNewline >= 0 ? content.slice(0, lastNewline + 1) : content;
406
- }
407
-
408
- // src/server/harness/pi-coding-agent/nativeSessionTranscript.ts
409
- import { createReadStream } from "node:fs";
410
- import { open as open2 } from "node:fs/promises";
411
- import { createInterface } from "node:readline";
412
-
413
- // src/server/harness/pi-coding-agent/piSessionMessages.ts
414
- function textFromPiContent(content) {
415
- if (typeof content === "string") return content;
416
- if (!Array.isArray(content)) return "";
417
- return content.map((part) => {
418
- const item = part;
419
- return item?.type === "text" && typeof item.text === "string" ? item.text : "";
420
- }).join("");
421
- }
422
-
423
- // src/server/harness/pi-coding-agent/sessionTitleAuthority.ts
424
- var USER_SESSION_TITLE_CUSTOM_TYPE = "boring.session-title-authority";
425
- var MAX_USER_SESSION_TITLE_CHARACTERS = 200;
426
- function normalizeUserSessionTitle(title) {
427
- const normalized = title.replace(/[\r\n]+/g, " ").trim();
428
- if (!normalized) throw new Error("Session title must not be empty");
429
- if (normalized.length > MAX_USER_SESSION_TITLE_CHARACTERS) {
430
- throw new Error(`Session title must be at most ${MAX_USER_SESSION_TITLE_CHARACTERS} characters`);
431
- }
432
- return normalized;
433
- }
434
- function userSessionTitleData(title) {
435
- return { titleSetByUser: true, title };
436
- }
437
- function isAttachedUserSessionTitleMarker(parent, marker) {
438
- if (marker?.type !== "custom" || typeof marker.id !== "string" || !marker.id || marker.customType !== USER_SESSION_TITLE_CUSTOM_TYPE) return false;
439
- if (parent && (typeof parent.id !== "string" || !parent.id)) return false;
440
- return marker.parentId === (parent?.id ?? null);
441
- }
442
- function userSessionTitleFromSequence(parent, marker, titleEntry) {
443
- if (!isAttachedUserSessionTitleMarker(parent, marker)) return void 0;
444
- if (titleEntry?.type !== "session_info" || typeof titleEntry.id !== "string" || !titleEntry.id || titleEntry.parentId !== marker?.id) return void 0;
445
- if (parent?.id === marker?.id || parent?.id === titleEntry.id || marker?.id === titleEntry.id) return void 0;
446
- const data = marker?.data;
447
- if (data?.titleSetByUser !== true || typeof data.title !== "string") return void 0;
448
- try {
449
- const title = normalizeUserSessionTitle(data.title);
450
- return titleEntry.name === title ? title : void 0;
451
- } catch {
452
- return void 0;
453
- }
454
- }
455
- function createUserSessionTitleProjection() {
456
- let parent;
457
- let marker;
458
- let previousKnown = false;
459
- let markerPredecessorKnown = false;
460
- let title;
461
- let timestamp;
462
- const reset = (predecessorKnown) => {
463
- parent = void 0;
464
- marker = void 0;
465
- previousKnown = predecessorKnown;
466
- markerPredecessorKnown = false;
467
- };
468
- return {
469
- get title() {
470
- return title;
471
- },
472
- get timestamp() {
473
- return timestamp;
474
- },
475
- accept(entry) {
476
- if (entry.type === "session" || entry.type === "pi_session_file") {
477
- reset(true);
478
- return;
479
- }
480
- const projected = markerPredecessorKnown ? userSessionTitleFromSequence(parent, marker, entry) : void 0;
481
- if (projected) {
482
- title = projected;
483
- timestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
484
- }
485
- parent = marker;
486
- marker = entry;
487
- markerPredecessorKnown = previousKnown;
488
- previousKnown = true;
489
- },
490
- breakSequence() {
491
- reset(false);
492
- }
493
- };
494
- }
495
- function createUserSessionTitleEntries(input) {
496
- const authority = {
497
- type: "custom",
498
- id: input.authorityId,
499
- parentId: input.parentId,
500
- timestamp: input.timestamp,
501
- customType: USER_SESSION_TITLE_CUSTOM_TYPE,
502
- data: userSessionTitleData(input.title)
503
- };
504
- return {
505
- authority,
506
- title: {
507
- type: "session_info",
508
- id: input.titleId,
509
- parentId: input.authorityId,
510
- timestamp: input.timestamp,
511
- name: input.title
512
- }
513
- };
514
- }
515
-
516
- // src/server/harness/pi-coding-agent/nativeSessionTranscript.ts
517
- var NATIVE_TAIL_CHUNK_BYTES = 64 * 1024;
518
- var NATIVE_TAIL_MAX_RECORD_BYTES = 256 * 1024;
519
- var NATIVE_TAIL_MAX_RECORD_FRAGMENTS = 4;
520
- async function nativeTranscriptOrdering(filepath, size) {
521
- const handle = await open2(filepath, "r");
522
- let end = size;
523
- let lineFragments = [];
524
- let hasEntries = false;
525
- try {
526
- while (end > 0) {
527
- const start = Math.max(0, end - NATIVE_TAIL_CHUNK_BYTES);
528
- const chunk = Buffer.alloc(end - start);
529
- const { bytesRead } = await handle.read(chunk, 0, chunk.length, start);
530
- let lineEnd = bytesRead;
531
- while (lineEnd > 0) {
532
- const newline = chunk.lastIndexOf(10, lineEnd - 1);
533
- if (newline < 0) break;
534
- const metadata2 = nativeRecordOrderingFromBoundedPrefix(
535
- nativeTailRecordPrefix(chunk.subarray(newline + 1, lineEnd), lineFragments)
536
- );
537
- lineFragments = [];
538
- hasEntries ||= metadata2.hasEntry;
539
- if (metadata2.latestMessageAtMs !== void 0) {
540
- return { hasEntries: true, latestMessageAtMs: metadata2.latestMessageAtMs };
541
- }
542
- lineEnd = newline;
543
- }
544
- if (lineEnd > 0) {
545
- lineFragments = retainNativeTailFragment(chunk.subarray(0, lineEnd), lineFragments);
546
- }
547
- end = start;
548
- }
549
- const metadata = nativeRecordOrderingFromBoundedPrefix(
550
- nativeTailRecordPrefix(lineFragments.at(-1) ?? Buffer.alloc(0), lineFragments.slice(0, -1))
551
- );
552
- return {
553
- hasEntries: hasEntries || metadata.hasEntry,
554
- ...metadata.latestMessageAtMs !== void 0 ? { latestMessageAtMs: metadata.latestMessageAtMs } : {}
555
- };
556
- } finally {
557
- await handle.close();
558
- }
559
- }
560
- function retainNativeTailFragment(fragment, fragments) {
561
- const next = [...fragments, fragment.subarray(0, NATIVE_TAIL_MAX_RECORD_BYTES)];
562
- while (next.length > NATIVE_TAIL_MAX_RECORD_FRAGMENTS || nativeTailFragmentBytes(next) > NATIVE_TAIL_MAX_RECORD_BYTES) {
563
- next.shift();
564
- }
565
- return next;
566
- }
567
- function nativeTailFragmentBytes(fragments) {
568
- return fragments.reduce((total, fragment) => total + fragment.length, 0);
569
- }
570
- function nativeTailRecordPrefix(recordStart, followingFragments) {
571
- const total = Math.min(
572
- NATIVE_TAIL_MAX_RECORD_BYTES,
573
- recordStart.length + nativeTailFragmentBytes(followingFragments)
574
- );
575
- const prefix = Buffer.allocUnsafe(total);
576
- let offset = 0;
577
- for (const fragment of [recordStart, ...followingFragments.slice().reverse()]) {
578
- const length = Math.min(fragment.length, total - offset);
579
- if (length <= 0) break;
580
- fragment.copy(prefix, offset, 0, length);
581
- offset += length;
582
- }
583
- return prefix;
584
- }
585
- function nativeRecordOrderingFromBoundedPrefix(prefix) {
586
- if (prefix.length === 0) return { hasEntry: false };
587
- const line = prefix.subarray(0, NATIVE_TAIL_MAX_RECORD_BYTES).toString("utf-8");
588
- const typeMatch = /^\s*\{\s*"type"\s*:\s*"([^"\\]+)"(?:\s*,|\s*})/.exec(line);
589
- if (!typeMatch || typeMatch[1] === "session") return { hasEntry: false };
590
- if (typeMatch[1] !== "message") return { hasEntry: true };
591
- const timestampMatch = /"timestamp"\s*:\s*"([^"\\]*(?:\\.[^"\\]*)*)"/.exec(line);
592
- if (!timestampMatch) return { hasEntry: true };
593
- const timestamp = Date.parse(timestampMatch[1]);
594
- return Number.isFinite(timestamp) ? { hasEntry: true, latestMessageAtMs: timestamp } : { hasEntry: true };
595
- }
596
- function createSessionTranscriptAccumulator() {
597
- let title;
598
- let titleTimestamp;
599
- let firstUserTitle;
600
- let entryCount = 0;
601
- let turnCount = 0;
602
- let hasAssistantReply = false;
603
- let latestMessageAtMs;
604
- const userTitle = createUserSessionTitleProjection();
605
- const authorityMarkerIds = /* @__PURE__ */ new Set();
606
- return {
607
- acceptLine(line, committed = true) {
608
- if (!line.trim()) return void 0;
609
- let entry;
610
- try {
611
- entry = JSON.parse(line);
612
- } catch {
613
- userTitle.breakSequence();
614
- return void 0;
615
- }
616
- if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
617
- userTitle.breakSequence();
618
- return void 0;
619
- }
620
- const record = entry;
621
- if (record.type === "custom" && record.customType === USER_SESSION_TITLE_CUSTOM_TYPE && typeof record.id === "string") authorityMarkerIds.add(record.id);
622
- if (committed) userTitle.accept(entry);
623
- else userTitle.breakSequence();
624
- if (entry.type !== "session") entryCount += 1;
625
- if (entry.type === "session_info" && typeof entry.name === "string") {
626
- if (!authorityMarkerIds.has(entry.parentId ?? "")) {
627
- title = entry.name;
628
- titleTimestamp = typeof entry.timestamp === "string" ? entry.timestamp : void 0;
629
- }
630
- } else if (entry.type === "message") {
631
- const timestamp = typeof entry.timestamp === "string" ? Date.parse(entry.timestamp) : Number.NaN;
632
- if (!Number.isNaN(timestamp)) latestMessageAtMs = timestamp;
633
- if (entry.message?.role === "user") {
634
- turnCount += 1;
635
- firstUserTitle ??= textFromPiContent(entry.message.content).slice(0, 80) || void 0;
636
- } else if (entry.message?.role === "assistant") {
637
- hasAssistantReply = true;
638
- }
639
- }
640
- return entry;
641
- },
642
- summary() {
643
- return {
644
- ...title ? { title } : {},
645
- ...titleTimestamp ? { titleTimestamp } : {},
646
- ...userTitle.title ? { userTitle: userTitle.title } : {},
647
- ...userTitle.timestamp ? { userTitleTimestamp: userTitle.timestamp } : {},
648
- ...firstUserTitle ? { firstUserTitle } : {},
649
- entryCount,
650
- turnCount,
651
- hasAssistantReply,
652
- ...latestMessageAtMs !== void 0 ? { latestMessageAtMs } : {}
653
- };
654
- }
655
- };
656
- }
657
- function parseSessionTranscript(content) {
658
- const accumulator = createSessionTranscriptAccumulator();
659
- const entries = [];
660
- const physicalLines = content.split("\n");
661
- const lastIndex = physicalLines.length - 1;
662
- for (const [index, line] of physicalLines.entries()) {
663
- const entry = accumulator.acceptLine(line, index < lastIndex);
664
- if (entry) entries.push(entry);
665
- }
666
- return { entries, summary: accumulator.summary() };
667
- }
668
- async function transcriptEndsWithNewline(filepath) {
669
- const handle = await open2(filepath, "r");
670
- try {
671
- const { size } = await handle.stat();
672
- if (size === 0) return true;
673
- const byte = Buffer.allocUnsafe(1);
674
- const { bytesRead } = await handle.read(byte, 0, 1, size - 1);
675
- return bytesRead === 1 && byte[0] === 10;
676
- } finally {
677
- await handle.close();
678
- }
679
- }
680
- async function summarizeSessionTranscript(filepath) {
681
- const accumulator = createSessionTranscriptAccumulator();
682
- const endsWithNewline = await transcriptEndsWithNewline(filepath);
683
- const input = createReadStream(filepath, { encoding: "utf-8" });
684
- const lines = createInterface({ input, crlfDelay: Infinity });
685
- let pending;
686
- for await (const line of lines) {
687
- if (pending !== void 0) accumulator.acceptLine(pending);
688
- pending = line;
689
- }
690
- if (pending !== void 0) accumulator.acceptLine(pending, endsWithNewline);
691
- return accumulator.summary();
692
- }
693
-
694
- // src/server/harness/pi-coding-agent/nativeSessionRename.ts
695
- var SESSION_TITLE_MAX_APPEND_ATTEMPTS = 3;
696
- var SESSION_TITLE_RETRY_CUSTOM_TYPE = "boring.session-title-retry";
697
- function sameSnapshot(a, b) {
698
- return a.dev === b.dev && a.ino === b.ino && a.size === b.size && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs;
699
- }
700
- async function readStableFile(filepath) {
701
- const before = await fsStat(filepath);
702
- const content = await readFile(filepath, "utf-8");
703
- const after = await fsStat(filepath);
704
- return sameSnapshot(before, after) ? { before, after, content } : null;
705
- }
706
- async function restoreVerifiedRenameMtime(filepath, before, verifiedSize) {
707
- try {
708
- const current = await fsStat(filepath);
709
- if (current.dev !== before.dev || current.ino !== before.ino || current.size !== verifiedSize) return;
710
- const atimeSeconds = fileTimeSeconds(before.atimeMs);
711
- const mtimeSeconds = fileTimeSeconds(before.mtimeMs);
712
- if (atimeSeconds === void 0 || mtimeSeconds === void 0) return;
713
- await utimes(filepath, atimeSeconds, mtimeSeconds);
714
- const restored = await fsStat(filepath);
715
- if (restored.size !== verifiedSize) {
716
- const restoredAtimeSeconds = fileTimeSeconds(restored.atimeMs);
717
- if (restoredAtimeSeconds !== void 0) await utimes(filepath, restoredAtimeSeconds, Date.now() / 1e3);
718
- }
719
- } catch {
720
- }
721
- }
722
- function committedPhysicalLines(content) {
723
- const lines = content.split("\n");
724
- if (!content.endsWith("\n")) lines.pop();
725
- return lines;
726
- }
727
- function hasVerifiedTitleSequence(content, append, title) {
728
- let parent;
729
- let marker;
730
- let previousKnown = false;
731
- let markerPredecessorKnown = false;
732
- for (const line of committedPhysicalLines(content)) {
733
- if (!line.trim()) continue;
734
- let entry;
735
- try {
736
- entry = JSON.parse(line);
737
- } catch {
738
- parent = void 0;
739
- marker = void 0;
740
- previousKnown = false;
741
- markerPredecessorKnown = false;
742
- continue;
743
- }
744
- if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
745
- parent = void 0;
746
- marker = void 0;
747
- previousKnown = false;
748
- markerPredecessorKnown = false;
749
- continue;
750
- }
751
- if (entry.type === "session" || entry.type === "pi_session_file") {
752
- parent = void 0;
753
- marker = void 0;
754
- previousKnown = true;
755
- markerPredecessorKnown = false;
756
- continue;
757
- }
758
- if (entry.id === append.title.id && marker?.id === append.authority.id && markerPredecessorKnown && marker.parentId === append.authority.parentId && entry.parentId === append.title.parentId && userSessionTitleFromSequence(parent, marker, entry) === title) return true;
759
- parent = marker;
760
- marker = entry;
761
- markerPredecessorKnown = previousKnown;
762
- previousKnown = true;
763
- }
764
- return false;
765
- }
766
- async function verifyTitleSequence(filepath, append, title) {
767
- const snapshot = await readStableFile(filepath);
768
- if (!snapshot || !hasVerifiedTitleSequence(snapshot.content, append, title)) return null;
769
- const parsed = parseSessionTranscript(snapshot.content);
770
- const tailId = parsed.entries.filter((entry) => entry.type !== "session" && typeof entry.id === "string").at(-1)?.id ?? null;
771
- return { stat: snapshot.after, content: snapshot.content, tailId };
772
- }
773
- function isExactRenameOnlyAppend(origin, verifiedContent, append) {
774
- const prefix = `${origin.content}${origin.content.endsWith("\n") ? "" : "\n"}`;
775
- if (!verifiedContent.startsWith(prefix)) return false;
776
- const suffix = verifiedContent.slice(prefix.length);
777
- const lines = suffix.split("\n");
778
- if (lines.length !== 3 || lines[2] !== "" || !lines[0]?.trim() || !lines[1]?.trim()) return false;
779
- try {
780
- const entries = lines.slice(0, 2).map((line) => JSON.parse(line));
781
- return entries[0]?.id === append.authority.id && entries[1]?.id === append.title.id;
782
- } catch {
783
- return false;
784
- }
785
- }
786
- function physicalTail(content) {
787
- const endsWithNewline = content.length === 0 || content.endsWith("\n");
788
- const line = content.split("\n").reverse().find((candidate) => candidate.trim());
789
- if (!line) return { endsWithNewline, valid: true, parentId: null };
790
- try {
791
- const entry = JSON.parse(line);
792
- if (!entry || typeof entry !== "object" || typeof entry.type !== "string") {
793
- return { endsWithNewline, valid: false, parentId: null };
794
- }
795
- if (entry.type === "session" || entry.type === "pi_session_file") {
796
- return { endsWithNewline, valid: true, parentId: null };
797
- }
798
- return typeof entry.id === "string" && entry.id ? { endsWithNewline, valid: true, parentId: entry.id } : { endsWithNewline, valid: false, parentId: null };
799
- } catch {
800
- return { endsWithNewline, valid: false, parentId: null };
801
- }
802
- }
803
- function latestConcurrentEntryId(manager, staleRenameIds) {
804
- for (const entry of manager.getEntries().reverse()) {
805
- if (!staleRenameIds.has(entry.id)) return entry.id;
806
- }
807
- return null;
808
- }
809
- function sessionTitleLockedError(message = "session changed while renaming; retry") {
810
- return Object.assign(new Error(message), {
811
- code: ErrorCode.enum.SESSION_LOCKED,
812
- statusCode: 409,
813
- retryable: true
814
- });
815
- }
816
- function sessionTranscriptUnreadableError(message) {
817
- return Object.assign(new Error(message), {
818
- code: ErrorCode.enum.SESSION_TRANSCRIPT_UNREADABLE,
819
- statusCode: 500,
820
- retryable: false
821
- });
822
- }
823
- async function repairNativeTerminalNewline(filepath) {
824
- const snapshot = await readStableFile(filepath);
825
- if (!snapshot) return false;
826
- const tail = physicalTail(snapshot.content);
827
- if (tail.endsWithNewline) return true;
828
- if (!tail.valid) throw sessionTitleLockedError("native session has an incomplete transcript record");
829
- await appendFile(filepath, "\n");
830
- const repaired = await readStableFile(filepath);
831
- return Boolean(repaired?.content.endsWith("\n"));
832
- }
833
- async function appendVerifiedNativeRename(filepath, sessionDir, cwd, title) {
834
- const staleRenameIds = /* @__PURE__ */ new Set();
835
- let renameOnlyOrigin;
836
- for (let attempt = 0; attempt < SESSION_TITLE_MAX_APPEND_ATTEMPTS; attempt += 1) {
837
- const attemptOrigin = await readStableFile(filepath);
838
- if (!attemptOrigin) continue;
839
- if (attempt === 0) renameOnlyOrigin = attemptOrigin;
840
- const header = parseJsonlPrefixEntries(await readJsonlPrefix(filepath)).find(
841
- (entry) => entry.type === "session"
842
- );
843
- if (header?.version !== CURRENT_SESSION_VERSION) {
844
- throw sessionTitleLockedError("This native session was created by an unsupported Pi version and cannot be renamed.");
845
- }
846
- if (!await repairNativeTerminalNewline(filepath)) continue;
847
- const committed = await readStableFile(filepath);
848
- if (!committed) continue;
849
- if (!physicalTail(committed.content).valid) {
850
- throw sessionTranscriptUnreadableError("native session contains a malformed transcript record");
851
- }
852
- const manager = SessionManager.open(filepath, sessionDir, cwd);
853
- if (staleRenameIds.size > 0) {
854
- const concurrentLeaf = latestConcurrentEntryId(manager, staleRenameIds);
855
- if (!concurrentLeaf) break;
856
- manager.branch(concurrentLeaf);
857
- manager.appendCustomEntry(SESSION_TITLE_RETRY_CUSTOM_TYPE, {});
858
- }
859
- const beforeAppend = await readStableFile(filepath);
860
- if (!beforeAppend || !beforeAppend.content.endsWith("\n")) continue;
861
- const tail = physicalTail(beforeAppend.content);
862
- const authorityParentId = manager.getLeafId();
863
- if (!tail.valid || tail.parentId !== authorityParentId) continue;
864
- const authorityId = manager.appendCustomEntry(
865
- USER_SESSION_TITLE_CUSTOM_TYPE,
866
- userSessionTitleData(title)
867
- );
868
- const titleId = manager.appendSessionInfo(title);
869
- const append = {
870
- authority: { id: authorityId, parentId: authorityParentId },
871
- title: { id: titleId, parentId: authorityId }
872
- };
873
- staleRenameIds.add(authorityId);
874
- staleRenameIds.add(titleId);
875
- const verified = await verifyTitleSequence(filepath, append, title);
876
- if (!verified) continue;
877
- if (attempt === 0 && verified.tailId === titleId && renameOnlyOrigin && isExactRenameOnlyAppend(renameOnlyOrigin, verified.content, append)) {
878
- await restoreVerifiedRenameMtime(filepath, renameOnlyOrigin.before, Number(verified.stat.size));
879
- }
880
- return;
881
- }
882
- throw sessionTitleLockedError();
883
- }
884
- function wrapperPredecessor(content) {
885
- const tail = physicalTail(content);
886
- if (!tail.valid) {
887
- if (!tail.endsWithNewline) throw sessionTitleLockedError("session has an incomplete transcript record");
888
- throw sessionTranscriptUnreadableError("session contains a malformed transcript record");
889
- }
890
- return {
891
- parentId: tail.parentId,
892
- separator: tail.endsWithNewline ? "" : "\n"
893
- };
894
- }
895
- async function appendVerifiedWrapperRename(filepath, title) {
896
- let renameOnlyStart;
897
- for (let attempt = 0; attempt < SESSION_TITLE_MAX_APPEND_ATTEMPTS; attempt += 1) {
898
- const snapshot = await readStableFile(filepath);
899
- if (!snapshot) continue;
900
- renameOnlyStart ??= snapshot.before;
901
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
902
- const predecessor = wrapperPredecessor(snapshot.content);
903
- const pair = createUserSessionTitleEntries({
904
- title,
905
- parentId: predecessor.parentId,
906
- timestamp,
907
- authorityId: randomUUID(),
908
- titleId: randomUUID()
909
- });
910
- const append = {
911
- authority: { id: pair.authority.id, parentId: pair.authority.parentId },
912
- title: { id: pair.title.id, parentId: pair.title.parentId }
913
- };
914
- const records = [pair.authority, pair.title].map((entry) => JSON.stringify(entry)).join("\n");
915
- const payload = `${predecessor.separator}${records}
916
- `;
917
- await appendFile(filepath, payload);
918
- const verified = await verifyTitleSequence(filepath, append, title);
919
- if (!verified) continue;
920
- const exactSize = Number(snapshot.after.size) + Buffer.byteLength(payload);
921
- if (attempt === 0 && Number(verified.stat.size) === exactSize && renameOnlyStart) {
922
- await restoreVerifiedRenameMtime(filepath, renameOnlyStart, exactSize);
923
- }
924
- return;
925
- }
926
- throw sessionTitleLockedError();
927
- }
928
- function fileTimeSeconds(milliseconds) {
929
- const seconds = milliseconds / 1e3;
930
- return Number.isFinite(seconds) ? seconds : void 0;
931
- }
932
-
933
- // src/server/harness/pi-coding-agent/sessionArchiveIndex.ts
934
- import { randomUUID as randomUUID2 } from "node:crypto";
935
- import { mkdir, readFile as readFile2, readdir, rename, rm, writeFile } from "node:fs/promises";
936
- import { join } from "node:path";
937
- var SESSION_ARCHIVE_DIRECTORY_NAME = ".archive";
938
- var ARCHIVE_MARKER_VERSION = 1;
939
- var ARCHIVE_MARKER_SUFFIX = ".json";
940
- function archiveDirectory(sessionDir) {
941
- return join(sessionDir, SESSION_ARCHIVE_DIRECTORY_NAME);
942
- }
943
- function markerFilename(sessionId) {
944
- return `${Buffer.from(sessionId, "utf8").toString("base64url")}${ARCHIVE_MARKER_SUFFIX}`;
945
- }
946
- function markerPath(sessionDir, sessionId) {
947
- return join(archiveDirectory(sessionDir), markerFilename(sessionId));
948
- }
949
- function parseMarker(raw) {
950
- try {
951
- const parsed = JSON.parse(raw);
952
- return parsed.version === ARCHIVE_MARKER_VERSION && typeof parsed.sessionId === "string" && parsed.sessionId.length > 0 && typeof parsed.archivedAt === "string" ? parsed : null;
953
- } catch {
954
- return null;
955
- }
956
- }
957
- async function readArchivedSessionIds(sessionDir) {
958
- const directory = archiveDirectory(sessionDir);
959
- const files = await readdir(directory).catch(() => []);
960
- const ids = /* @__PURE__ */ new Set();
961
- await Promise.all(files.filter((file) => file.endsWith(ARCHIVE_MARKER_SUFFIX)).map(async (file) => {
962
- const marker = parseMarker(await readFile2(join(directory, file), "utf8").catch(() => ""));
963
- if (marker && file === markerFilename(marker.sessionId)) ids.add(marker.sessionId);
964
- }));
965
- return ids;
966
- }
967
- async function writeSessionArchived(sessionDir, sessionId, archived, now = /* @__PURE__ */ new Date()) {
968
- const target = markerPath(sessionDir, sessionId);
969
- if (!archived) {
970
- try {
971
- await rm(target);
972
- return true;
973
- } catch (error) {
974
- if (error.code === "ENOENT") return false;
975
- throw error;
976
- }
977
- }
978
- await mkdir(archiveDirectory(sessionDir), { recursive: true });
979
- const marker = {
980
- version: ARCHIVE_MARKER_VERSION,
981
- sessionId,
982
- archivedAt: now.toISOString()
983
- };
984
- const tmp = `${target}.${randomUUID2()}.tmp`;
985
- try {
986
- await writeFile(tmp, `${JSON.stringify(marker)}
987
- `, { encoding: "utf8", flag: "wx" });
988
- await rename(tmp, target);
989
- return true;
990
- } catch (error) {
991
- await rm(tmp, { force: true }).catch(() => {
992
- });
993
- throw error;
994
- }
995
- }
996
- async function forgetArchivedSession(sessionDir, sessionId) {
997
- await rm(markerPath(sessionDir, sessionId), { force: true });
998
- }
999
-
1000
- // src/server/harness/pi-coding-agent/sessions.ts
1001
- function legacyAssistantModel(entry) {
1002
- const message = entry.message;
1003
- if (message.role !== "assistant") return void 0;
1004
- if (typeof message.provider === "string" && typeof message.model === "string") {
1005
- return { provider: message.provider, id: message.model };
1006
- }
1007
- if (typeof message.model !== "object" || message.model === null) return void 0;
1008
- const model = message.model;
1009
- return typeof model.provider === "string" && typeof model.id === "string" ? { provider: model.provider, id: model.id } : void 0;
1010
- }
1011
- function currentModelFromTranscript(entries) {
1012
- for (let index = entries.length - 1; index >= 0; index -= 1) {
1013
- const entry = entries[index];
1014
- if (!entry) continue;
1015
- if (entry.type === "model_change") return { provider: entry.provider, id: entry.modelId };
1016
- if (entry.type === "message") {
1017
- const model = legacyAssistantModel(entry);
1018
- if (model) return model;
1019
- }
1020
- }
1021
- return void 0;
1022
- }
1023
- function sessionBaseDir(explicitRoot) {
1024
- const explicit = explicitRoot?.trim();
1025
- if (explicit) return resolve(explicit);
1026
- const configured = getEnv(SESSION_ROOT_ENV)?.trim();
1027
- return configured ? resolve(configured) : join2(homedir(), ".pi", "agent", "sessions");
1028
- }
1029
- function defaultSessionDir(cwd, explicitRoot) {
1030
- if (explicitRoot && cwd.trim().length === 0) return sessionBaseDir(explicitRoot);
1031
- const safePath = `--${cwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
1032
- return join2(sessionBaseDir(explicitRoot), safePath);
1033
- }
1034
- var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
1035
- var SESSION_ROOT_ENV = "BORING_AGENT_SESSION_ROOT";
1036
- var SUMMARY_PREFIX_BYTES2 = 64 * 1024;
1037
- var MAX_CONCURRENT_SESSION_SUMMARIES = 4;
1038
- var DEFAULT_LEGACY_WORKSPACE_ID = "default";
1039
- var TRUSTED_LOCAL_USER_ID = "local";
1040
- function withArchivedFlag(summary, archivedIds) {
1041
- return archivedIds.has(summary.id) ? { ...summary, archived: true } : summary;
1042
- }
1043
- function sessionDirForNamespace(namespace, explicitRoot) {
1044
- const safeNamespace = namespace.trim();
1045
- if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
1046
- throw new Error("session namespace must contain only letters, numbers, underscores, and dashes");
1047
- }
1048
- return join2(sessionBaseDir(explicitRoot), safeNamespace);
1049
- }
1050
- function normalizeListOptions(options) {
1051
- return {
1052
- limit: options?.limit === void 0 ? void 0 : Math.max(0, options.limit),
1053
- offset: Math.max(0, options?.offset ?? 0),
1054
- includeId: options?.includeId,
1055
- includeEmpty: options?.includeEmpty === true,
1056
- archived: options?.archived ?? "all",
1057
- after: options?.after
1058
- };
1059
- }
1060
- var PiSessionStore = class {
1061
- cwd;
1062
- sessionDir;
1063
- allowLegacyUnscopedAccess;
1064
- pathDerivedLegacyAccess;
1065
- allowUnscopedNativeAccess;
1066
- prefixCache = /* @__PURE__ */ new Map();
1067
- listInFlight = /* @__PURE__ */ new Map();
1068
- writerTails = /* @__PURE__ */ new Map();
1069
- constructor(cwd, options) {
1070
- this.cwd = cwd;
1071
- if (typeof options === "string") {
1072
- this.sessionDir = options;
1073
- this.allowLegacyUnscopedAccess = true;
1074
- this.pathDerivedLegacyAccess = false;
1075
- this.allowUnscopedNativeAccess = false;
1076
- return;
1077
- }
1078
- this.allowLegacyUnscopedAccess = true;
1079
- this.pathDerivedLegacyAccess = options?.sessionDir === void 0 && options?.sessionNamespace === void 0;
1080
- this.allowUnscopedNativeAccess = options?.allowUnscopedNativeAccess === true;
1081
- this.sessionDir = options?.sessionDir ?? (options?.sessionNamespace ? sessionDirForNamespace(options.sessionNamespace, options.sessionRoot) : defaultSessionDir(options?.storageCwd ?? cwd, options?.sessionRoot));
1082
- }
1083
- getSessionDir() {
1084
- return this.sessionDir;
1085
- }
1086
- async has(ctx, sessionId) {
1087
- try {
1088
- await this.resolveSessionFile(sessionId, ctx);
1089
- return true;
1090
- } catch (error) {
1091
- if (error instanceof Error && error.message === `Session not found: ${sessionId}`) return false;
1092
- throw error;
1093
- }
1094
- }
1095
- async list(ctx, options) {
1096
- const normalizedOptions = normalizeListOptions(options);
1097
- const inFlightKey = JSON.stringify([
1098
- ctx.workspaceId,
1099
- ctx.userId ?? null,
1100
- normalizedOptions.limit ?? null,
1101
- normalizedOptions.offset,
1102
- normalizedOptions.includeId ?? null,
1103
- normalizedOptions.includeEmpty,
1104
- normalizedOptions.archived,
1105
- normalizedOptions.after ?? null
1106
- ]);
1107
- const inFlight = this.listInFlight.get(inFlightKey);
1108
- if (inFlight) return inFlight;
1109
- const promise = this.listUncached(ctx, normalizedOptions);
1110
- this.listInFlight.set(inFlightKey, promise);
1111
- try {
1112
- return await promise;
1113
- } finally {
1114
- if (this.listInFlight.get(inFlightKey) === promise) this.listInFlight.delete(inFlightKey);
1115
- }
1116
- }
1117
- async listUncached(ctx, options) {
1118
- const files = await readdir2(this.sessionDir).catch(() => []);
1119
- const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
1120
- const filepaths = jsonlFiles.map((f) => join2(this.sessionDir, f));
1121
- const fileStats = await Promise.all(filepaths.map(async (filepath) => {
1122
- try {
1123
- return { filepath, stat: await fsStat2(filepath) };
1124
- } catch {
1125
- return null;
1126
- }
1127
- }));
1128
- const existingFiles = fileStats.filter((item) => item !== null);
1129
- const referencedPiFiles = await this.referencedPiFiles(existingFiles);
1130
- const visibleFiles = await Promise.all(existingFiles.filter(({ filepath }) => !referencedPiFiles.has(resolve(filepath))).map(async (file) => ({ ...file, ...await this.sessionSortKey(file) })));
1131
- visibleFiles.sort((a, b) => compareSessionOrder(
1132
- [a.orderingUpdatedAtMs, "", a.sortId],
1133
- [b.orderingUpdatedAtMs, "", b.sortId]
1134
- ));
1135
- const { offset, limit } = options;
1136
- const includeId = options.includeId;
1137
- const archivedIds = await readArchivedSessionIds(this.sessionDir);
1138
- const after = options.after;
1139
- const afterFiles = after ? visibleFiles.filter((file) => compareSessionOrder(
1140
- [file.orderingUpdatedAtMs, after.agentTypeId, file.sortId],
1141
- after.position
1142
- ) > 0) : visibleFiles;
1143
- const filteredFiles = options.archived === "all" ? afterFiles : afterFiles.filter((file) => archivedIds.has(file.sortId) === (options.archived === "archived"));
1144
- const pageSummaries = await this.summarizeVisiblePage(filteredFiles, {
1145
- ctx,
1146
- offset,
1147
- limit,
1148
- includeId,
1149
- includeEmpty: options.includeEmpty,
1150
- archivedIds
1151
- });
1152
- if (!includeId || pageSummaries.some((summary) => summary.id === includeId)) return pageSummaries;
1153
- const includeSummary = await this.summarizeIncludedSession(ctx, includeId, referencedPiFiles);
1154
- return includeSummary ? [...pageSummaries, withArchivedFlag(includeSummary, archivedIds)] : pageSummaries;
1155
- }
1156
- async create(ctx, init) {
1157
- const manualTitle = init?.title !== void 0 ? normalizeUserSessionTitle(init.title) : void 0;
1158
- await mkdir2(this.sessionDir, { recursive: true });
1159
- const id = randomUUID3();
1160
- const now = (/* @__PURE__ */ new Date()).toISOString();
1161
- const originChannel = init?.originChannel ?? "web";
1162
- const header = {
1163
- type: "session",
1164
- version: CURRENT_SESSION_VERSION2,
1165
- id,
1166
- timestamp: now,
1167
- cwd: this.cwd,
1168
- boringSessionCtx: normalizeSessionCtx(ctx) ?? {},
1169
- boringOriginChannel: originChannel
1170
- };
1171
- const lines = [JSON.stringify(header)];
1172
- if (manualTitle) {
1173
- const pair = createUserSessionTitleEntries({
1174
- title: manualTitle,
1175
- parentId: null,
1176
- timestamp: now,
1177
- authorityId: randomUUID3(),
1178
- titleId: randomUUID3()
1179
- });
1180
- lines.push(JSON.stringify(pair.authority), JSON.stringify(pair.title));
1181
- }
1182
- const filepath = join2(this.sessionDir, `${nativeSessionFilename(id, now)}`);
1183
- await writeFile2(filepath, lines.join("\n") + "\n", "utf-8");
1184
- return {
1185
- id,
1186
- title: manualTitle ?? "New session",
1187
- createdAt: now,
1188
- updatedAt: now,
1189
- turnCount: 0,
1190
- nativeSessionId: id,
1191
- hasAssistantReply: false,
1192
- originChannel
1193
- };
1194
- }
1195
- /**
1196
- * Visibility only. Resolve tenancy and build the receipt through the bounded
1197
- * listing projection; never call `load`, whose legacy compatibility path may
1198
- * compact ui_snapshot entries and therefore mutate transcript bytes/mtime.
1199
- */
1200
- async setArchived(ctx, sessionId, archived) {
1201
- const filepath = await this.resolveSessionFile(sessionId, ctx);
1202
- const summary = await this.summarizeFile(ctx, filepath);
1203
- if (!summary) throw new Error(`Session not found: ${sessionId}`);
1204
- await writeSessionArchived(this.sessionDir, summary.id, archived);
1205
- try {
1206
- await this.resolveSessionFile(summary.id, ctx);
1207
- } catch (error) {
1208
- await forgetArchivedSession(this.sessionDir, summary.id);
1209
- throw error;
1210
- }
1211
- return archived ? { ...summary, archived: true } : summary;
1212
- }
1213
- async load(ctx, sessionId) {
1214
- const resolved = await this.resolveSessionTranscript(ctx, sessionId);
1215
- const title = projectedTranscriptTitle(resolved.sessionSummary, resolved.linkedSummary) ?? "New session";
1216
- const updatedAtMs = resolved.transcriptSummary.latestMessageAtMs ?? Math.max(resolved.fileStat.mtime.getTime(), resolved.linkedMtimeMs ?? 0);
1217
- return withArchivedFlag({
1218
- id: resolved.resolvedSessionId,
1219
- title,
1220
- createdAt: resolved.header?.timestamp ?? resolved.fileStat.birthtime.toISOString(),
1221
- updatedAt: new Date(updatedAtMs).toISOString(),
1222
- turnCount: resolved.transcriptSummary.turnCount,
1223
- ...resolved.directNative ? {
1224
- nativeSessionId: resolved.resolvedSessionId,
1225
- hasAssistantReply: resolved.transcriptSummary.hasAssistantReply
1226
- } : {},
1227
- originChannel: readHeaderOriginChannel(resolved.header)
1228
- }, await readArchivedSessionIds(this.sessionDir));
1229
- }
1230
- /**
1231
- * Returns the persisted pi message objects in file order so callers can run
1232
- * them through buildPiChatHistory — the same canonical projection the live
1233
- * event path uses. This is the cold-load counterpart to the live snapshot.
1234
- */
1235
- async loadEntries(ctx, sessionId) {
1236
- const resolved = await this.resolveSessionTranscript(ctx, sessionId);
1237
- const messages = resolved.transcriptEntries.filter((entry) => entry.type === "message").map((entry) => withStableMessageId(entry.message, entry.id));
1238
- return {
1239
- id: resolved.resolvedSessionId,
1240
- messages,
1241
- currentModel: currentModelFromTranscript(resolved.transcriptEntries)
1242
- };
1243
- }
1244
- async loadAttachment(ctx, sessionId, messageId, index) {
1245
- const resolved = await this.resolveSessionTranscript(ctx, sessionId);
1246
- const entry = resolved.transcriptEntries.filter((item) => item.type === "message").find((item) => item.id === messageId || messageIdFromPiMessage(item.message) === messageId);
1247
- const part = entry ? piImagePartAt(entry.message, index) : null;
1248
- if (!part) throw new Error(`Session attachment not found: ${sessionId}`);
1249
- const data = imagePartBuffer(part);
1250
- if (!data) throw new Error(`Session attachment not found: ${sessionId}`);
1251
- return {
1252
- data,
1253
- mediaType: part.mimeType ?? "application/octet-stream",
1254
- ...part.filename ? { filename: part.filename } : {}
1255
- };
1256
- }
1257
- async resolveSessionTranscript(ctx, sessionId) {
1258
- const filepath = await this.resolveSessionFile(sessionId, ctx);
1259
- let content;
1260
- try {
1261
- content = await readFile3(filepath, "utf-8");
1262
- } catch {
1263
- throw new Error(`Session not found: ${sessionId}`);
1264
- }
1265
- const parsedFile = parseSessionTranscript(content);
1266
- const fileEntries = parsedFile.entries;
1267
- const header = fileEntries.find(
1268
- (e) => e.type === "session"
1269
- );
1270
- const timestampNamedNative = isTimestampNamedPiSessionFile(
1271
- filepath,
1272
- header?.id ?? sessionId
1273
- );
1274
- if (!this.headerBelongsToCtx(header, ctx, timestampNamedNative)) {
1275
- throw new Error(`Session not found: ${sessionId}`);
1276
- }
1277
- const sessionEntries = fileEntries.filter(
1278
- (e) => e.type !== "session" && e.type !== "ui_snapshot"
1279
- );
1280
- const fileStat = await fsStat2(filepath);
1281
- const linkedPiFile = extractPiSessionFilePath(fileEntries);
1282
- const directNative = timestampNamedNative && !linkedPiFile;
1283
- const linked = linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? await this.readLinkedPiSession(linkedPiFile) : null;
1284
- const linkedEntries = linked?.entries.filter(
1285
- (e) => e.type !== "session"
1286
- ) ?? [];
1287
- const transcriptSummary = selectTranscriptSummary(parsedFile.summary, linked?.summary);
1288
- const transcriptEntries = transcriptSummary === linked?.summary ? linkedEntries : sessionEntries;
1289
- return {
1290
- resolvedSessionId: header?.id ?? sessionId,
1291
- header,
1292
- transcriptEntries,
1293
- sessionSummary: parsedFile.summary,
1294
- ...linked ? { linkedSummary: linked.summary } : {},
1295
- transcriptSummary,
1296
- fileStat,
1297
- linkedMtimeMs: linked?.mtimeMs,
1298
- filepath,
1299
- directNative
1300
- };
1301
- }
1302
- /**
1303
- * Addressed rename capability. Every resolved wrapper/native transcript is
1304
- * appended through the same JSONL path under one process-local writer lock.
1305
- */
1306
- async rename(ctx, sessionId, title) {
1307
- const trimmed = normalizeUserSessionTitle(title);
1308
- return await this.withWriter(sessionId, async () => {
1309
- const resolved = await this.resolveSessionTranscript(ctx, sessionId);
1310
- if (resolved.directNative) {
1311
- try {
1312
- await appendVerifiedNativeRename(
1313
- resolved.filepath,
1314
- this.sessionDir,
1315
- this.cwd,
1316
- trimmed
1317
- );
1318
- } finally {
1319
- this.prefixCache.delete(resolved.filepath);
1320
- }
1321
- return await this.load(ctx, sessionId);
1322
- }
1323
- await appendVerifiedWrapperRename(resolved.filepath, trimmed);
1324
- this.prefixCache.delete(resolved.filepath);
1325
- return await this.load(ctx, sessionId);
1326
- });
1327
- }
1328
- async withWriter(key, action) {
1329
- const previous = this.writerTails.get(key) ?? Promise.resolve();
1330
- let release;
1331
- const current = new Promise((resolve2) => {
1332
- release = resolve2;
1333
- });
1334
- const tail = previous.then(() => current);
1335
- this.writerTails.set(key, tail);
1336
- await previous;
1337
- try {
1338
- return await action();
1339
- } finally {
1340
- release();
1341
- if (this.writerTails.get(key) === tail) this.writerTails.delete(key);
1342
- }
1343
- }
1344
- // Synchronous variant used during session initialization so that no async
1345
- // I/O hop is introduced before createAgentSession (which would break test
1346
- // timing when fake timers are in use). The file is tiny (metadata only).
1347
- loadPiSessionFileSync(ctx, sessionId) {
1348
- if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
1349
- try {
1350
- const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
1351
- let filepath = direct;
1352
- let content;
1353
- try {
1354
- content = readFileSync(direct, "utf-8");
1355
- } catch {
1356
- const files = readdirSync(this.sessionDir).filter(
1357
- (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
1358
- );
1359
- if (files.length === 0) return null;
1360
- filepath = join2(this.sessionDir, files[0]);
1361
- content = readFileSync(filepath, "utf-8");
1362
- }
1363
- const entries = safeParseEntries(content);
1364
- const header = entries.find((entry) => entry.type === "session");
1365
- const directNative = isTimestampNamedPiSessionFile(filepath, header?.id ?? sessionId);
1366
- const linkedPiFile = extractPiSessionFilePath(entries);
1367
- if (!directNative) {
1368
- if (!this.headerBelongsToCtx(header, ctx)) return null;
1369
- return linkedPiFile;
1370
- }
1371
- if (this.nativeFileBelongsToCtx(header, ctx)) return filepath;
1372
- if (readHeaderSessionCtx(header) !== null) return null;
1373
- const existingWrapper = this.findWrapperReferencingNativeSessionSync(filepath);
1374
- if (existingWrapper) {
1375
- const existingEntries = parseJsonlPrefixEntries2(readJsonlPrefixSync(existingWrapper));
1376
- if (extractSessionHeaderId(existingEntries) !== sessionId) return null;
1377
- const wrapperHeader = existingEntries.find((entry) => entry.type === "session");
1378
- if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
1379
- return extractPiSessionFilePath(existingEntries);
1380
- }
1381
- this.ensureWrapperForNativeSessionSync(sessionId, filepath, entries, ctx);
1382
- return filepath;
1383
- } catch {
1384
- return null;
1385
- }
1386
- }
1387
- async loadPiSessionFile(ctx, sessionId) {
1388
- if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) return null;
1389
- try {
1390
- const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
1391
- let filepath = direct;
1392
- let content;
1393
- try {
1394
- content = await readFile3(direct, "utf-8");
1395
- } catch {
1396
- const files = await readdir2(this.sessionDir).catch(() => []);
1397
- const match = files.find(
1398
- (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
1399
- );
1400
- if (!match) return null;
1401
- filepath = join2(this.sessionDir, match);
1402
- content = await readFile3(filepath, "utf-8");
1403
- }
1404
- const entries = safeParseEntries(content);
1405
- const header = entries.find((entry) => entry.type === "session");
1406
- const directNative = isTimestampNamedPiSessionFile(filepath, header?.id ?? sessionId);
1407
- const linkedPiFile = extractPiSessionFilePath(entries);
1408
- if (!directNative) {
1409
- if (!this.headerBelongsToCtx(header, ctx)) return null;
1410
- return linkedPiFile;
1411
- }
1412
- if (this.nativeFileBelongsToCtx(header, ctx)) return filepath;
1413
- if (readHeaderSessionCtx(header) !== null) return null;
1414
- const existingWrapper = await this.findWrapperReferencingNativeSession(filepath);
1415
- if (existingWrapper) {
1416
- const wrapperSessionId2 = await this.readSessionFileId(existingWrapper);
1417
- if (wrapperSessionId2 !== sessionId) return null;
1418
- const wrapperEntries = parseJsonlPrefixEntries2(await readJsonlPrefix2(existingWrapper));
1419
- const wrapperHeader = wrapperEntries.find((entry) => entry.type === "session");
1420
- if (!this.headerBelongsToCtx(wrapperHeader, ctx)) return null;
1421
- return extractPiSessionFilePath(wrapperEntries);
1422
- }
1423
- await this.ensureWrapperForNativeSession(sessionId, filepath, ctx);
1424
- return filepath;
1425
- } catch {
1426
- return null;
1427
- }
1428
- }
1429
- async savePiSessionFile(ctx, sessionId, piFilePath) {
1430
- const filepath = await this.resolveSessionFile(sessionId, ctx);
1431
- const entry = JSON.stringify({
1432
- type: "pi_session_file",
1433
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1434
- path: piFilePath
1435
- });
1436
- await appendFile2(filepath, entry + "\n");
1437
- }
1438
- async delete(ctx, sessionId) {
1439
- const filepath = await this.resolveSessionFile(sessionId, ctx).catch(
1440
- () => null
1441
- );
1442
- if (!filepath) return;
1443
- const fileSessionId = await this.readSessionFileId(filepath);
1444
- if (fileSessionId && fileSessionId !== sessionId) return;
1445
- const linkedPiFile = await this.linkedPiFileFor(filepath);
1446
- await rm2(filepath, { force: true });
1447
- this.prefixCache.delete(filepath);
1448
- if (linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)) {
1449
- await rm2(linkedPiFile, { force: true });
1450
- this.prefixCache.delete(linkedPiFile);
1451
- }
1452
- await forgetArchivedSession(this.sessionDir, fileSessionId ?? sessionId);
1453
- }
1454
- async resolveSessionFile(sessionId, ctx) {
1455
- if (!SAFE_NATIVE_SESSION_ID.test(sessionId)) {
1456
- throw new Error(`Session not found: ${sessionId}`);
1457
- }
1458
- const direct = join2(this.sessionDir, `${sessionId}.jsonl`);
1459
- try {
1460
- await fsStat2(direct);
1461
- if (ctx) await this.assertFileBelongsToCtx(direct, ctx, sessionId);
1462
- return direct;
1463
- } catch {
1464
- }
1465
- const files = await readdir2(this.sessionDir).catch(() => []);
1466
- const match = files.find(
1467
- (f) => f.endsWith(`_${sessionId}.jsonl`) || f === `${sessionId}.jsonl`
1468
- );
1469
- if (!match) throw new Error(`Session not found: ${sessionId}`);
1470
- const matchedPath = join2(this.sessionDir, match);
1471
- if (!isTimestampNamedPiSessionFile(matchedPath, sessionId)) {
1472
- if (ctx) await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
1473
- return matchedPath;
1474
- }
1475
- if (ctx && (this.pathDerivedLegacyAccess || this.allowUnscopedNativeAccess || await this.nativeFilePin(matchedPath) !== null)) {
1476
- await this.assertFileBelongsToCtx(matchedPath, ctx, sessionId);
1477
- return matchedPath;
1478
- }
1479
- if (!ctx && this.pathDerivedLegacyAccess) return matchedPath;
1480
- throw new Error(`Session not found: ${sessionId}`);
1481
- }
1482
- async assertFileBelongsToCtx(filepath, ctx, sessionId) {
1483
- const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath));
1484
- const header = entries.find((entry) => entry.type === "session");
1485
- const directNative = !extractPiSessionFilePath(entries) && isTimestampNamedPiSessionFile(filepath, header?.id ?? sessionId);
1486
- if (!this.headerBelongsToCtx(header, ctx, directNative)) {
1487
- throw new Error(`Session not found: ${sessionId}`);
1488
- }
1489
- }
1490
- async readSessionFileId(filepath) {
1491
- try {
1492
- const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath));
1493
- return extractSessionHeaderId(entries);
1494
- } catch {
1495
- return null;
1496
- }
1497
- }
1498
- async linkedPiFileFor(filepath) {
1499
- try {
1500
- const content = await readFile3(filepath, "utf-8");
1501
- return extractPiSessionFilePath(safeParseEntries(content));
1502
- } catch {
1503
- return null;
1504
- }
1505
- }
1506
- async referencedPiFiles(files) {
1507
- const referenced = /* @__PURE__ */ new Set();
1508
- await Promise.all(files.map(async ({ filepath, stat }) => {
1509
- try {
1510
- const piFilePath = (await this.readPrefixCache(filepath, stat)).referencedPiFile;
1511
- if (piFilePath && resolve(piFilePath) !== resolve(filepath)) {
1512
- referenced.add(resolve(piFilePath));
1513
- }
1514
- } catch {
1515
- }
1516
- }));
1517
- return referenced;
1518
- }
1519
- async sessionSortKey({ filepath, stat }) {
1520
- let fileMtimeMs = Number(stat.mtimeMs);
1521
- let headerId;
1522
- const stemFallback = () => ({
1523
- orderingUpdatedAtMs: fileMtimeMs,
1524
- sortId: wrapperSessionId(filepath)
1525
- });
1526
- try {
1527
- const cached = await this.readPrefixCache(filepath, stat);
1528
- fileMtimeMs = cached.mtimeMs;
1529
- headerId = cached.headerId ?? void 0;
1530
- if (!headerId) return stemFallback();
1531
- const linkedPiFile = cached.referencedPiFile;
1532
- const hasLinkedTranscript = Boolean(
1533
- linkedPiFile && resolve(linkedPiFile) !== resolve(filepath)
1534
- );
1535
- const linkedStat = hasLinkedTranscript ? await fsStat2(linkedPiFile).catch(() => void 0) : void 0;
1536
- const linkedSnapshotMatches = linkedStat ? cached.linkedDev === Number(linkedStat.dev) && cached.linkedIno === Number(linkedStat.ino) && cached.linkedMtimeMs === Number(linkedStat.mtimeMs) && cached.linkedCtimeMs === Number(linkedStat.ctimeMs) && cached.linkedSize === Number(linkedStat.size) : cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
1537
- if (cached.orderingUpdatedAtMs !== void 0 && (!hasLinkedTranscript || linkedSnapshotMatches)) {
1538
- return { orderingUpdatedAtMs: cached.orderingUpdatedAtMs, sortId: headerId };
1539
- }
1540
- let selectedOrdering;
1541
- if (linkedPiFile && linkedStat) {
1542
- const linkedOrdering = await nativeTranscriptOrdering(
1543
- linkedPiFile,
1544
- Number(linkedStat.size)
1545
- ).catch(() => void 0);
1546
- if (linkedOrdering?.hasEntries) selectedOrdering = linkedOrdering;
1547
- }
1548
- selectedOrdering ??= await nativeTranscriptOrdering(filepath, cached.size).catch(() => ({ hasEntries: false }));
1549
- const orderingUpdatedAtMs = selectedOrdering.latestMessageAtMs ?? Math.max(fileMtimeMs, linkedStat ? Number(linkedStat.mtimeMs) : 0);
1550
- const { summary: _staleSummary, ...cacheWithoutSummary } = cached;
1551
- this.prefixCache.set(filepath, {
1552
- ...cacheWithoutSummary,
1553
- orderingUpdatedAtMs,
1554
- ...linkedStat ? {
1555
- linkedDev: Number(linkedStat.dev),
1556
- linkedIno: Number(linkedStat.ino),
1557
- linkedMtimeMs: Number(linkedStat.mtimeMs),
1558
- linkedCtimeMs: Number(linkedStat.ctimeMs),
1559
- linkedSize: Number(linkedStat.size)
1560
- } : {}
1561
- });
1562
- return { orderingUpdatedAtMs, sortId: headerId };
1563
- } catch {
1564
- return headerId ? { orderingUpdatedAtMs: fileMtimeMs, sortId: headerId } : stemFallback();
1565
- }
1566
- }
1567
- async summarizeFile(ctx, filepath, existingStat) {
1568
- try {
1569
- const fileStat = existingStat ?? await fsStat2(filepath);
1570
- const cached = this.cachedPrefix(filepath, fileStat);
1571
- if (cached && "summary" in cached && cached.sessionCtx !== void 0 && this.storedCtxBelongsToCtx(cached.sessionCtx, ctx) && await this.cachedSummaryIsFresh(filepath, cached)) {
1572
- return cached.summary ?? null;
1573
- }
1574
- const content = await readJsonlPrefix2(filepath);
1575
- const firstNewline = content.indexOf("\n");
1576
- if (firstNewline === -1) return null;
1577
- const header = JSON.parse(
1578
- content.slice(0, firstNewline)
1579
- );
1580
- if (header.type !== "session") return null;
1581
- const sessionCtx = readHeaderSessionCtx(header);
1582
- const directNative = isTimestampNamedPiSessionFile(filepath, header.id);
1583
- if (directNative ? !this.nativeFileBelongsToCtx(header, ctx) : !this.storedCtxBelongsToCtx(sessionCtx, ctx)) return null;
1584
- const entries = parseJsonlPrefixEntries2(content);
1585
- const linkedPiFile = extractPiSessionFilePath(entries);
1586
- const [sessionScan, linked, order] = await Promise.all([
1587
- this.readTranscriptSummary(filepath),
1588
- linkedPiFile && resolve(linkedPiFile) !== resolve(filepath) ? this.readLinkedPiSessionSummary(linkedPiFile) : null,
1589
- this.sessionSortKey({ filepath, stat: fileStat })
1590
- ]);
1591
- if (!sessionScan) return null;
1592
- const sessionTranscript = sessionScan.summary;
1593
- const transcript = selectTranscriptSummary(sessionTranscript, linked?.summary);
1594
- const title = projectedTranscriptTitle(sessionTranscript, linked?.summary) ?? "New session";
1595
- const summary = {
1596
- id: header.id,
1597
- title,
1598
- createdAt: header.timestamp,
1599
- updatedAt: new Date(order.orderingUpdatedAtMs).toISOString(),
1600
- turnCount: transcript.turnCount,
1601
- ...directNative ? {
1602
- nativeSessionId: header.id,
1603
- hasAssistantReply: transcript.hasAssistantReply
1604
- } : {},
1605
- originChannel: readHeaderOriginChannel(header)
1606
- };
1607
- const stableForCache = sessionScan.stable && sameFileSnapshot(fileStat, sessionScan.stat) && (!linked || linked.stable);
1608
- if (stableForCache) {
1609
- this.prefixCache.set(filepath, {
1610
- dev: Number(sessionScan.stat.dev),
1611
- ino: Number(sessionScan.stat.ino),
1612
- mtimeMs: Number(sessionScan.stat.mtimeMs),
1613
- ctimeMs: Number(sessionScan.stat.ctimeMs),
1614
- size: Number(sessionScan.stat.size),
1615
- referencedPiFile: linkedPiFile,
1616
- // Preserve the exact ordering timestamp emitted above while
1617
- // retaining snapshot identity required by title-authority caching.
1618
- headerId: header.id,
1619
- orderingUpdatedAtMs: order.orderingUpdatedAtMs,
1620
- sessionCtx,
1621
- ...linked ? {
1622
- linkedDev: Number(linked.dev),
1623
- linkedIno: Number(linked.ino),
1624
- linkedMtimeMs: linked.mtimeMs,
1625
- linkedCtimeMs: linked.ctimeMs,
1626
- linkedSize: linked.size
1627
- } : {},
1628
- summary
1629
- });
1630
- }
1631
- return summary;
1632
- } catch {
1633
- return null;
1634
- }
1635
- }
1636
- cachedPrefix(filepath, fileStat) {
1637
- const cached = this.prefixCache.get(filepath);
1638
- if (!cached) return void 0;
1639
- if (cached.dev !== Number(fileStat.dev) || cached.ino !== Number(fileStat.ino) || cached.mtimeMs !== Number(fileStat.mtimeMs) || cached.ctimeMs !== Number(fileStat.ctimeMs) || cached.size !== Number(fileStat.size)) return void 0;
1640
- return cached;
1641
- }
1642
- async cachedSummaryIsFresh(filepath, cached) {
1643
- const linkedPiFile = cached.referencedPiFile;
1644
- if (!linkedPiFile || resolve(linkedPiFile) === resolve(filepath)) return true;
1645
- try {
1646
- const linkedStat = await fsStat2(linkedPiFile);
1647
- return cached.linkedDev === Number(linkedStat.dev) && cached.linkedIno === Number(linkedStat.ino) && cached.linkedMtimeMs === Number(linkedStat.mtimeMs) && cached.linkedCtimeMs === Number(linkedStat.ctimeMs) && cached.linkedSize === Number(linkedStat.size);
1648
- } catch {
1649
- return cached.linkedMtimeMs === void 0 && cached.linkedSize === void 0;
1650
- }
1651
- }
1652
- async readPrefixCache(filepath, fileStat) {
1653
- const cached = this.cachedPrefix(filepath, fileStat);
1654
- if (cached) return cached;
1655
- let before = fileStat;
1656
- let entry;
1657
- for (let attempt = 0; attempt < 2; attempt += 1) {
1658
- const content = await readJsonlPrefix2(filepath);
1659
- const after = await fsStat2(filepath);
1660
- const entries = parseJsonlPrefixEntries2(content);
1661
- const header = entries.find((item) => item.type === "session");
1662
- entry = {
1663
- dev: Number(after.dev),
1664
- ino: Number(after.ino),
1665
- mtimeMs: Number(after.mtimeMs),
1666
- ctimeMs: Number(after.ctimeMs),
1667
- size: Number(after.size),
1668
- referencedPiFile: extractPiSessionFilePath(entries),
1669
- headerId: header?.id ?? null,
1670
- sessionCtx: readHeaderSessionCtx(header)
1671
- };
1672
- if (sameFileSnapshot(before, after)) {
1673
- this.prefixCache.set(filepath, entry);
1674
- return entry;
1675
- }
1676
- before = after;
1677
- }
1678
- return entry;
1679
- }
1680
- async summarizeVisiblePage(visibleFiles, options) {
1681
- if (options.limit === 0) return [];
1682
- const page = [];
1683
- let validSeen = 0;
1684
- let index = 0;
1685
- const batchSize = Math.min(
1686
- MAX_CONCURRENT_SESSION_SUMMARIES,
1687
- options.limit === void 0 ? Math.max(1, visibleFiles.length) : Math.max(1, options.limit)
1688
- );
1689
- while (index < visibleFiles.length && (options.limit === void 0 || page.length < options.limit)) {
1690
- const batch = visibleFiles.slice(index, index + batchSize);
1691
- index += batch.length;
1692
- const summaries = await Promise.all(
1693
- batch.map(({ filepath, stat }) => this.summarizeFile(options.ctx, filepath, stat))
1694
- );
1695
- for (const rawSummary of summaries) {
1696
- if (!rawSummary) continue;
1697
- const summary = withArchivedFlag(rawSummary, options.archivedIds);
1698
- if (summary.turnCount === 0 && !options.includeEmpty && summary.id !== options.includeId) {
1699
- continue;
1700
- }
1701
- if (validSeen < options.offset) {
1702
- validSeen += 1;
1703
- continue;
1704
- }
1705
- if (options.limit !== void 0 && page.length >= options.limit) break;
1706
- page.push(summary);
1707
- validSeen += 1;
1708
- }
1709
- }
1710
- return page;
1711
- }
1712
- async summarizeIncludedSession(ctx, sessionId, referencedPiFiles) {
1713
- try {
1714
- const filepath = await this.resolveSessionFile(sessionId, ctx);
1715
- if (referencedPiFiles.has(resolve(filepath))) return null;
1716
- return this.summarizeFile(ctx, filepath);
1717
- } catch {
1718
- return null;
1719
- }
1720
- }
1721
- findWrapperReferencingNativeSessionSync(nativePath) {
1722
- const resolvedNativePath = resolve(nativePath);
1723
- try {
1724
- const files = readdirSync(this.sessionDir).filter((file) => file.endsWith(".jsonl"));
1725
- for (const file of files) {
1726
- const filepath = join2(this.sessionDir, file);
1727
- if (resolve(filepath) === resolvedNativePath) continue;
1728
- try {
1729
- const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(readJsonlPrefixSync(filepath)));
1730
- if (linkedPiFile && resolve(linkedPiFile) === resolvedNativePath) return filepath;
1731
- } catch {
1732
- }
1733
- }
1734
- } catch {
1735
- return null;
1736
- }
1737
- return null;
1738
- }
1739
- async findWrapperReferencingNativeSession(nativePath) {
1740
- const resolvedNativePath = resolve(nativePath);
1741
- const files = await readdir2(this.sessionDir).catch(() => []);
1742
- for (const file of files) {
1743
- if (!file.endsWith(".jsonl")) continue;
1744
- const filepath = join2(this.sessionDir, file);
1745
- if (resolve(filepath) === resolvedNativePath) continue;
1746
- try {
1747
- const linkedPiFile = extractPiSessionFilePath(parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath)));
1748
- if (linkedPiFile && resolve(linkedPiFile) === resolvedNativePath) return filepath;
1749
- } catch {
1750
- }
1751
- }
1752
- return null;
1753
- }
1754
- ensureWrapperForNativeSessionSync(sessionId, nativePath, entries, ctx) {
1755
- const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
1756
- if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
1757
- try {
1758
- readFileSync(wrapperPath, "utf-8");
1759
- return wrapperPath;
1760
- } catch {
1761
- }
1762
- try {
1763
- writeFileSync(
1764
- wrapperPath,
1765
- buildNativePiSessionWrapper(sessionId, this.cwd, nativePath, entries, ctx),
1766
- { encoding: "utf-8", flag: "wx" }
1767
- );
1768
- } catch (error) {
1769
- if (error.code !== "EEXIST") throw error;
1770
- }
1771
- this.prefixCache.delete(wrapperPath);
1772
- return wrapperPath;
1773
- }
1774
- async ensureWrapperForNativeSession(sessionId, nativePath, ctx) {
1775
- const wrapperPath = join2(this.sessionDir, `${sessionId}.jsonl`);
1776
- if (resolve(wrapperPath) === resolve(nativePath)) return wrapperPath;
1777
- try {
1778
- await fsStat2(wrapperPath);
1779
- return wrapperPath;
1780
- } catch {
1781
- }
1782
- const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(nativePath));
1783
- try {
1784
- await writeFile2(
1785
- wrapperPath,
1786
- buildNativePiSessionWrapper(sessionId, this.cwd, nativePath, entries, ctx),
1787
- { encoding: "utf-8", flag: "wx" }
1788
- );
1789
- } catch (error) {
1790
- if (error.code !== "EEXIST") throw error;
1791
- }
1792
- this.prefixCache.delete(wrapperPath);
1793
- return wrapperPath;
1794
- }
1795
- async readLinkedPiSession(filepath) {
1796
- let last;
1797
- let parsed;
1798
- try {
1799
- for (let attempt = 0; attempt < 2; attempt += 1) {
1800
- const before = await fsStat2(filepath);
1801
- parsed = parseSessionTranscript(await readFile3(filepath, "utf-8"));
1802
- const after = await fsStat2(filepath);
1803
- last = after;
1804
- if (sameFileSnapshot(before, after)) {
1805
- return {
1806
- ...parsed,
1807
- dev: Number(after.dev),
1808
- ino: Number(after.ino),
1809
- mtimeMs: Number(after.mtimeMs),
1810
- ctimeMs: Number(after.ctimeMs),
1811
- size: Number(after.size),
1812
- stable: true
1813
- };
1814
- }
1815
- }
1816
- return parsed && last ? {
1817
- ...parsed,
1818
- dev: Number(last.dev),
1819
- ino: Number(last.ino),
1820
- mtimeMs: Number(last.mtimeMs),
1821
- ctimeMs: Number(last.ctimeMs),
1822
- size: Number(last.size),
1823
- stable: false
1824
- } : null;
1825
- } catch {
1826
- return null;
1827
- }
1828
- }
1829
- async readTranscriptSummary(filepath) {
1830
- let last;
1831
- let summary;
1832
- try {
1833
- for (let attempt = 0; attempt < 2; attempt += 1) {
1834
- const before = await fsStat2(filepath);
1835
- summary = await summarizeSessionTranscript(filepath);
1836
- const after = await fsStat2(filepath);
1837
- last = after;
1838
- if (sameFileSnapshot(before, after)) return { summary, stat: after, stable: true };
1839
- }
1840
- return summary && last ? { summary, stat: last, stable: false } : null;
1841
- } catch {
1842
- return null;
1843
- }
1844
- }
1845
- async readLinkedPiSessionSummary(filepath) {
1846
- const scan = await this.readTranscriptSummary(filepath);
1847
- return scan ? {
1848
- summary: scan.summary,
1849
- dev: Number(scan.stat.dev),
1850
- ino: Number(scan.stat.ino),
1851
- mtimeMs: Number(scan.stat.mtimeMs),
1852
- ctimeMs: Number(scan.stat.ctimeMs),
1853
- size: Number(scan.stat.size),
1854
- stable: scan.stable
1855
- } : null;
1856
- }
1857
- headerBelongsToCtx(header, ctx, directNative = false) {
1858
- if (directNative) return this.nativeFileBelongsToCtx(header, ctx);
1859
- return header ? this.storedCtxBelongsToCtx(readHeaderSessionCtx(header), ctx) : isEmptySessionCtx(ctx);
1860
- }
1861
- /**
1862
- * A bare Pi transcript carries no tenancy, so only a path-derived trusted
1863
- * local store may reach it. Boring-created native transcripts carry exact
1864
- * persisted tenancy and remain reachable by hosted/namespaced stores without
1865
- * minting a compatibility wrapper.
1866
- */
1867
- nativeFileBelongsToCtx(header, ctx) {
1868
- const pinned = readHeaderSessionCtx(header);
1869
- if (pinned === null && this.allowUnscopedNativeAccess) return true;
1870
- if (this.pathDerivedLegacyAccess) return this.storedCtxBelongsToCtx(pinned, ctx);
1871
- return pinned !== null && sameSessionCtx(pinned, ctx);
1872
- }
1873
- /** The Boring tenancy pin on a native transcript, or null when unpinned. */
1874
- async nativeFilePin(filepath) {
1875
- try {
1876
- const entries = parseJsonlPrefixEntries2(await readJsonlPrefix2(filepath));
1877
- return readHeaderSessionCtx(entries.find((entry) => entry.type === "session"));
1878
- } catch {
1879
- return null;
1880
- }
1881
- }
1882
- storedCtxBelongsToCtx(storedCtx, ctx) {
1883
- const trustedLocalCtx = !ctx.userId || ctx.userId === TRUSTED_LOCAL_USER_ID;
1884
- if (storedCtx === null) {
1885
- return this.allowLegacyUnscopedAccess && (isLegacyUnscopedCtx(ctx) || this.pathDerivedLegacyAccess && trustedLocalCtx);
1886
- }
1887
- if (sameSessionCtx(storedCtx, ctx)) return true;
1888
- if (!this.pathDerivedLegacyAccess || !trustedLocalCtx) return false;
1889
- if (isEmptySessionCtx(storedCtx)) return true;
1890
- return Boolean(
1891
- storedCtx.workspaceId && storedCtx.workspaceId === ctx.workspaceId && (!storedCtx.userId || !ctx.userId && storedCtx.userId === TRUSTED_LOCAL_USER_ID)
1892
- );
1893
- }
1894
- };
1895
- async function readJsonlPrefix2(filepath, maxBytes = SUMMARY_PREFIX_BYTES2) {
1896
- const handle = await open3(filepath, "r");
1897
- try {
1898
- const buffer = Buffer.alloc(maxBytes);
1899
- const { bytesRead } = await handle.read(buffer, 0, maxBytes, 0);
1900
- let content = buffer.subarray(0, bytesRead).toString("utf-8");
1901
- if (bytesRead === maxBytes) {
1902
- const lastNewline = content.lastIndexOf("\n");
1903
- if (lastNewline >= 0) content = content.slice(0, lastNewline + 1);
1904
- }
1905
- return content;
1906
- } finally {
1907
- await handle.close();
1908
- }
1909
- }
1910
- function readJsonlPrefixSync(filepath, maxBytes = SUMMARY_PREFIX_BYTES2) {
1911
- const fd = openSync(filepath, "r");
1912
- try {
1913
- const buffer = Buffer.alloc(maxBytes);
1914
- const bytesRead = readSync(fd, buffer, 0, maxBytes, 0);
1915
- let content = buffer.subarray(0, bytesRead).toString("utf-8");
1916
- if (bytesRead === maxBytes) {
1917
- const lastNewline = content.lastIndexOf("\n");
1918
- if (lastNewline >= 0) content = content.slice(0, lastNewline + 1);
1919
- }
1920
- return content;
1921
- } finally {
1922
- closeSync(fd);
1923
- }
1924
- }
1925
- function extractPiSessionFilePath(entries) {
1926
- let piFilePath = null;
1927
- for (const e of entries) {
1928
- const rec = e;
1929
- if (rec.type === "pi_session_file" && typeof rec.path === "string") {
1930
- piFilePath = rec.path;
1931
- }
1932
- }
1933
- return piFilePath;
1934
- }
1935
- function readHeaderSessionCtx(header) {
1936
- if (!header || !Object.prototype.hasOwnProperty.call(header, "boringSessionCtx")) return null;
1937
- const raw = header.boringSessionCtx;
1938
- if (!raw || typeof raw !== "object") return {};
1939
- return normalizeSessionCtx(raw) ?? {};
1940
- }
1941
- function readHeaderOriginChannel(header) {
1942
- const raw = header?.boringOriginChannel;
1943
- return typeof raw === "string" && raw.length > 0 ? raw : "web";
1944
- }
1945
- function normalizeSessionCtx(ctx) {
1946
- if (!ctx?.workspaceId && !ctx?.userId) return void 0;
1947
- return {
1948
- ...ctx.workspaceId ? { workspaceId: ctx.workspaceId } : {},
1949
- ...ctx.userId ? { userId: ctx.userId } : {}
1950
- };
1951
- }
1952
- function sameSessionCtx(a, b) {
1953
- return (a?.workspaceId ?? "") === (b?.workspaceId ?? "") && (a?.userId ?? "") === (b?.userId ?? "");
1954
- }
1955
- function isEmptySessionCtx(ctx) {
1956
- return !ctx?.workspaceId && !ctx?.userId;
1957
- }
1958
- function isLegacyUnscopedCtx(ctx) {
1959
- return isEmptySessionCtx(ctx) || ctx?.workspaceId === DEFAULT_LEGACY_WORKSPACE_ID && !ctx.userId;
1960
- }
1961
- function buildNativePiSessionWrapper(sessionId, cwd, piFilePath, entries, ctx) {
1962
- const nativeHeader = entries.find((entry) => entry.type === "session");
1963
- const timestamp = nativeHeader?.timestamp ?? (/* @__PURE__ */ new Date()).toISOString();
1964
- const header = {
1965
- type: "session",
1966
- version: CURRENT_SESSION_VERSION2,
1967
- id: sessionId,
1968
- timestamp,
1969
- cwd: nativeHeader?.cwd ?? cwd,
1970
- ...ctx !== void 0 ? { boringSessionCtx: normalizeSessionCtx(ctx) ?? {} } : {}
1971
- };
1972
- return [
1973
- header,
1974
- {
1975
- type: "pi_session_file",
1976
- timestamp,
1977
- path: piFilePath
1978
- }
1979
- ].map((line) => JSON.stringify(line)).join("\n") + "\n";
1980
- }
1981
- function extractSessionHeaderId(entries) {
1982
- const header = entries.find((entry) => entry.type === "session");
1983
- return header?.id ?? null;
1984
- }
1985
- function nativeSessionFilename(sessionId, isoTimestamp) {
1986
- return `${isoTimestamp.replace(/[:.]/g, "-")}_${sessionId}.jsonl`;
1987
- }
1988
- function wrapperSessionId(filepath) {
1989
- const base = basename(filepath);
1990
- return base.endsWith(".jsonl") ? base.slice(0, -".jsonl".length) : base;
1991
- }
1992
- function isTimestampNamedPiSessionFile(filepath, sessionId) {
1993
- const filename = basename(filepath);
1994
- return /^\d{4}-\d{2}-\d{2}/.test(filename) && filename.endsWith(`_${sessionId}.jsonl`);
1995
- }
1996
- function sameFileSnapshot(first, second) {
1997
- return first.dev === second.dev && first.ino === second.ino && first.size === second.size && first.mtimeMs === second.mtimeMs && first.ctimeMs === second.ctimeMs;
1998
- }
1999
- function selectTranscriptSummary(wrapper, native) {
2000
- return native && native.entryCount > 0 ? native : wrapper;
2001
- }
2002
- function projectedTranscriptTitle(wrapper, native) {
2003
- const selected = selectTranscriptSummary(wrapper, native);
2004
- return wrapper.userTitle ?? selected.title ?? wrapper.title ?? native?.title ?? selected.firstUserTitle;
2005
- }
2006
- function safeParseEntries(content) {
2007
- try {
2008
- return parseSessionEntries(content);
2009
- } catch {
2010
- const results = [];
2011
- for (const line of content.split("\n")) {
2012
- if (!line.trim()) continue;
2013
- try {
2014
- results.push(JSON.parse(line));
2015
- } catch {
2016
- }
2017
- }
2018
- return results;
2019
- }
2020
- }
2021
- function parseJsonlPrefixEntries2(content) {
2022
- const entries = [];
2023
- for (const line of content.split("\n")) {
2024
- if (!line.trim()) continue;
2025
- try {
2026
- entries.push(JSON.parse(line));
2027
- } catch {
2028
- }
2029
- }
2030
- return entries;
2031
- }
2032
- function withStableMessageId(message, entryId) {
2033
- if (!entryId || !message || typeof message !== "object" || Array.isArray(message)) return message;
2034
- if (typeof message.id === "string") return message;
2035
- return { ...message, id: entryId };
2036
- }
2037
- function messageIdFromPiMessage(message) {
2038
- if (!message || typeof message !== "object" || Array.isArray(message)) return void 0;
2039
- const id = message.id;
2040
- return typeof id === "string" && id.length > 0 ? id : void 0;
2041
- }
2042
- function piImagePartAt(message, index) {
2043
- if (!Number.isInteger(index) || index < 0) return null;
2044
- if (!message || typeof message !== "object" || Array.isArray(message)) return null;
2045
- const content = message.content;
2046
- if (!Array.isArray(content)) return null;
2047
- const part = content[index];
2048
- if (!part || typeof part !== "object" || Array.isArray(part)) return null;
2049
- const record = part;
2050
- if (record.type !== "image") return null;
2051
- return {
2052
- type: "image",
2053
- ...typeof record.data === "string" ? { data: record.data } : {},
2054
- ...typeof record.mimeType === "string" && record.mimeType.length > 0 ? { mimeType: record.mimeType } : {},
2055
- ...typeof record.filename === "string" && record.filename.length > 0 ? { filename: record.filename } : {}
2056
- };
2057
- }
2058
- function imagePartBuffer(part) {
2059
- const raw = part.data;
2060
- if (!raw) return null;
2061
- const match = raw.match(/^data:[^;]+;base64,(.+)$/);
2062
- try {
2063
- return Buffer.from(match ? match[1] : raw, "base64");
2064
- } catch {
2065
- return null;
2066
- }
2067
- }
2068
-
2069
- // src/server/models/modelConfig.ts
2070
- import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent";
2071
- var INFOMANIAK_PROVIDER = "infomaniak";
2072
- var INFOMANIAK_API_BASE = "https://api.infomaniak.com";
2073
- var DEFAULT_CUSTOM_MODEL_MAX_TOKENS = 16384;
2074
- var DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW = 2e5;
2075
- var DEFAULT_INFOMANIAK_MODELS = [
2076
- "moonshotai/Kimi-K2.6",
2077
- "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-FP8",
2078
- "Qwen/Qwen3.5-122B-A10B-FP8"
2079
- ];
2080
- function clean(value) {
2081
- const trimmed = value?.trim();
2082
- return trimmed && trimmed.length > 0 ? trimmed : void 0;
2083
- }
2084
- function parseEncodedModelSelection(value) {
2085
- const encoded = clean(value);
2086
- if (!encoded) return void 0;
2087
- const idx = encoded.indexOf(":");
2088
- return idx > 0 && idx < encoded.length - 1 ? { provider: encoded.slice(0, idx), id: encoded.slice(idx + 1) } : void 0;
2089
- }
2090
- function readPositiveInt(name, fallback) {
2091
- const raw = clean(getEnv(name));
2092
- if (!raw) return fallback;
2093
- const parsed = Number(raw);
2094
- return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
2095
- }
2096
- function readBoolean(name, fallback) {
2097
- const raw = clean(getEnv(name))?.toLowerCase();
2098
- if (!raw) return fallback;
2099
- if (["1", "true", "yes", "on"].includes(raw)) return true;
2100
- if (["0", "false", "no", "off"].includes(raw)) return false;
2101
- return fallback;
2102
- }
2103
- function readModelInput(name) {
2104
- const raw = clean(getEnv(name));
2105
- if (!raw) return ["text"];
2106
- const parsed = raw.split(",").map((part) => part.trim()).filter((part) => part === "text" || part === "image");
2107
- return parsed.length > 0 ? parsed : ["text"];
2108
- }
2109
- function readMaxTokensField(name, fallback = "max_completion_tokens") {
2110
- const raw = clean(getEnv(name));
2111
- return raw === "max_tokens" || raw === "max_completion_tokens" ? raw : fallback;
2112
- }
2113
- function buildOpenAICompletionsCompat(envPrefix, defaults = {}) {
2114
- return {
2115
- supportsStore: readBoolean(`${envPrefix}_SUPPORTS_STORE`, defaults.supportsStore ?? false),
2116
- supportsDeveloperRole: readBoolean(`${envPrefix}_SUPPORTS_DEVELOPER_ROLE`, defaults.supportsDeveloperRole ?? true),
2117
- supportsReasoningEffort: readBoolean(`${envPrefix}_SUPPORTS_REASONING_EFFORT`, defaults.supportsReasoningEffort ?? true),
2118
- supportsUsageInStreaming: readBoolean(`${envPrefix}_SUPPORTS_USAGE_IN_STREAMING`, defaults.supportsUsageInStreaming ?? true),
2119
- maxTokensField: readMaxTokensField(`${envPrefix}_MAX_TOKENS_FIELD`, defaults.maxTokensField)
2120
- };
2121
- }
2122
- function readApiKeyEnv(candidates) {
2123
- for (const candidate of candidates) {
2124
- const envName = clean(candidate);
2125
- if (envName && clean(getEnv(envName))) return envName;
2126
- }
2127
- return void 0;
2128
- }
2129
- function buildOpenAICompatibleProviderConfig(opts) {
2130
- return {
2131
- baseUrl: opts.baseUrl,
2132
- // Pi resolves `$ENV_VAR` config references through
2133
- // the provider environment. A bare name is a literal API key, which would
2134
- // send "INFOMANIAK_API_TOKEN" as the bearer token and cause provider 401s.
2135
- apiKey: `$${opts.apiKeyEnv}`,
2136
- api: "openai-completions",
2137
- models: opts.models.map((model) => ({
2138
- id: model.id,
2139
- name: model.name ?? model.id,
2140
- api: "openai-completions",
2141
- reasoning: readBoolean(`${opts.envPrefix}_REASONING`, true),
2142
- input: readModelInput(`${opts.envPrefix}_INPUT`),
2143
- cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
2144
- contextWindow: readPositiveInt(
2145
- `${opts.envPrefix}_CONTEXT_WINDOW`,
2146
- DEFAULT_CUSTOM_MODEL_CONTEXT_WINDOW
2147
- ),
2148
- maxTokens: readPositiveInt(
2149
- `${opts.envPrefix}_MAX_TOKENS`,
2150
- DEFAULT_CUSTOM_MODEL_MAX_TOKENS
2151
- ),
2152
- compat: buildOpenAICompletionsCompat(opts.envPrefix, opts.compatDefaults)
2153
- }))
2154
- };
2155
- }
2156
- function readInfomaniakBaseUrl() {
2157
- const explicit = clean(getEnv("BORING_AGENT_INFOMANIAK_BASE_URL"));
2158
- if (explicit) return explicit;
2159
- const productId = clean(getEnv("BORING_AGENT_INFOMANIAK_PRODUCT_ID"));
2160
- if (!productId) return void 0;
2161
- return `${INFOMANIAK_API_BASE}/2/ai/${productId}/openai/v1`;
2162
- }
2163
- function readInfomaniakModelIds() {
2164
- const configured = clean(getEnv("BORING_AGENT_INFOMANIAK_MODELS"))?.split(",").map((part) => part.trim()).filter(Boolean);
2165
- const ids = configured?.length ? configured : [...DEFAULT_INFOMANIAK_MODELS];
2166
- const legacyModel = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL"));
2167
- if (legacyModel && !ids.includes(legacyModel)) ids.push(legacyModel);
2168
- return Array.from(new Set(ids));
2169
- }
2170
- function readInfomaniakProvider() {
2171
- const modelIds = readInfomaniakModelIds();
2172
- const defaultModelId = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL")) ?? clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID")) ?? modelIds[0];
2173
- const baseUrl = readInfomaniakBaseUrl();
2174
- const apiKeyEnv = readApiKeyEnv([
2175
- clean(getEnv("BORING_AGENT_INFOMANIAK_API_KEY_ENV")) ?? "",
2176
- "INFOMANIAK_API_TOKEN",
2177
- "BORING_AGENT_INFOMANIAK_API_KEY"
2178
- ]);
2179
- if (!defaultModelId || !baseUrl || !apiKeyEnv) return void 0;
2180
- const provider = clean(getEnv("BORING_AGENT_INFOMANIAK_PROVIDER")) ?? INFOMANIAK_PROVIDER;
2181
- const modelName = clean(getEnv("BORING_AGENT_INFOMANIAK_MODEL_NAME"));
2182
- const models = modelIds.map((id) => {
2183
- const model = { id };
2184
- if (id === defaultModelId && modelName) model.name = modelName;
2185
- return model;
2186
- });
2187
- if (!models.some((model) => model.id === defaultModelId)) {
2188
- models.push({ id: defaultModelId, name: modelName });
2189
- }
2190
- return {
2191
- provider,
2192
- models: models.map((model) => ({ provider, id: model.id })),
2193
- defaultModel: { provider, id: defaultModelId },
2194
- config: buildOpenAICompatibleProviderConfig({
2195
- models,
2196
- apiKeyEnv,
2197
- baseUrl,
2198
- envPrefix: "BORING_AGENT_INFOMANIAK",
2199
- // Infomaniak's OpenAI-compatible chat endpoint rejects OpenAI's newer
2200
- // `developer` role (surfacing as "400 Unexpected message role"). Hosts
2201
- // can opt back in with BORING_AGENT_INFOMANIAK_SUPPORTS_DEVELOPER_ROLE=1.
2202
- compatDefaults: { supportsDeveloperRole: false, supportsReasoningEffort: false }
2203
- })
2204
- };
2205
- }
2206
- function readCustomProvider() {
2207
- const provider = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_PROVIDER"));
2208
- const modelId = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_ID"));
2209
- const baseUrl = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_BASE_URL"));
2210
- const apiKeyEnv = readApiKeyEnv([
2211
- clean(getEnv("BORING_AGENT_CUSTOM_MODEL_API_KEY_ENV")) ?? "",
2212
- "BORING_AGENT_CUSTOM_MODEL_API_KEY"
2213
- ]);
2214
- if (!provider || !modelId || !baseUrl || !apiKeyEnv) return void 0;
2215
- const modelName = clean(getEnv("BORING_AGENT_CUSTOM_MODEL_NAME"));
2216
- const model = { id: modelId };
2217
- if (modelName) model.name = modelName;
2218
- return {
2219
- provider,
2220
- models: [{ provider, id: modelId }],
2221
- defaultModel: { provider, id: modelId },
2222
- config: buildOpenAICompatibleProviderConfig({
2223
- models: [model],
2224
- apiKeyEnv,
2225
- baseUrl,
2226
- envPrefix: "BORING_AGENT_CUSTOM_MODEL"
2227
- })
2228
- };
2229
- }
2230
- function registerConfiguredModelProviders(registry) {
2231
- const registered = [];
2232
- const seen = /* @__PURE__ */ new Set();
2233
- for (const provider of [readInfomaniakProvider(), readCustomProvider()]) {
2234
- if (!provider) continue;
2235
- if (seen.has(provider.provider)) continue;
2236
- registry.registerProvider(provider.provider, provider.config);
2237
- registered.push(...provider.models);
2238
- seen.add(provider.provider);
2239
- }
2240
- return registered;
2241
- }
2242
- function readPiSettingsDefaultModel() {
2243
- try {
2244
- const settings = SettingsManager.create(process.cwd(), getAgentDir());
2245
- const provider = clean(settings.getDefaultProvider());
2246
- const id = clean(settings.getDefaultModel());
2247
- return provider && id ? { provider, id } : void 0;
2248
- } catch {
2249
- return void 0;
2250
- }
2251
- }
2252
- function readConfiguredDefaultModel() {
2253
- const encoded = parseEncodedModelSelection(getEnv("BORING_AGENT_DEFAULT_MODEL"));
2254
- if (encoded) return encoded;
2255
- const explicitProvider = clean(getEnv("BORING_AGENT_DEFAULT_MODEL_PROVIDER"));
2256
- const explicitId = clean(getEnv("BORING_AGENT_DEFAULT_MODEL_ID"));
2257
- if (explicitProvider && explicitId) {
2258
- return { provider: explicitProvider, id: explicitId };
2259
- }
2260
- return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.defaultModel ?? readCustomProvider()?.defaultModel;
2261
- }
2262
-
2263
- // src/server/models/modelRuntime.ts
2264
- import {
2265
- ModelRuntime
2266
- } from "@mariozechner/pi-coding-agent";
2267
- async function createConfiguredModelRuntime(options) {
2268
- const modelRuntime = await ModelRuntime.create(options);
2269
- const configuredModels = registerConfiguredModelProviders(modelRuntime);
2270
- await modelRuntime.refresh({ allowNetwork: false });
2271
- return { modelRuntime, configuredModels };
2272
- }
2273
-
2274
- // src/server/piPackages.ts
2275
- var PI_PACKAGE_RESOURCE_FILTERS = [
2276
- "extensions",
2277
- "skills",
2278
- "prompts",
2279
- "themes"
2280
- ];
2281
- function sortedFilterValues(source) {
2282
- return Object.fromEntries(
2283
- PI_PACKAGE_RESOURCE_FILTERS.map((filter) => {
2284
- const value = source[filter];
2285
- return [filter, value ? [...value].sort() : value];
2286
- })
2287
- );
2288
- }
2289
- function hasResourceFilters(source) {
2290
- return PI_PACKAGE_RESOURCE_FILTERS.some((filter) => source[filter] !== void 0);
2291
- }
2292
- function piPackageSourceKey(source) {
2293
- if (typeof source === "string") return JSON.stringify({ source });
2294
- if (!hasResourceFilters(source)) return JSON.stringify({ source: source.source });
2295
- return JSON.stringify({
2296
- source: source.source,
2297
- ...sortedFilterValues(source)
2298
- });
2299
- }
2300
- function compactPiPackages(sources) {
2301
- const seen = /* @__PURE__ */ new Set();
2302
- const result = [];
2303
- for (const source of sources) {
2304
- if (!source) continue;
2305
- const key = piPackageSourceKey(source);
2306
- if (seen.has(key)) continue;
2307
- seen.add(key);
2308
- result.push(source);
2309
- }
2310
- return result;
2311
- }
2312
- function mergePiPackageSources(base = [], additional = []) {
2313
- return compactPiPackages([...base, ...additional]);
2314
- }
2315
-
2316
- // src/server/harness/pi-coding-agent/resourceSettingsManager.ts
2317
- import { existsSync, readFileSync as readFileSync2 } from "node:fs";
2318
- import { join as join3 } from "node:path";
2319
- import { SettingsManager as SettingsManager2 } from "@mariozechner/pi-coding-agent";
2320
- function readSettingsFileIfPresent(path) {
2321
- return existsSync(path) ? readFileSync2(path, "utf-8") : void 0;
2322
- }
2323
- function mergeInjectedProjectPackages(settingsJson, piPackages, includePackage, includeConfiguredPackages = true) {
2324
- const settings = settingsJson ? JSON.parse(settingsJson) : {};
2325
- const configuredPackages = includeConfiguredPackages && Array.isArray(settings.packages) ? settings.packages : [];
2326
- return JSON.stringify({
2327
- ...settings,
2328
- packages: mergePiPackageSources(configuredPackages, piPackages).filter((source) => includePackage?.(source) ?? true)
2329
- });
2330
- }
2331
- function createResourceSettingsManager(cwd, agentDir, piPackages, options = {}) {
2332
- if (piPackages.length === 0 && !options.includePackage && options.includeConfiguredPackages !== false) {
2333
- return SettingsManager2.create(cwd, agentDir);
2334
- }
2335
- const globalSettingsPath = join3(agentDir, "settings.json");
2336
- const projectSettingsPath = join3(cwd, ".pi", "settings.json");
2337
- let globalSettingsOverrideJson;
2338
- let projectSettingsOverrideJson;
2339
- const storage = {
2340
- withLock(scope, fn) {
2341
- if (scope === "global") {
2342
- const current2 = globalSettingsOverrideJson ?? mergeInjectedProjectPackages(
2343
- readSettingsFileIfPresent(globalSettingsPath),
2344
- [],
2345
- options.includePackage,
2346
- options.includeConfiguredPackages
2347
- );
2348
- const next2 = fn(current2);
2349
- if (next2 !== void 0) globalSettingsOverrideJson = next2;
2350
- return;
2351
- }
2352
- const current = projectSettingsOverrideJson ?? mergeInjectedProjectPackages(
2353
- readSettingsFileIfPresent(projectSettingsPath),
2354
- piPackages,
2355
- options.includePackage,
2356
- options.includeConfiguredPackages
2357
- );
2358
- const next = fn(current);
2359
- if (next !== void 0) projectSettingsOverrideJson = next;
2360
- }
2361
- };
2362
- return SettingsManager2.fromStorage(storage);
2363
- }
2364
-
2365
- // src/server/harness/pi-coding-agent/createHarness.ts
2366
- var WORKSPACE_PATHS_GUIDELINE = [
2367
- "## Workspace paths",
2368
- "",
2369
- '- The "Current working directory" line in this prompt is the workspace root. Tool path arguments must be relative to it (e.g. `README.md`, `src/foo.ts`).',
2370
- "- Never pass an absolute path or a path that walks outside the workspace (no leading `/`, no `..` that escapes the root). The sandbox will reject it and the call is wasted.",
2371
- "- For `find`/`grep`/`ls`: omit the `path` argument to search from the workspace root. Pass `path` only when you need to restrict to a subdirectory, and only as a workspace-relative path.",
2372
- "- For `read`/`edit`/`write`: pass workspace-relative paths only."
2373
- ].join("\n");
2374
- var PYTHON_RUNTIME_GUIDELINE = [
2375
- "## Python runtime",
2376
- "",
2377
- "- Python 3 and the Astral `uv` package manager are available on PATH.",
2378
- "- Run scripts with `python3`.",
2379
- "- Install/manage packages with `uv pip install <pkg>` (fast; targets the workspace venv at `.boring-agent/venv`). `uv` is the canonical package manager here \u2014 don't assume only `pip`.",
2380
- "- Create venvs with `uv venv` if needed."
2381
- ].join("\n");
2382
- function composeSystemPromptAppend(hostAppend) {
2383
- return [WORKSPACE_PATHS_GUIDELINE, PYTHON_RUNTIME_GUIDELINE, hostAppend?.trim()].filter(Boolean).join("\n\n");
2384
- }
2385
- function withPiHarnessDefaults(pi) {
2386
- const { noContextFiles = true, noSkills = true, ...rest } = pi ?? {};
2387
- return { ...rest, noContextFiles, noSkills };
2388
- }
2389
- function buildDynamicPromptExtension(source) {
2390
- return (pi) => {
2391
- pi.on("before_agent_start", async (event) => {
2392
- const extra = (await source())?.trim();
2393
- if (!extra) return;
2394
- return { systemPrompt: `${event.systemPrompt}
2395
-
2396
- ${extra}` };
2397
- });
2398
- };
2399
- }
2400
- var PI_RELATIVE_SKILL_PATH_GUIDANCE = "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.";
2401
- var RESOURCE_RELATIVE_SKILL_PATH_GUIDANCE = "When a skill location is a JSON resource locator, pass its filesystem and path fields to the read tool. Resolve referenced relative paths against the locator's directory in the same filesystem; never convert a resource locator to a host path.";
2402
- function unescapeXmlText(value) {
2403
- return value.replace(/&quot;/g, '"').replace(/&apos;/g, "'").replace(/&gt;/g, ">").replace(/&lt;/g, "<").replace(/&amp;/g, "&");
2404
- }
2405
- function escapeXmlText(value) {
2406
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2407
- }
2408
- function projectSkillResourceLocations(systemPrompt, locate) {
2409
- let projected = false;
2410
- const output = systemPrompt.replace(/<location>([^<]*)<\/location>/g, (match, encodedPath) => {
2411
- const resource = locate(unescapeXmlText(encodedPath));
2412
- if (!resource) return match;
2413
- projected = true;
2414
- return `<location>${escapeXmlText(JSON.stringify(resource))}</location>`;
2415
- });
2416
- if (!projected) return systemPrompt;
2417
- return output.replace(PI_RELATIVE_SKILL_PATH_GUIDANCE, RESOURCE_RELATIVE_SKILL_PATH_GUIDANCE);
2418
- }
2419
- function buildSkillResourceProjectionExtension(locate) {
2420
- return (pi) => {
2421
- pi.on("before_agent_start", async (event) => ({
2422
- systemPrompt: projectSkillResourceLocations(event.systemPrompt, locate)
2423
- }));
2424
- };
2425
- }
2426
- function buildToolErrorResultExtension() {
2427
- return (pi) => {
2428
- pi.on("tool_result", async (event) => {
2429
- const marked = unmarkToolResultErrorDetails(event.details);
2430
- if (!marked.isMarked) return;
2431
- return {
2432
- details: marked.details,
2433
- isError: true
2434
- };
2435
- });
2436
- };
2437
- }
2438
- function modelUnavailableError(input) {
2439
- return Object.assign(new Error("Requested model is not available."), {
2440
- statusCode: 400,
2441
- code: ErrorCode.enum.TOOL_INVALID_INPUT,
2442
- details: { provider: input.model?.provider, model: input.model?.id }
2443
- });
2444
- }
2445
- function meteredSlashCommandPromptError(command) {
2446
- return Object.assign(new Error("Slash command prompt execution is disabled while metering is configured."), {
2447
- statusCode: 409,
2448
- code: ErrorCode.enum.METERING_UNSUPPORTED_COMMAND,
2449
- details: { command }
2450
- });
2451
- }
2452
- function meteredExtensionCommandContext(ctx, command) {
2453
- const block = () => {
2454
- throw meteredSlashCommandPromptError(command);
2455
- };
2456
- const guarded = Object.defineProperties({}, Object.getOwnPropertyDescriptors(ctx));
2457
- guarded.compact = block;
2458
- guarded.newSession = async () => block();
2459
- guarded.fork = async () => block();
2460
- guarded.navigateTree = async () => block();
2461
- guarded.switchSession = async () => block();
2462
- return guarded;
2463
- }
2464
- function resolveRequestedModel(modelRuntime, input, options = {}) {
2465
- const requestedId = input.model?.id;
2466
- if (!input.model || !requestedId) return void 0;
2467
- const model = modelRuntime.getModel(input.model.provider, requestedId);
2468
- const available = modelRuntime.getAvailableSnapshot();
2469
- const hasAuth = Boolean(model) && available.some(
2470
- (m) => m.provider === model.provider && m.id === model.id
2471
- );
2472
- if (!model || !hasAuth) {
2473
- if (options.strict) throw modelUnavailableError(input);
2474
- return void 0;
2475
- }
2476
- return model;
2477
- }
2478
- function resolveDefaultModel(modelRuntime, override, strict) {
2479
- if (override) {
2480
- return resolveRequestedModel(modelRuntime, { model: override }, { strict });
2481
- }
2482
- const configured = readConfiguredDefaultModel();
2483
- if (configured) {
2484
- const model = modelRuntime.getModel(configured.provider, configured.id);
2485
- if (model) return model;
2486
- }
2487
- return void 0;
2488
- }
2489
- function sessionCtxForInput(input, ctx) {
2490
- return normalizeSessionCtx2(input.ctx ?? ctx.sessionCtx ?? { workspaceId: ctx.workspaceId }) ?? {};
2491
- }
2492
- function sessionCtxFromRunContext(ctx) {
2493
- return normalizeSessionCtx2(ctx.sessionCtx ?? { workspaceId: ctx.workspaceId }) ?? {};
2494
- }
2495
- function normalizeSessionCtx2(ctx) {
2496
- if (!ctx?.workspaceId) return void 0;
2497
- return { workspaceId: ctx.workspaceId };
2498
- }
2499
- function sessionCacheKey(sessionId, ctx) {
2500
- return JSON.stringify([sessionId, ctx.workspaceId ?? ""]);
2501
- }
2502
- async function applyRequestedSessionOptions(handle, input, options = {}) {
2503
- const requestedModel = resolveRequestedModel(handle.modelRuntime, input, { strict: options.strictModelResolution });
2504
- if (requestedModel) {
2505
- const current = handle.piSession.model;
2506
- if (!current || current.provider !== requestedModel.provider || current.id !== requestedModel.id) {
2507
- await handle.piSession.setModel(requestedModel);
2508
- }
2509
- }
2510
- if (input.thinkingLevel) {
2511
- handle.piSession.setThinkingLevel(input.thinkingLevel);
2512
- }
2513
- }
2514
- var log = createLogger("pi-harness");
2515
- function deriveSourcePlugin(sourceInfo) {
2516
- if (!sourceInfo) return void 0;
2517
- const path = typeof sourceInfo.path === "string" ? sourceInfo.path : "";
2518
- const source = typeof sourceInfo.source === "string" ? sourceInfo.source : "";
2519
- const runtimePlugin = path.match(/[/\\]\.pi[/\\]extensions[/\\]([^/\\]+)/);
2520
- if (runtimePlugin) return runtimePlugin[1];
2521
- const provisionedSkill = path.match(/[/\\]\.boring-agent[/\\]skills[/\\]([^/\\]+)/);
2522
- if (provisionedSkill) return provisionedSkill[1];
2523
- if (source.startsWith("npm:")) return source.slice(4) || void 0;
2524
- if (source.startsWith("git/")) return source.split("/").filter(Boolean).pop();
2525
- const fileExtension = path.match(/[/\\]extensions[/\\]([^/\\]+)\.[cm]?[tj]sx?$/);
2526
- if (fileExtension) return fileExtension[1];
2527
- return void 0;
2528
- }
2529
- function normalizeSlashCommandInfo(command) {
2530
- const sourcePlugin = deriveSourcePlugin(command.sourceInfo);
2531
- return {
2532
- name: command.name,
2533
- ...command.description ? { description: command.description } : {},
2534
- source: command.source,
2535
- ...sourcePlugin ? { sourcePlugin } : {}
2536
- };
2537
- }
2538
- function rememberQueuedFollowUpRunContexts(piSession, state, getRunContext) {
2539
- const agent = piSession.agent;
2540
- if (!agent || typeof agent.followUp !== "function") return () => {
2541
- };
2542
- const originalFollowUp = agent.followUp;
2543
- const wrappedFollowUp = function(message) {
2544
- const ctx = getRunContext();
2545
- if (ctx && message && typeof message === "object") state.queuedFollowUpContexts.set(message, ctx);
2546
- return originalFollowUp.call(this, message);
2547
- };
2548
- agent.followUp = wrappedFollowUp;
2549
- return () => {
2550
- if (agent.followUp === wrappedFollowUp) agent.followUp = originalFollowUp;
2551
- };
2552
- }
2553
- function updateRunContextStateFromPiEvent(state, event, activateRunContext) {
2554
- if (event.type !== "message_start") return;
2555
- const message = event.message;
2556
- if (!message || typeof message !== "object") return;
2557
- const ctx = state.queuedFollowUpContexts.get(message);
2558
- if (!ctx) return;
2559
- state.queuedFollowUpContexts.delete(message);
2560
- activateRunContext(ctx);
2561
- }
2562
- function createPiCodingAgentHarness(opts) {
2563
- const pi = withPiHarnessDefaults(opts.pi);
2564
- const sessionStore = new PiSessionStore(opts.runtimeCwd ?? opts.cwd, {
2565
- sessionNamespace: opts.sessionNamespace,
2566
- sessionRoot: opts.sessionRoot,
2567
- sessionDir: opts.sessionDir,
2568
- storageCwd: opts.cwd
2569
- });
2570
- const piSessions = /* @__PURE__ */ new Map();
2571
- const runContextStorage = new AsyncLocalStorage();
2572
- const effectiveSkillPaths = [];
2573
- const effectivePackages = [];
2574
- const effectiveExtensionPaths = [];
2575
- const refreshEffectiveResources = () => {
2576
- const dynamic = pi.getHotReloadableResources?.() ?? {};
2577
- effectiveSkillPaths.splice(
2578
- 0,
2579
- effectiveSkillPaths.length,
2580
- ...pi.additionalSkillPaths ?? [],
2581
- ...dynamic.additionalSkillPaths ?? []
2582
- );
2583
- effectivePackages.splice(
2584
- 0,
2585
- effectivePackages.length,
2586
- ...mergePiPackageSources(pi.packages ?? [], dynamic.packages ?? [])
2587
- );
2588
- effectiveExtensionPaths.splice(
2589
- 0,
2590
- effectiveExtensionPaths.length,
2591
- ...pi.extensionPaths ?? [],
2592
- ...dynamic.extensionPaths ?? []
2593
- );
2594
- };
2595
- refreshEffectiveResources();
2596
- const piSessionCreations = /* @__PURE__ */ new Map();
2597
- const sessionGenerations = /* @__PURE__ */ new Map();
2598
- const generationOf = (sessionKey) => sessionGenerations.get(sessionKey) ?? 0;
2599
- async function getOrCreatePiSession(sessionId, input, ctx) {
2600
- const sessionCtx = sessionCtxForInput(input, ctx);
2601
- const sessionKey = sessionCacheKey(sessionId, sessionCtx);
2602
- const existing = piSessions.get(sessionKey);
2603
- if (existing) {
2604
- await applyRequestedSessionOptions(existing, input, { strictModelResolution: pi.strictModelResolution });
2605
- return existing;
2606
- }
2607
- const inFlight = piSessionCreations.get(sessionKey);
2608
- if (inFlight) {
2609
- const handle = await inFlight;
2610
- await applyRequestedSessionOptions(handle, input, { strictModelResolution: pi.strictModelResolution });
2611
- return handle;
2612
- }
2613
- const generation = generationOf(sessionKey);
2614
- const creation = createPiSession(sessionId, sessionCtx, input, ctx).then((handle) => {
2615
- if (generationOf(sessionKey) === generation) return handle;
2616
- disposeHandleAt(sessionKey, handle);
2617
- throw Object.assign(new Error("session not found"), {
2618
- code: ErrorCode.enum.SESSION_NOT_FOUND,
2619
- statusCode: 404
2620
- });
2621
- });
2622
- piSessionCreations.set(sessionKey, creation);
2623
- creation.catch(() => {
2624
- });
2625
- try {
2626
- return await creation;
2627
- } finally {
2628
- if (piSessionCreations.get(sessionKey) === creation) piSessionCreations.delete(sessionKey);
2629
- }
2630
- }
2631
- async function bindRunContext(ctx, run) {
2632
- return await runContextStorage.run(ctx, run);
2633
- }
2634
- function createRunBoundAdapter(handle, sessionId, ctx) {
2635
- const adapter = createPiAgentSessionAdapter(handle.piSession, { sessionId });
2636
- return {
2637
- ...adapter,
2638
- prompt: (promptInput) => bindRunContext(ctx, () => adapter.prompt(promptInput)),
2639
- followUp: (text, options) => bindRunContext(ctx, () => adapter.followUp(text, options)),
2640
- clearFollowUp: (options) => adapter.clearFollowUp(options),
2641
- ...adapter.continueQueuedFollowUp ? { continueQueuedFollowUp: () => bindRunContext(ctx, () => adapter.continueQueuedFollowUp()) } : {}
2642
- };
2643
- }
2644
- async function createPiSession(sessionId, sessionCtx, input, ctx) {
2645
- const { modelRuntime } = await createConfiguredModelRuntime(
2646
- pi.credentialStore ? { credentials: pi.credentialStore } : void 0
2647
- );
2648
- const resolvedModel = resolveRequestedModel(modelRuntime, input, { strict: pi.strictModelResolution });
2649
- const model = resolvedModel ?? resolveDefaultModel(modelRuntime, pi.defaultModel, pi.strictModelResolution);
2650
- const savedPiFile = await sessionStore.loadPiSessionFile(sessionCtx, sessionId);
2651
- const runtimeCwd = opts.runtimeCwd ?? ctx.workdir;
2652
- if (!savedPiFile) {
2653
- throw Object.assign(
2654
- new Error(`Session not found: ${sessionId}`),
2655
- { code: ErrorCode.enum.SESSION_NOT_FOUND, statusCode: 404 }
2656
- );
2657
- }
2658
- let sessionManager;
2659
- try {
2660
- sessionManager = SessionManager2.open(savedPiFile, void 0, runtimeCwd);
2661
- } catch (error) {
2662
- throw Object.assign(
2663
- new Error(`failed to open pi session transcript for '${sessionId}'`),
2664
- { code: ErrorCode.enum.SESSION_TRANSCRIPT_UNREADABLE, statusCode: 500, cause: error }
2665
- );
2666
- }
2667
- refreshEffectiveResources();
2668
- const composedSystemPromptAppend = composeSystemPromptAppend(opts.systemPromptAppend);
2669
- const dynamicPromptExtension = opts.systemPromptDynamic ? buildDynamicPromptExtension(opts.systemPromptDynamic) : void 0;
2670
- const agentDir = getAgentDir2();
2671
- const toolErrorResultExtension = buildToolErrorResultExtension();
2672
- const skillResourceProjectionExtension = pi.locateSkillResource ? buildSkillResourceProjectionExtension(pi.locateSkillResource) : void 0;
2673
- const extensionFactories = [
2674
- toolErrorResultExtension,
2675
- ...dynamicPromptExtension ? [dynamicPromptExtension] : [],
2676
- ...pi.extensionFactories ?? [],
2677
- ...skillResourceProjectionExtension ? [skillResourceProjectionExtension] : []
2678
- ];
2679
- const settingsManager = createResourceSettingsManager(
2680
- opts.cwd,
2681
- agentDir,
2682
- effectivePackages,
2683
- { includeConfiguredPackages: pi.noAmbientPackages !== true }
2684
- );
2685
- const resourceLoader = new DefaultResourceLoader({
2686
- cwd: opts.cwd,
2687
- agentDir,
2688
- settingsManager,
2689
- appendSystemPromptOverride: (base) => [...base, composedSystemPromptAppend],
2690
- ...effectiveExtensionPaths.length ? { additionalExtensionPaths: effectiveExtensionPaths } : {},
2691
- ...extensionFactories.length ? { extensionFactories } : {},
2692
- ...pi.noContextFiles ? { noContextFiles: true } : {},
2693
- ...pi.noSkills ? { noSkills: true } : {},
2694
- ...pi.noExtensions ? { noExtensions: true } : {},
2695
- ...effectiveSkillPaths.length ? { additionalSkillPaths: effectiveSkillPaths } : {},
2696
- // skillsOverride REPLACES Pi's resolved skill set, which includes
2697
- // skills contributed by host-declared pi packages (e.g.
2698
- // @hachej/boring-pi → boring-plugin-authoring). Only trigger it for
2699
- // the explicit `noSkills` opt-out, where the host wants a clean slate.
2700
- // Passing additionalSkillPaths is not, by itself, a request to throw
2701
- // away package skills — those should keep flowing through Pi's loader
2702
- // and merge with the additional paths.
2703
- ...pi.noSkills ? {
2704
- skillsOverride: () => loadSkills({
2705
- cwd: opts.cwd,
2706
- agentDir,
2707
- skillPaths: effectiveSkillPaths,
2708
- includeDefaults: false
2709
- })
2710
- } : {}
2711
- });
2712
- await resourceLoader?.reload();
2713
- const runContextState = { queuedFollowUpContexts: /* @__PURE__ */ new WeakMap() };
2714
- const { session: piSession } = await createAgentSession({
2715
- cwd: runtimeCwd,
2716
- // Suppress Pi's built-in filesystem/shell tools while keeping Boring's
2717
- // adapted tool catalog active. Do NOT pass an explicit empty tool-name
2718
- // allowlist: in the current Pi SDK that disables custom tools too.
2719
- noTools: "builtin",
2720
- customTools: adaptToolsForPi(opts.tools, sessionId, opts.telemetry, () => runContextStorage.getStore()),
2721
- model,
2722
- thinkingLevel: input.thinkingLevel ?? "off",
2723
- sessionManager,
2724
- modelRuntime,
2725
- settingsManager,
2726
- ...resourceLoader ? { resourceLoader } : {}
2727
- });
2728
- const restoreFollowUpContextWrapper = rememberQueuedFollowUpRunContexts(piSession, runContextState, () => runContextStorage.getStore());
2729
- const unsubscribePiRunContextListener = piSession.subscribe((event) => updateRunContextStateFromPiEvent(runContextState, event, (ctx2) => runContextStorage.enterWith(ctx2)));
2730
- const unsubscribeRunContextListener = () => {
2731
- unsubscribePiRunContextListener();
2732
- restoreFollowUpContextWrapper();
2733
- };
2734
- const handle = {
2735
- piSession,
2736
- modelRuntime,
2737
- sessionManager,
2738
- resourceLoader,
2739
- sessionId,
2740
- sessionCtx,
2741
- runContextState,
2742
- unsubscribeRunContextListener
2743
- };
2744
- piSessions.set(sessionCacheKey(sessionId, sessionCtx), handle);
2745
- return handle;
2746
- }
2747
- async function reloadPiSession(sessionId) {
2748
- const handles = piSessionHandlesFor(sessionId);
2749
- if (handles.length === 0) return false;
2750
- refreshEffectiveResources();
2751
- await Promise.all(handles.map((handle) => handle.piSession.reload()));
2752
- return true;
2753
- }
2754
- function disposeHandleAt(key, handle) {
2755
- handle.unsubscribeRunContextListener();
2756
- handle.piSession.dispose();
2757
- if (piSessions.get(key) === handle) piSessions.delete(key);
2758
- }
2759
- function disposePiSession(sessionId, ctx) {
2760
- if (ctx) {
2761
- const key = sessionCacheKey(sessionId, ctx);
2762
- const handle = piSessions.get(key);
2763
- if (!handle) return;
2764
- disposeHandleAt(key, handle);
2765
- return;
2766
- }
2767
- for (const [key, handle] of piSessions) {
2768
- if (handle.sessionId !== sessionId) continue;
2769
- disposeHandleAt(key, handle);
2770
- }
2771
- }
2772
- function piSessionHandlesFor(sessionId) {
2773
- return [...piSessions.values()].filter((handle) => handle.sessionId === sessionId);
2774
- }
2775
- async function getOrCreatePiSessionForCommand(sessionId, ctx) {
2776
- const sessionCtx = normalizeSessionCtx2(ctx.sessionCtx) ?? sessionCtxFromRunContext(ctx);
2777
- return getOrCreatePiSession(sessionId, { sessionId, content: "", ctx: sessionCtx }, ctx);
2778
- }
2779
- const originalDelete = sessionStore.delete.bind(sessionStore);
2780
- sessionStore.delete = async (ctx, sessionId) => {
2781
- const key = sessionCacheKey(sessionId, ctx);
2782
- sessionGenerations.set(key, generationOf(key) + 1);
2783
- await Promise.allSettled([piSessionCreations.get(key)]);
2784
- await originalDelete(ctx, sessionId);
2785
- disposePiSession(sessionId, ctx);
2786
- };
2787
- return {
2788
- id: "pi-coding-agent",
2789
- placement: "server",
2790
- sessions: sessionStore,
2791
- /**
2792
- * Pi exposes the resolved system prompt as a getter on AgentSession.
2793
- * Sessions are created lazily on the first prompt, so callers may see
2794
- * `undefined` for a session that hasn't been written to yet — that's
2795
- * the expected pre-first-turn state, not an error.
2796
- */
2797
- getSystemPrompt(sessionId) {
2798
- const prompt = piSessionHandlesFor(sessionId)[0]?.piSession.systemPrompt;
2799
- return prompt && pi.locateSkillResource ? projectSkillResourceLocations(prompt, pi.locateSkillResource) : prompt;
2800
- },
2801
- hasPiSession(sessionId, ctx) {
2802
- return ctx ? piSessions.has(sessionCacheKey(sessionId, ctx)) : piSessionHandlesFor(sessionId).length > 0;
2803
- },
2804
- /**
2805
- * Surface Pi's skill/extension load diagnostics for a session so silent
2806
- * load failures (bad SKILL.md, extension import errors) reach the UI and
2807
- * the agent. Returns [] when the session has no live pi session yet.
2808
- * The resourceLoader getters are synchronous.
2809
- */
2810
- getResourceDiagnostics(sessionId) {
2811
- const handle = piSessionHandlesFor(sessionId)[0];
2812
- if (!handle) return [];
2813
- const out = [];
2814
- const seen = /* @__PURE__ */ new Set();
2815
- const push = (entry) => {
2816
- const key = `${entry.source}
2817
- ${entry.message}`;
2818
- if (seen.has(key)) return;
2819
- seen.add(key);
2820
- out.push(entry);
2821
- };
2822
- for (const diagnostic of handle.resourceLoader.getSkills().diagnostics) {
2823
- push({
2824
- source: "pi-skills",
2825
- message: diagnostic.path && !diagnostic.message.includes(diagnostic.path) ? `${diagnostic.message} (${diagnostic.path})` : diagnostic.message,
2826
- ...diagnostic.path ? { path: diagnostic.path } : {}
2827
- });
2828
- }
2829
- for (const error of handle.resourceLoader.getExtensions().errors) {
2830
- push({
2831
- source: "pi-extensions",
2832
- message: error.error.includes(error.path) ? error.error : `${error.error} (${error.path})`,
2833
- path: error.path
2834
- });
2835
- }
2836
- return out;
2837
- },
2838
- reloadSession: reloadPiSession,
2839
- async getSlashCommands(sessionId, ctx) {
2840
- const handle = await getOrCreatePiSessionForCommand(sessionId, ctx);
2841
- return handle.resourceLoader.getExtensions().runtime.getCommands().map(normalizeSlashCommandInfo);
2842
- },
2843
- async executeSlashCommand(sessionId, name, args, ctx) {
2844
- const handle = await getOrCreatePiSessionForCommand(sessionId, ctx);
2845
- await bindRunContext(ctx, async () => {
2846
- const command = handle.piSession.extensionRunner.getCommand(name);
2847
- if (command) {
2848
- const commandContext = handle.piSession.extensionRunner.createCommandContext();
2849
- await command.handler(args, ctx.allowPromptDispatch === false ? meteredExtensionCommandContext(commandContext, name) : commandContext);
2850
- return;
2851
- }
2852
- const knownCommand = handle.resourceLoader.getExtensions().runtime.getCommands().some((candidate) => candidate.name === name);
2853
- if (!knownCommand) throw new Error(`command '${name}' not registered in session '${sessionId}'`);
2854
- if (ctx.allowPromptDispatch === false) throw meteredSlashCommandPromptError(name);
2855
- const text = args.trim() ? `/${name} ${args}` : `/${name}`;
2856
- await handle.piSession.prompt(text);
2857
- });
2858
- },
2859
- async getPiSessionAdapter(input, ctx) {
2860
- if (!input.sessionId) throw new Error("sessionId is required to create a Pi session adapter");
2861
- const handle = await getOrCreatePiSession(input.sessionId, input, ctx);
2862
- return createRunBoundAdapter(handle, input.sessionId, ctx);
2863
- }
2864
- };
2865
- }
2866
-
2867
- export {
2868
- getEnv,
2869
- PiSessionStore,
2870
- parseEncodedModelSelection,
2871
- readConfiguredDefaultModel,
2872
- createConfiguredModelRuntime,
2873
- PI_PACKAGE_RESOURCE_FILTERS,
2874
- piPackageSourceKey,
2875
- compactPiPackages,
2876
- mergePiPackageSources,
2877
- createResourceSettingsManager,
2878
- withPiHarnessDefaults,
2879
- projectSkillResourceLocations,
2880
- deriveSourcePlugin,
2881
- createPiCodingAgentHarness
2882
- };