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.
Files changed (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +48 -4
  3. package/bin/pi-forge.mjs +37 -0
  4. package/dist/client/assets/CodeMirrorEditor-BqaaP1EE.js +34 -0
  5. package/dist/client/assets/CodeMirrorEditor-BqaaP1EE.js.map +1 -0
  6. package/dist/client/assets/index-B-529kgJ.css +32 -0
  7. package/dist/client/assets/index-BzKzxXFs.js +392 -0
  8. package/dist/client/assets/index-BzKzxXFs.js.map +1 -0
  9. package/dist/client/assets/workbox-window.prod.es5-BBnX5xw4.js +3 -0
  10. package/dist/client/assets/workbox-window.prod.es5-BBnX5xw4.js.map +1 -0
  11. package/dist/client/icons/icon-192.png +0 -0
  12. package/dist/client/icons/icon-512.png +0 -0
  13. package/dist/client/icons/icon-maskable-512.png +0 -0
  14. package/dist/client/icons/icon.svg +9 -0
  15. package/dist/client/index.html +24 -0
  16. package/dist/client/manifest.webmanifest +1 -0
  17. package/dist/client/offline.html +142 -0
  18. package/dist/client/sw.js +3 -0
  19. package/dist/client/sw.js.map +1 -0
  20. package/dist/client/workbox-6d7155ed.js +3 -0
  21. package/dist/client/workbox-6d7155ed.js.map +1 -0
  22. package/dist/server/agent-resource-loader.js +126 -0
  23. package/dist/server/agent-resource-loader.js.map +1 -0
  24. package/dist/server/attachment-converters.js +96 -0
  25. package/dist/server/attachment-converters.js.map +1 -0
  26. package/dist/server/auth.js +209 -0
  27. package/dist/server/auth.js.map +1 -0
  28. package/dist/server/compaction-history.js +106 -0
  29. package/dist/server/compaction-history.js.map +1 -0
  30. package/dist/server/concurrency.js +49 -0
  31. package/dist/server/concurrency.js.map +1 -0
  32. package/dist/server/config-export.js +220 -0
  33. package/dist/server/config-export.js.map +1 -0
  34. package/dist/server/config-manager.js +528 -0
  35. package/dist/server/config-manager.js.map +1 -0
  36. package/dist/server/config.js +326 -0
  37. package/dist/server/config.js.map +1 -0
  38. package/dist/server/conversion-worker.mjs +90 -0
  39. package/dist/server/diagnostics.js +137 -0
  40. package/dist/server/diagnostics.js.map +1 -0
  41. package/dist/server/extensions-discovery.js +147 -0
  42. package/dist/server/extensions-discovery.js.map +1 -0
  43. package/dist/server/file-manager.js +734 -0
  44. package/dist/server/file-manager.js.map +1 -0
  45. package/dist/server/file-references.js +215 -0
  46. package/dist/server/file-references.js.map +1 -0
  47. package/dist/server/file-searcher.js +385 -0
  48. package/dist/server/file-searcher.js.map +1 -0
  49. package/dist/server/git-runner.js +684 -0
  50. package/dist/server/git-runner.js.map +1 -0
  51. package/dist/server/index.js +468 -0
  52. package/dist/server/index.js.map +1 -0
  53. package/dist/server/mcp/config.js +133 -0
  54. package/dist/server/mcp/config.js.map +1 -0
  55. package/dist/server/mcp/manager.js +351 -0
  56. package/dist/server/mcp/manager.js.map +1 -0
  57. package/dist/server/mcp/tool-bridge.js +173 -0
  58. package/dist/server/mcp/tool-bridge.js.map +1 -0
  59. package/dist/server/project-manager.js +301 -0
  60. package/dist/server/project-manager.js.map +1 -0
  61. package/dist/server/pty-manager.js +354 -0
  62. package/dist/server/pty-manager.js.map +1 -0
  63. package/dist/server/routes/_schemas.js +73 -0
  64. package/dist/server/routes/_schemas.js.map +1 -0
  65. package/dist/server/routes/auth.js +164 -0
  66. package/dist/server/routes/auth.js.map +1 -0
  67. package/dist/server/routes/config.js +1163 -0
  68. package/dist/server/routes/config.js.map +1 -0
  69. package/dist/server/routes/control.js +464 -0
  70. package/dist/server/routes/control.js.map +1 -0
  71. package/dist/server/routes/exec.js +217 -0
  72. package/dist/server/routes/exec.js.map +1 -0
  73. package/dist/server/routes/files.js +847 -0
  74. package/dist/server/routes/files.js.map +1 -0
  75. package/dist/server/routes/git.js +837 -0
  76. package/dist/server/routes/git.js.map +1 -0
  77. package/dist/server/routes/health.js +97 -0
  78. package/dist/server/routes/health.js.map +1 -0
  79. package/dist/server/routes/mcp.js +300 -0
  80. package/dist/server/routes/mcp.js.map +1 -0
  81. package/dist/server/routes/projects.js +259 -0
  82. package/dist/server/routes/projects.js.map +1 -0
  83. package/dist/server/routes/prompt.js +496 -0
  84. package/dist/server/routes/prompt.js.map +1 -0
  85. package/dist/server/routes/sessions.js +783 -0
  86. package/dist/server/routes/sessions.js.map +1 -0
  87. package/dist/server/routes/stream.js +69 -0
  88. package/dist/server/routes/stream.js.map +1 -0
  89. package/dist/server/routes/terminal.js +335 -0
  90. package/dist/server/routes/terminal.js.map +1 -0
  91. package/dist/server/session-registry.js +1197 -0
  92. package/dist/server/session-registry.js.map +1 -0
  93. package/dist/server/skill-overrides.js +151 -0
  94. package/dist/server/skill-overrides.js.map +1 -0
  95. package/dist/server/skills-export.js +257 -0
  96. package/dist/server/skills-export.js.map +1 -0
  97. package/dist/server/sse-bridge.js +220 -0
  98. package/dist/server/sse-bridge.js.map +1 -0
  99. package/dist/server/tool-overrides.js +277 -0
  100. package/dist/server/tool-overrides.js.map +1 -0
  101. package/dist/server/turn-diff-builder.js +280 -0
  102. package/dist/server/turn-diff-builder.js.map +1 -0
  103. 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"}