pi-forge 0.0.0 → 1.1.4
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 +21 -0
- package/README.md +48 -4
- package/bin/pi-forge.mjs +37 -0
- package/dist/client/assets/CodeMirrorEditor-BqaaP1EE.js +34 -0
- package/dist/client/assets/CodeMirrorEditor-BqaaP1EE.js.map +1 -0
- package/dist/client/assets/index-B-529kgJ.css +32 -0
- package/dist/client/assets/index-BzKzxXFs.js +392 -0
- package/dist/client/assets/index-BzKzxXFs.js.map +1 -0
- package/dist/client/assets/workbox-window.prod.es5-BBnX5xw4.js +3 -0
- package/dist/client/assets/workbox-window.prod.es5-BBnX5xw4.js.map +1 -0
- package/dist/client/icons/icon-192.png +0 -0
- package/dist/client/icons/icon-512.png +0 -0
- package/dist/client/icons/icon-maskable-512.png +0 -0
- package/dist/client/icons/icon.svg +9 -0
- package/dist/client/index.html +24 -0
- package/dist/client/manifest.webmanifest +1 -0
- package/dist/client/offline.html +142 -0
- package/dist/client/sw.js +3 -0
- package/dist/client/sw.js.map +1 -0
- package/dist/client/workbox-6d7155ed.js +3 -0
- package/dist/client/workbox-6d7155ed.js.map +1 -0
- package/dist/server/agent-resource-loader.js +126 -0
- package/dist/server/agent-resource-loader.js.map +1 -0
- package/dist/server/attachment-converters.js +96 -0
- package/dist/server/attachment-converters.js.map +1 -0
- package/dist/server/auth.js +209 -0
- package/dist/server/auth.js.map +1 -0
- package/dist/server/compaction-history.js +106 -0
- package/dist/server/compaction-history.js.map +1 -0
- package/dist/server/concurrency.js +49 -0
- package/dist/server/concurrency.js.map +1 -0
- package/dist/server/config-export.js +220 -0
- package/dist/server/config-export.js.map +1 -0
- package/dist/server/config-manager.js +528 -0
- package/dist/server/config-manager.js.map +1 -0
- package/dist/server/config.js +326 -0
- package/dist/server/config.js.map +1 -0
- package/dist/server/conversion-worker.mjs +90 -0
- package/dist/server/diagnostics.js +137 -0
- package/dist/server/diagnostics.js.map +1 -0
- package/dist/server/extensions-discovery.js +147 -0
- package/dist/server/extensions-discovery.js.map +1 -0
- package/dist/server/file-manager.js +734 -0
- package/dist/server/file-manager.js.map +1 -0
- package/dist/server/file-references.js +215 -0
- package/dist/server/file-references.js.map +1 -0
- package/dist/server/file-searcher.js +385 -0
- package/dist/server/file-searcher.js.map +1 -0
- package/dist/server/git-runner.js +684 -0
- package/dist/server/git-runner.js.map +1 -0
- package/dist/server/index.js +468 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/mcp/config.js +133 -0
- package/dist/server/mcp/config.js.map +1 -0
- package/dist/server/mcp/manager.js +351 -0
- package/dist/server/mcp/manager.js.map +1 -0
- package/dist/server/mcp/tool-bridge.js +173 -0
- package/dist/server/mcp/tool-bridge.js.map +1 -0
- package/dist/server/project-manager.js +301 -0
- package/dist/server/project-manager.js.map +1 -0
- package/dist/server/pty-manager.js +354 -0
- package/dist/server/pty-manager.js.map +1 -0
- package/dist/server/routes/_schemas.js +73 -0
- package/dist/server/routes/_schemas.js.map +1 -0
- package/dist/server/routes/auth.js +164 -0
- package/dist/server/routes/auth.js.map +1 -0
- package/dist/server/routes/config.js +1163 -0
- package/dist/server/routes/config.js.map +1 -0
- package/dist/server/routes/control.js +464 -0
- package/dist/server/routes/control.js.map +1 -0
- package/dist/server/routes/exec.js +217 -0
- package/dist/server/routes/exec.js.map +1 -0
- package/dist/server/routes/files.js +847 -0
- package/dist/server/routes/files.js.map +1 -0
- package/dist/server/routes/git.js +837 -0
- package/dist/server/routes/git.js.map +1 -0
- package/dist/server/routes/health.js +97 -0
- package/dist/server/routes/health.js.map +1 -0
- package/dist/server/routes/mcp.js +300 -0
- package/dist/server/routes/mcp.js.map +1 -0
- package/dist/server/routes/projects.js +259 -0
- package/dist/server/routes/projects.js.map +1 -0
- package/dist/server/routes/prompt.js +496 -0
- package/dist/server/routes/prompt.js.map +1 -0
- package/dist/server/routes/sessions.js +783 -0
- package/dist/server/routes/sessions.js.map +1 -0
- package/dist/server/routes/stream.js +69 -0
- package/dist/server/routes/stream.js.map +1 -0
- package/dist/server/routes/terminal.js +335 -0
- package/dist/server/routes/terminal.js.map +1 -0
- package/dist/server/session-registry.js +1197 -0
- package/dist/server/session-registry.js.map +1 -0
- package/dist/server/skill-overrides.js +151 -0
- package/dist/server/skill-overrides.js.map +1 -0
- package/dist/server/skills-export.js +257 -0
- package/dist/server/skills-export.js.map +1 -0
- package/dist/server/sse-bridge.js +220 -0
- package/dist/server/sse-bridge.js.map +1 -0
- package/dist/server/tool-overrides.js +277 -0
- package/dist/server/tool-overrides.js.map +1 -0
- package/dist/server/turn-diff-builder.js +280 -0
- package/dist/server/turn-diff-builder.js.map +1 -0
- package/package.json +53 -12
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Worker } from "node:worker_threads";
|
|
2
|
+
/**
|
|
3
|
+
* Office-format → text converter dispatcher.
|
|
4
|
+
*
|
|
5
|
+
* Conversion runs in a `worker_threads` worker (see
|
|
6
|
+
* `conversion-worker.mjs`) because pdfjs-dist and ExcelJS do heavy
|
|
7
|
+
* synchronous JS work that blocks Node's event loop for seconds on
|
|
8
|
+
* real-world files. While the loop is blocked, the SSE bridge's
|
|
9
|
+
* heartbeat can't fire and the underlying TCP socket stalls long
|
|
10
|
+
* enough for Node's HTTP machinery (or any L7 proxy) to drop the
|
|
11
|
+
* stream — producing a "Reconnecting…" banner in chat right after the
|
|
12
|
+
* user submits a prompt with an attachment.
|
|
13
|
+
*
|
|
14
|
+
* One-shot worker per conversion call (no pool). Conversion is rare
|
|
15
|
+
* relative to other server activity; pool startup overhead would
|
|
16
|
+
* outweigh the savings until you're processing many files per minute.
|
|
17
|
+
*
|
|
18
|
+
* Errors (corrupt file, encrypted PDF, unparseable office doc, worker
|
|
19
|
+
* crash) come back as `ConversionError` so the route can surface a
|
|
20
|
+
* clean upload-time message instead of a generic 500.
|
|
21
|
+
*/
|
|
22
|
+
export class ConversionError extends Error {
|
|
23
|
+
filename;
|
|
24
|
+
format;
|
|
25
|
+
constructor(filename, format, cause) {
|
|
26
|
+
super(`failed to convert ${format.toUpperCase()} "${filename}": ${describe(cause)}`);
|
|
27
|
+
this.filename = filename;
|
|
28
|
+
this.format = format;
|
|
29
|
+
this.name = "ConversionError";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function describe(err) {
|
|
33
|
+
if (err instanceof Error)
|
|
34
|
+
return err.message;
|
|
35
|
+
return String(err);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Match the upload to a converter. Dispatch is by lowercased
|
|
39
|
+
* extension first, then MIME — extensions are more reliable in
|
|
40
|
+
* practice because browsers send `application/octet-stream` for these
|
|
41
|
+
* formats more often than the canonical MIME types.
|
|
42
|
+
*/
|
|
43
|
+
export function pickConverter(filename, mime) {
|
|
44
|
+
const ext = filename.includes(".") ? filename.split(".").pop()?.toLowerCase() : undefined;
|
|
45
|
+
if (ext === "pdf" || mime === "application/pdf")
|
|
46
|
+
return "pdf";
|
|
47
|
+
if (ext === "docx" ||
|
|
48
|
+
mime === "application/vnd.openxmlformats-officedocument.wordprocessingml.document") {
|
|
49
|
+
return "docx";
|
|
50
|
+
}
|
|
51
|
+
if (ext === "xlsx" ||
|
|
52
|
+
mime === "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") {
|
|
53
|
+
return "xlsx";
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
// Resolve the worker file relative to THIS module. Works in both
|
|
58
|
+
// `tsx` dev (this file is TS in src/) and compiled prod (this file is
|
|
59
|
+
// JS in dist/) because the build step copies the .mjs alongside.
|
|
60
|
+
const WORKER_URL = new URL("./conversion-worker.mjs", import.meta.url);
|
|
61
|
+
let nextRequestId = 0;
|
|
62
|
+
export async function convertAttachment(format, filename, buf) {
|
|
63
|
+
const id = ++nextRequestId;
|
|
64
|
+
// Transfer the buffer's underlying ArrayBuffer to the worker — zero
|
|
65
|
+
// copy. We slice() first because Node Buffers share their pool's
|
|
66
|
+
// ArrayBuffer with other buffers; transferring the whole pool would
|
|
67
|
+
// detach unrelated buffers.
|
|
68
|
+
const ab = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
69
|
+
const worker = new Worker(WORKER_URL);
|
|
70
|
+
return new Promise((resolve, reject) => {
|
|
71
|
+
worker.once("message", (msg) => {
|
|
72
|
+
if (msg.ok && typeof msg.text === "string") {
|
|
73
|
+
resolve(msg.text);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
reject(new ConversionError(filename, format, msg.error ?? "worker returned no text"));
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
worker.once("error", (err) => {
|
|
80
|
+
reject(new ConversionError(filename, format, err));
|
|
81
|
+
});
|
|
82
|
+
worker.once("exit", (code) => {
|
|
83
|
+
if (code !== 0) {
|
|
84
|
+
// `error` will have already rejected; this is a safety net for
|
|
85
|
+
// an exit without a prior error event.
|
|
86
|
+
reject(new ConversionError(filename, format, `worker exited with code ${code}`));
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
worker.postMessage({ id, format, buf: ab }, [ab]);
|
|
90
|
+
}).finally(() => {
|
|
91
|
+
// Worker is one-shot — terminate so the thread exits cleanly even
|
|
92
|
+
// if the postMessage handler didn't trigger an organic exit.
|
|
93
|
+
void worker.terminate();
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=attachment-converters.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"attachment-converters.js","sourceRoot":"","sources":["../src/attachment-converters.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { chmodSync, existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { randomBytes, scrypt as scryptCb, timingSafeEqual } from "node:crypto";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import jwt from "jsonwebtoken";
|
|
6
|
+
import { config } from "./config.js";
|
|
7
|
+
const scrypt = promisify(scryptCb);
|
|
8
|
+
/**
|
|
9
|
+
* scrypt cost params. N=16384 (2^14) targets ~50–100 ms per verify on
|
|
10
|
+
* modern hardware — slow enough to make brute-forcing expensive,
|
|
11
|
+
* fast enough that an interactive login feels instant. r/p left at
|
|
12
|
+
* the recommended defaults. Bump N when verifies start feeling fast.
|
|
13
|
+
*/
|
|
14
|
+
const SCRYPT_N = 16384;
|
|
15
|
+
const SCRYPT_R = 8;
|
|
16
|
+
const SCRYPT_P = 1;
|
|
17
|
+
const SCRYPT_KEYLEN = 64;
|
|
18
|
+
const SCRYPT_SALT_BYTES = 16;
|
|
19
|
+
const HASH_PREFIX = "scrypt";
|
|
20
|
+
/**
|
|
21
|
+
* Constant-time string comparison. Pads the shorter buffer so timingSafeEqual
|
|
22
|
+
* can run on equal-length inputs, then enforces the length check after-the-fact
|
|
23
|
+
* so length isn't leaked through early-return timing.
|
|
24
|
+
*/
|
|
25
|
+
export function constantTimeStringEqual(presented, expected) {
|
|
26
|
+
const a = Buffer.from(presented, "utf8");
|
|
27
|
+
const b = Buffer.from(expected, "utf8");
|
|
28
|
+
const len = Math.max(a.length, b.length);
|
|
29
|
+
const aPadded = Buffer.alloc(len);
|
|
30
|
+
const bPadded = Buffer.alloc(len);
|
|
31
|
+
a.copy(aPadded);
|
|
32
|
+
b.copy(bPadded);
|
|
33
|
+
return timingSafeEqual(aPadded, bPadded) && a.length === b.length;
|
|
34
|
+
}
|
|
35
|
+
export function generateToken(opts) {
|
|
36
|
+
if (config.auth.jwtSecret === undefined) {
|
|
37
|
+
throw new Error("auth: cannot generate token — JWT_SECRET not configured");
|
|
38
|
+
}
|
|
39
|
+
const expiresIn = config.auth.jwtExpiresInSeconds;
|
|
40
|
+
const token = jwt.sign({
|
|
41
|
+
sub: "ui-user",
|
|
42
|
+
mustChangePassword: opts.mustChangePassword,
|
|
43
|
+
}, config.auth.jwtSecret, {
|
|
44
|
+
algorithm: "HS256",
|
|
45
|
+
expiresIn,
|
|
46
|
+
});
|
|
47
|
+
const expiresAt = new Date(Date.now() + expiresIn * 1000).toISOString();
|
|
48
|
+
return { token, expiresAt };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Verify a JWT and return the typed payload, or undefined on any failure
|
|
52
|
+
* (bad signature, expired, missing JWT_SECRET, wrong subject).
|
|
53
|
+
*/
|
|
54
|
+
export function verifyToken(token) {
|
|
55
|
+
if (config.auth.jwtSecret === undefined)
|
|
56
|
+
return undefined;
|
|
57
|
+
try {
|
|
58
|
+
const decoded = jwt.verify(token, config.auth.jwtSecret, {
|
|
59
|
+
algorithms: ["HS256"],
|
|
60
|
+
});
|
|
61
|
+
if (typeof decoded !== "object" || decoded === null)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (decoded.sub !== "ui-user")
|
|
64
|
+
return undefined;
|
|
65
|
+
if (typeof decoded.iat !== "number" || typeof decoded.exp !== "number")
|
|
66
|
+
return undefined;
|
|
67
|
+
// mustChangePassword may be absent on tokens issued before this
|
|
68
|
+
// field existed; treat absence as `false` so existing sessions
|
|
69
|
+
// don't get force-rerouted to the change-password screen.
|
|
70
|
+
const mustChangePassword = typeof decoded.mustChangePassword === "boolean"
|
|
71
|
+
? decoded.mustChangePassword
|
|
72
|
+
: false;
|
|
73
|
+
return { sub: "ui-user", iat: decoded.iat, exp: decoded.exp, mustChangePassword };
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// jsonwebtoken throws on malformed/expired/wrong-secret tokens.
|
|
77
|
+
// Caller treats undefined as "no valid token" without
|
|
78
|
+
// distinguishing why — clients can't act on the distinction
|
|
79
|
+
// (and we don't want to leak which case applies to a brute-forcer).
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function verifyApiKey(presented) {
|
|
84
|
+
const expected = config.auth.apiKey;
|
|
85
|
+
if (expected === undefined)
|
|
86
|
+
return false;
|
|
87
|
+
return constantTimeStringEqual(presented, expected);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Verify a presented password against either the on-disk hash (if
|
|
91
|
+
* present) or the env UI_PASSWORD (fallback). The returned `source`
|
|
92
|
+
* lets the caller decide whether to set `mustChangePassword` on the
|
|
93
|
+
* issued token: `env` means the user is logging in with the
|
|
94
|
+
* deployment-baked credential and (if `requirePasswordChange` is on)
|
|
95
|
+
* must change it before doing anything else.
|
|
96
|
+
*
|
|
97
|
+
* Once a hash exists on disk, the env password is IGNORED — that
|
|
98
|
+
* file is the canonical credential and should survive env-rotation
|
|
99
|
+
* just like jwt-secret does.
|
|
100
|
+
*/
|
|
101
|
+
export async function verifyPasswordWithSource(presented) {
|
|
102
|
+
const stored = readStoredHash();
|
|
103
|
+
if (stored !== undefined) {
|
|
104
|
+
const ok = await verifyAgainstStoredHash(presented, stored);
|
|
105
|
+
return { ok, source: "stored" };
|
|
106
|
+
}
|
|
107
|
+
const envPw = config.auth.uiPassword;
|
|
108
|
+
if (envPw !== undefined) {
|
|
109
|
+
return { ok: constantTimeStringEqual(presented, envPw), source: "env" };
|
|
110
|
+
}
|
|
111
|
+
return { ok: false, source: "none" };
|
|
112
|
+
}
|
|
113
|
+
export function passwordConfigured() {
|
|
114
|
+
return readStoredHash() !== undefined || config.auth.uiPassword !== undefined;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Hash the new password and atomically replace the on-disk file.
|
|
118
|
+
* Mode 0600 — only the pi-forge process owner should be able to
|
|
119
|
+
* read it. Atomic replace via tmp + rename so a crash mid-write
|
|
120
|
+
* doesn't leave a half-written hash that locks the user out.
|
|
121
|
+
*/
|
|
122
|
+
export async function persistPassword(plain) {
|
|
123
|
+
const encoded = await hashPassword(plain);
|
|
124
|
+
const path = config.auth.passwordHashFile;
|
|
125
|
+
const tmp = `${path}.tmp`;
|
|
126
|
+
writeFileSync(tmp, `${encoded}\n`, { mode: 0o600 });
|
|
127
|
+
chmodSync(tmp, 0o600);
|
|
128
|
+
renameSync(tmp, path);
|
|
129
|
+
}
|
|
130
|
+
async function hashPassword(plain) {
|
|
131
|
+
const salt = randomBytes(SCRYPT_SALT_BYTES);
|
|
132
|
+
const hash = await scrypt(plain, salt, SCRYPT_KEYLEN);
|
|
133
|
+
return [
|
|
134
|
+
HASH_PREFIX,
|
|
135
|
+
String(SCRYPT_N),
|
|
136
|
+
String(SCRYPT_R),
|
|
137
|
+
String(SCRYPT_P),
|
|
138
|
+
salt.toString("base64"),
|
|
139
|
+
hash.toString("base64"),
|
|
140
|
+
].join("$");
|
|
141
|
+
}
|
|
142
|
+
function parseHash(encoded) {
|
|
143
|
+
const parts = encoded.split("$");
|
|
144
|
+
if (parts.length !== 6)
|
|
145
|
+
return undefined;
|
|
146
|
+
if (parts[0] !== HASH_PREFIX)
|
|
147
|
+
return undefined;
|
|
148
|
+
const n = Number.parseInt(parts[1] ?? "", 10);
|
|
149
|
+
const r = Number.parseInt(parts[2] ?? "", 10);
|
|
150
|
+
const p = Number.parseInt(parts[3] ?? "", 10);
|
|
151
|
+
if (!Number.isFinite(n) || !Number.isFinite(r) || !Number.isFinite(p))
|
|
152
|
+
return undefined;
|
|
153
|
+
try {
|
|
154
|
+
const salt = Buffer.from(parts[4] ?? "", "base64");
|
|
155
|
+
const hash = Buffer.from(parts[5] ?? "", "base64");
|
|
156
|
+
if (salt.length === 0 || hash.length === 0)
|
|
157
|
+
return undefined;
|
|
158
|
+
return { n, r, p, salt, hash };
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function readStoredHash() {
|
|
165
|
+
const path = config.auth.passwordHashFile;
|
|
166
|
+
if (!existsSync(path))
|
|
167
|
+
return undefined;
|
|
168
|
+
try {
|
|
169
|
+
const v = readFileSync(path, "utf8").trim();
|
|
170
|
+
return v.length > 0 ? v : undefined;
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function scryptWithOptions(password, salt, keylen, options) {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
scryptCb(password, salt, keylen, options, (err, derived) => {
|
|
179
|
+
if (err !== null) {
|
|
180
|
+
reject(err);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
resolve(derived);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
async function verifyAgainstStoredHash(presented, encoded) {
|
|
188
|
+
const parsed = parseHash(encoded);
|
|
189
|
+
if (parsed === undefined)
|
|
190
|
+
return false;
|
|
191
|
+
// Honour the stored params (not our current constants) so older
|
|
192
|
+
// hashes with different cost parameters still verify after a
|
|
193
|
+
// params bump. New hashes always use the current SCRYPT_* values.
|
|
194
|
+
const candidate = await scryptWithOptions(presented, parsed.salt, parsed.hash.length, {
|
|
195
|
+
N: parsed.n,
|
|
196
|
+
r: parsed.r,
|
|
197
|
+
p: parsed.p,
|
|
198
|
+
});
|
|
199
|
+
if (candidate.length !== parsed.hash.length)
|
|
200
|
+
return false;
|
|
201
|
+
return timingSafeEqual(candidate, parsed.hash);
|
|
202
|
+
}
|
|
203
|
+
export function extractBearer(headerValue) {
|
|
204
|
+
if (!headerValue)
|
|
205
|
+
return undefined;
|
|
206
|
+
const m = /^Bearer\s+(\S+)$/i.exec(headerValue);
|
|
207
|
+
return m?.[1];
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
function isCompactionEntry(e) {
|
|
2
|
+
return e.type === "compaction";
|
|
3
|
+
}
|
|
4
|
+
function isMessageEntry(e) {
|
|
5
|
+
return e.type === "message";
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Walk `session.sessionManager.getEntries()` once and produce one
|
|
9
|
+
* CompactionEvent per CompactionEntry. The mapping back to a position
|
|
10
|
+
* in `session.messages` is the tricky part: the post-compaction
|
|
11
|
+
* messages array is rebuilt from the entries that come AT or AFTER
|
|
12
|
+
* the latest compaction's `firstKeptEntryId`, with the compaction
|
|
13
|
+
* summary itself synthesised in. We compute `insertBeforeIndex` by
|
|
14
|
+
* counting how many "kept" message entries appear in `messages`
|
|
15
|
+
* before the position where this compaction's first-kept entry lands.
|
|
16
|
+
*/
|
|
17
|
+
export function buildCompactionHistory(session) {
|
|
18
|
+
const entries = session.sessionManager.getEntries();
|
|
19
|
+
const compactions = [];
|
|
20
|
+
for (const e of entries)
|
|
21
|
+
if (isCompactionEntry(e))
|
|
22
|
+
compactions.push(e);
|
|
23
|
+
if (compactions.length === 0)
|
|
24
|
+
return [];
|
|
25
|
+
// Walk entries in order; for each compaction event, capture the
|
|
26
|
+
// message entries between the previous compaction (or session start)
|
|
27
|
+
// and this one. Those are the "archived" messages — the ones that
|
|
28
|
+
// were summarised and dropped.
|
|
29
|
+
const events = [];
|
|
30
|
+
let archiveBuffer = [];
|
|
31
|
+
for (const entry of entries) {
|
|
32
|
+
if (isMessageEntry(entry)) {
|
|
33
|
+
archiveBuffer.push(entry.message);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (isCompactionEntry(entry)) {
|
|
37
|
+
events.push({
|
|
38
|
+
id: entry.id,
|
|
39
|
+
timestamp: entry.timestamp,
|
|
40
|
+
summary: entry.summary,
|
|
41
|
+
tokensBefore: entry.tokensBefore,
|
|
42
|
+
// Filled in below — depends on the index of `firstKeptEntryId`
|
|
43
|
+
// within the *kept* portion of the messages stream.
|
|
44
|
+
insertBeforeIndex: 0,
|
|
45
|
+
archivedMessages: archiveBuffer,
|
|
46
|
+
});
|
|
47
|
+
// Reset for the next archive window. Anything between this
|
|
48
|
+
// compaction and the next one (or end of session) belongs to
|
|
49
|
+
// the next event's archive — but only if there IS a next
|
|
50
|
+
// compaction. If not, those messages live in `session.messages`
|
|
51
|
+
// and don't need archiving.
|
|
52
|
+
archiveBuffer = [];
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// Compute insertBeforeIndex for each event. Given a compaction whose
|
|
56
|
+
// firstKeptEntryId points at message-entry M, the number of message
|
|
57
|
+
// entries BEFORE M (within the post-this-compaction kept portion of
|
|
58
|
+
// the entries array) tells us where to splice the card.
|
|
59
|
+
//
|
|
60
|
+
// For the LAST compaction, the kept portion lines up with what
|
|
61
|
+
// session.messages actually contains, so we can count message
|
|
62
|
+
// entries from `firstKeptEntryId` forward up to the compaction's
|
|
63
|
+
// own position. Earlier compactions' kept portions were re-archived
|
|
64
|
+
// by later compactions — those events sit at the TOP of the
|
|
65
|
+
// (current) display, so insertBeforeIndex = 0 for all but the last.
|
|
66
|
+
// (Once a compaction's kept window has itself been archived, no
|
|
67
|
+
// post-compaction message in `session.messages` corresponds to it,
|
|
68
|
+
// so the only sensible position is "before everything else".)
|
|
69
|
+
for (let i = 0; i < events.length - 1; i++) {
|
|
70
|
+
const ev = events[i];
|
|
71
|
+
if (ev !== undefined)
|
|
72
|
+
ev.insertBeforeIndex = 0;
|
|
73
|
+
}
|
|
74
|
+
const last = events[events.length - 1];
|
|
75
|
+
if (last !== undefined) {
|
|
76
|
+
const lastCompaction = compactions[compactions.length - 1];
|
|
77
|
+
if (lastCompaction !== undefined) {
|
|
78
|
+
last.insertBeforeIndex = countMessagesBetween(entries, lastCompaction.firstKeptEntryId, lastCompaction.id);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return events;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Count message-typed entries between two entry ids (exclusive of the
|
|
85
|
+
* end id). Returns the position where the compaction card should
|
|
86
|
+
* splice into `session.messages`. When `firstKeptEntryId` doesn't
|
|
87
|
+
* resolve (legacy session, manual JSONL edit), returns 0 — the card
|
|
88
|
+
* lands at the top of the chat, which is the safe fallback.
|
|
89
|
+
*/
|
|
90
|
+
function countMessagesBetween(entries, firstKeptEntryId, endEntryId) {
|
|
91
|
+
const startIdx = entries.findIndex((e) => e.id === firstKeptEntryId);
|
|
92
|
+
if (startIdx === -1)
|
|
93
|
+
return 0;
|
|
94
|
+
let count = 0;
|
|
95
|
+
for (let i = startIdx; i < entries.length; i++) {
|
|
96
|
+
const e = entries[i];
|
|
97
|
+
if (e === undefined)
|
|
98
|
+
break;
|
|
99
|
+
if (e.id === endEntryId)
|
|
100
|
+
break;
|
|
101
|
+
if (isMessageEntry(e))
|
|
102
|
+
count++;
|
|
103
|
+
}
|
|
104
|
+
return count;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=compaction-history.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compaction-history.js","sourceRoot":"","sources":["../src/compaction-history.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single-process concurrency helpers. The pi-forge is single-tenant and
|
|
3
|
+
* single-process by design, so these are intentionally in-memory only —
|
|
4
|
+
* cross-process coordination is out of scope.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Build a serialised-lock primitive. Each call to the returned function
|
|
8
|
+
* waits for the previous to settle (success or failure) before running.
|
|
9
|
+
* Failures don't propagate to subsequent waiters; the caller owns its own
|
|
10
|
+
* error handling.
|
|
11
|
+
*
|
|
12
|
+
* Use when a read-modify-write sequence on a single shared resource must
|
|
13
|
+
* be serialised — e.g. config files mutated by multiple routes.
|
|
14
|
+
*/
|
|
15
|
+
export function makeLock() {
|
|
16
|
+
let chain = Promise.resolve();
|
|
17
|
+
return (fn) => {
|
|
18
|
+
const next = chain.then(fn, fn);
|
|
19
|
+
chain = next.catch(() => undefined);
|
|
20
|
+
return next;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Build an in-flight Promise dedupe primitive keyed by some lookup key.
|
|
25
|
+
* If a call for `key` is already in flight, subsequent callers receive the
|
|
26
|
+
* SAME promise instead of starting a duplicate operation. The entry is
|
|
27
|
+
* removed when the in-flight promise settles.
|
|
28
|
+
*
|
|
29
|
+
* Use when two routes can independently lazy-load the same resource and
|
|
30
|
+
* both ending up with their own copy is harmful — e.g. resumeSession
|
|
31
|
+
* creating two AgentSession instances backing the same JSONL file.
|
|
32
|
+
*/
|
|
33
|
+
export function makeDedupe() {
|
|
34
|
+
const inflight = new Map();
|
|
35
|
+
return (key, fn) => {
|
|
36
|
+
const existing = inflight.get(key);
|
|
37
|
+
if (existing !== undefined)
|
|
38
|
+
return existing;
|
|
39
|
+
const promise = fn().finally(() => {
|
|
40
|
+
// Only remove if WE are still the entry — defensive against
|
|
41
|
+
// re-entrant code paths that might somehow overwrite it.
|
|
42
|
+
if (inflight.get(key) === promise)
|
|
43
|
+
inflight.delete(key);
|
|
44
|
+
});
|
|
45
|
+
inflight.set(key, promise);
|
|
46
|
+
return promise;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=concurrency.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concurrency.js","sourceRoot":"","sources":["../src/concurrency.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;GAQG;AACH,MAAM,UAAU,QAAQ;IACtB,IAAI,KAAK,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAChD,OAAO,CAAI,EAAoB,EAAc,EAAE;QAC7C,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC;QAChC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACpC,OAAO,IAAI,CAAC;IACd,CAAC,CAAC;AACJ,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,UAAU;IACxB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAC1C,OAAO,CAAC,GAAM,EAAE,EAAoB,EAAc,EAAE;QAClD,MAAM,QAAQ,GAAG,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnC,IAAI,QAAQ,KAAK,SAAS;YAAE,OAAO,QAAQ,CAAC;QAC5C,MAAM,OAAO,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YAChC,4DAA4D;YAC5D,yDAAyD;YACzD,IAAI,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,OAAO;gBAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1D,CAAC,CAAC,CAAC;QACH,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC3B,OAAO,OAAO,CAAC;IACjB,CAAC,CAAC;AACJ,CAAC"}
|