@hachej/boring-agent 0.1.111 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +262 -51
- package/dist/agents.d.ts +75 -0
- package/dist/agents.js +91 -0
- package/dist/approval.d.ts +71 -0
- package/dist/approval.js +126 -0
- package/dist/artifacts.d.ts +53 -0
- package/dist/artifacts.js +129 -0
- package/dist/ask-user.d.ts +40 -0
- package/dist/ask-user.js +134 -0
- package/dist/attachment.d.ts +5 -0
- package/dist/attachment.js +4 -0
- package/dist/browser-task.d.ts +32 -0
- package/dist/browser-task.js +119 -0
- package/dist/chat-transport.d.ts +80 -0
- package/dist/chat-transport.js +306 -0
- package/dist/contracts.d.ts +40 -0
- package/dist/contracts.js +1 -0
- package/dist/conversations.d.ts +136 -0
- package/dist/conversations.js +366 -0
- package/dist/definitions.d.ts +37 -0
- package/dist/definitions.js +99 -0
- package/dist/delivery-input.d.ts +35 -0
- package/dist/delivery-input.js +56 -0
- package/dist/delivery.d.ts +33 -0
- package/dist/delivery.js +154 -0
- package/dist/durable-wait.d.ts +23 -0
- package/dist/durable-wait.js +79 -0
- package/dist/file-convert.d.ts +46 -0
- package/dist/file-convert.js +159 -0
- package/dist/file-guard.d.ts +50 -0
- package/dist/file-guard.js +177 -0
- package/dist/file-types.d.ts +17 -0
- package/dist/file-types.js +95 -0
- package/dist/gateway-provider.d.ts +24 -0
- package/dist/gateway-provider.js +42 -0
- package/dist/git.d.ts +34 -0
- package/dist/git.js +79 -0
- package/dist/harness-pool.d.ts +38 -0
- package/dist/harness-pool.js +134 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1 -0
- package/dist/json-schema-tool.d.ts +25 -0
- package/dist/json-schema-tool.js +19 -0
- package/dist/memory/optchat-core.d.ts +118 -0
- package/dist/memory/optchat-core.js +317 -0
- package/dist/memory/optchat.d.ts +156 -0
- package/dist/memory/optchat.js +432 -0
- package/dist/mentions.d.ts +56 -0
- package/dist/mentions.js +140 -0
- package/dist/metering.d.ts +183 -0
- package/dist/metering.js +443 -0
- package/dist/model-gateway.d.ts +86 -0
- package/dist/model-gateway.js +241 -0
- package/dist/native-version.d.ts +9 -0
- package/dist/native-version.js +7 -0
- package/dist/native.d.ts +4 -0
- package/dist/native.js +1 -0
- package/dist/presentation.d.ts +17 -0
- package/dist/presentation.js +39 -0
- package/dist/projection-format.d.ts +68 -0
- package/dist/projection-format.js +118 -0
- package/dist/projection-window.d.ts +9 -0
- package/dist/projection-window.js +93 -0
- package/dist/projection.d.ts +22 -0
- package/dist/projection.js +153 -0
- package/dist/question-response.d.ts +20 -0
- package/dist/question-response.js +75 -0
- package/dist/questions.d.ts +74 -0
- package/dist/questions.js +199 -0
- package/dist/self-evolving.d.ts +55 -0
- package/dist/self-evolving.js +239 -0
- package/dist/skills.d.ts +15 -0
- package/dist/skills.js +51 -0
- package/dist/tool-evidence.d.ts +14 -0
- package/dist/tool-evidence.js +54 -0
- package/dist/validation.d.ts +38 -0
- package/dist/validation.js +68 -0
- package/dist/workspaces.d.ts +107 -0
- package/dist/workspaces.js +212 -0
- package/package.json +145 -150
- package/dist/DebugDrawer-JNA43SVR.js +0 -8
- package/dist/admin-DORVQfLn.d.ts +0 -340
- package/dist/chunk-2BGLZZ7T.js +0 -10
- package/dist/chunk-42D6KAQV.js +0 -2882
- package/dist/chunk-4XOW57KJ.js +0 -343
- package/dist/chunk-72KT6VWV.js +0 -1338
- package/dist/chunk-7XAOVFSN.js +0 -28
- package/dist/chunk-KJIEMSQP.js +0 -198
- package/dist/chunk-KN72XWP7.js +0 -394
- package/dist/chunk-N6THTH7P.js +0 -110
- package/dist/chunk-V3H5A3BR.js +0 -22501
- package/dist/chunk-YGRQ4Q5P.js +0 -20
- package/dist/chunk-YTUZFKB3.js +0 -78
- package/dist/core/index.d.ts +0 -28
- package/dist/core/index.js +0 -617
- package/dist/createHarness-DOLOHA62.js +0 -18
- package/dist/eval/index.d.ts +0 -247
- package/dist/eval/index.js +0 -541
- package/dist/file-search-BUgMSKZE.d.ts +0 -289
- package/dist/front/artifacts.d.ts +0 -34
- package/dist/front/artifacts.js +0 -21
- package/dist/front/index.d.ts +0 -1082
- package/dist/front/index.js +0 -12912
- package/dist/front/styles.css +0 -5380
- package/dist/harness-qHlMJhZo.d.ts +0 -276
- package/dist/middleware-CaAEXpi5.d.ts +0 -115
- package/dist/piChatEvent-DpE0lIb9.d.ts +0 -450
- package/dist/piChatSessionService-CBct-hmz.d.ts +0 -86
- package/dist/sandbox-BMhut3kh.d.ts +0 -317
- package/dist/server/agent-host/testing/compositionRouteProof.d.ts +0 -8
- package/dist/server/agent-host/testing/compositionRouteProof.js +0 -78
- package/dist/server/agent-host/testing/gatewayConformance.d.ts +0 -43
- package/dist/server/agent-host/testing/gatewayConformance.js +0 -605
- package/dist/server/index.d.ts +0 -2278
- package/dist/server/index.js +0 -274
- package/dist/server/pi-session-readability.d.ts +0 -12
- package/dist/server/pi-session-readability.js +0 -23
- package/dist/server/worker/index.d.ts +0 -68
- package/dist/server/worker/index.js +0 -377
- package/dist/share-entry-CxS6UJ_L.d.ts +0 -330
- package/dist/shared/index.d.ts +0 -2111
- package/dist/shared/index.js +0 -262
- package/dist/types-DcWoQMn8.d.ts +0 -1377
- package/docs/ACCESSIBILITY.md +0 -52
- package/docs/AGENT_GATEWAY_V0.md +0 -212
- package/docs/AGENT_PACKAGES.md +0 -90
- package/docs/API.md +0 -123
- package/docs/AUTHORING.md +0 -68
- package/docs/CSP.md +0 -41
- package/docs/ERROR_CODES.md +0 -229
- package/docs/KNOWN_LIMITATIONS.md +0 -100
- package/docs/MIGRATION.md +0 -50
- package/docs/PERFORMANCE.md +0 -68
- package/docs/PLUGINS.md +0 -138
- package/docs/README.md +0 -98
- package/docs/RISKS-MULTI-TAB.md +0 -46
- package/docs/SESSION_IDENTITY.md +0 -91
- package/docs/STYLING.md +0 -73
- package/docs/UI-SHADCN.md +0 -56
- package/docs/VERCEL_COSTS.md +0 -103
- package/docs/plans/archive/AGENT_EVAL_FRAMEWORK.md +0 -466
- package/docs/plans/archive/agent-package-spec.md +0 -1777
- package/docs/plans/archive/harness-followup-capabilities.md +0 -440
- package/docs/plans/archive/harness-tool-ui-capabilities.md +0 -144
- package/docs/plans/archive/pi-followup-history-projection.md +0 -229
- package/docs/plans/archive/pi-tools-migration.md +0 -1061
- package/docs/plans/archive/reviews/pi-followup-history-codex-review.md +0 -11
- package/docs/plans/archive/reviews/pi-followup-history-gpt-review.md +0 -64
- package/docs/plans/archive/reviews/pi-followup-history-opus-review.md +0 -43
- package/docs/plans/archive/reviews/pi-followup-history-xai-review.md +0 -43
- package/docs/plans/archive/vercel-base-snapshot-template-plan.md +0 -527
- package/docs/plans/archive/vercel-persistent-sandbox-adapter.md +0 -553
- package/docs/runtime-provisioning.md +0 -157
- package/docs/runtime.md +0 -296
- package/docs/tools.md +0 -102
package/dist/chunk-42D6KAQV.js
DELETED
|
@@ -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(/"/g, '"').replace(/'/g, "'").replace(/>/g, ">").replace(/</g, "<").replace(/&/g, "&");
|
|
2404
|
-
}
|
|
2405
|
-
function escapeXmlText(value) {
|
|
2406
|
-
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
-
};
|