pinomad 0.5.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{chunk-f0v3sht0.js → chunk-07769pqx.js} +10 -8
- package/dist/{chunk-ebfaf3k2.js → chunk-0xh6cy9v.js} +1 -1
- package/dist/{chunk-aqj4wg33.js → chunk-12kaxggg.js} +29 -12
- package/dist/{chunk-d4wdt5qs.js → chunk-1nyzr0mg.js} +2282 -1027
- package/dist/chunk-1t58fh0k.js +16 -0
- package/dist/{chunk-4by4x5c4.js → chunk-6ysezfy5.js} +44 -14
- package/dist/{chunk-kmbh44dt.js → chunk-7qb9tq54.js} +42 -6
- package/dist/{chunk-gqr7ahv4.js → chunk-8yme2wep.js} +2 -2
- package/dist/chunk-b1h374jr.js +159 -0
- package/dist/{chunk-hsp12cjt.js → chunk-e4ahjbmz.js} +212 -50
- package/dist/{chunk-9d59jjmt.js → chunk-fbkd73zr.js} +36 -10
- package/dist/chunk-kzbzacz9.js +12 -0
- package/dist/{chunk-zttf1hvh.js → chunk-m4c5gvce.js} +4 -3
- package/dist/{chunk-cjfmfyjb.js → chunk-ngf1zatm.js} +7 -6
- package/dist/{chunk-1d9sppj0.js → chunk-rtwxjb90.js} +1 -1
- package/dist/{chunk-m4q9wafw.js → chunk-sj7rnvh4.js} +118 -6
- package/dist/chunk-sz3rzk55.js +79 -0
- package/dist/{chunk-26d04z02.js → chunk-ywmm63ks.js} +7 -3
- package/dist/chunk-znhn8af7.js +33 -0
- package/dist/pinomad.js +36 -12
- package/package.json +1 -1
- package/web/assets/index-C0rwAo0H.css +1 -0
- package/web/assets/index-DGee2kNU.js +394 -0
- package/web/index.html +2 -2
- package/dist/chunk-5ng86fmy.js +0 -14
- package/dist/chunk-crv3dqzp.js +0 -63
- package/web/assets/index-BkF0-im5.css +0 -1
- package/web/assets/index-D4lgm3Y4.js +0 -383
- package/web/assets/montserrat-latin-ext-wght-normal-BsZE-iaG.woff2 +0 -0
- package/web/assets/montserrat-latin-wght-normal-l_AIctKy.woff2 +0 -0
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
import {
|
|
3
3
|
serveWebClient
|
|
4
4
|
} from "./chunk-r6w5aqkw.js";
|
|
5
|
+
import {
|
|
6
|
+
Published,
|
|
7
|
+
constantValue,
|
|
8
|
+
openHostSettings,
|
|
9
|
+
sameList,
|
|
10
|
+
settingsDefaults
|
|
11
|
+
} from "./chunk-b1h374jr.js";
|
|
5
12
|
import {
|
|
6
13
|
DevicesDoc,
|
|
7
14
|
RevocationsDoc,
|
|
@@ -14,7 +21,7 @@ import {
|
|
|
14
21
|
readServerBinding,
|
|
15
22
|
readServerConfig,
|
|
16
23
|
resolveRelayOrigin
|
|
17
|
-
} from "./chunk-
|
|
24
|
+
} from "./chunk-0xh6cy9v.js";
|
|
18
25
|
import {
|
|
19
26
|
SECURE_PROLOGUE,
|
|
20
27
|
SecureSession,
|
|
@@ -24,30 +31,41 @@ import {
|
|
|
24
31
|
respondIK,
|
|
25
32
|
startRelayLink,
|
|
26
33
|
toBase64Url
|
|
27
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-7qb9tq54.js";
|
|
28
35
|
import {
|
|
29
36
|
packagedVersion,
|
|
30
37
|
webRoot
|
|
31
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-rtwxjb90.js";
|
|
32
39
|
import {
|
|
33
|
-
|
|
40
|
+
ATTACHMENT_TTL_MS,
|
|
34
41
|
INVALID_FRAME_CLOSE_CODE,
|
|
42
|
+
MAX_ATTACHMENTS_PER_MESSAGE,
|
|
43
|
+
MAX_ATTACHMENT_NAME_LENGTH,
|
|
44
|
+
MAX_PENDING_ATTACHMENTS,
|
|
45
|
+
MAX_PENDING_ATTACHMENT_BYTES,
|
|
35
46
|
PROTOCOL,
|
|
36
|
-
THINKING_LEVELS,
|
|
37
47
|
UNAUTHORIZED_CLOSE_CODE,
|
|
38
|
-
|
|
39
|
-
|
|
48
|
+
attachmentBeginProblem,
|
|
49
|
+
attachmentBlock,
|
|
50
|
+
attachmentChunkProblem,
|
|
51
|
+
base64DecodedLength,
|
|
52
|
+
isAttachmentId,
|
|
40
53
|
organize,
|
|
54
|
+
parseAttachmentBlock,
|
|
41
55
|
parseClientFrame
|
|
42
|
-
} from "./chunk-
|
|
56
|
+
} from "./chunk-e4ahjbmz.js";
|
|
57
|
+
import {
|
|
58
|
+
THINKING_LEVELS,
|
|
59
|
+
isThinkingLevel
|
|
60
|
+
} from "./chunk-znhn8af7.js";
|
|
43
61
|
import {
|
|
44
62
|
DEFAULT_DATA_DIR
|
|
45
|
-
} from "./chunk-
|
|
63
|
+
} from "./chunk-ngf1zatm.js";
|
|
46
64
|
import"./chunk-df32avw6.js";
|
|
47
65
|
|
|
48
66
|
// apps/host/src/main.ts
|
|
49
67
|
import { homedir as homedir3 } from "node:os";
|
|
50
|
-
import { join as
|
|
68
|
+
import { join as join12, resolve as resolve7 } from "node:path";
|
|
51
69
|
import { parseArgs } from "node:util";
|
|
52
70
|
import { createModels } from "@earendil-works/pi-ai/models";
|
|
53
71
|
import { fauxAssistantMessage, fauxProvider } from "@earendil-works/pi-ai/providers/faux";
|
|
@@ -181,7 +199,7 @@ function result(questions, resolution) {
|
|
|
181
199
|
var askUserQuestion = defineTool({
|
|
182
200
|
name: "ask_user_question",
|
|
183
201
|
callers: ["model"],
|
|
184
|
-
description: "Ask the user 1–4 multiple-choice questions when their decision is needed to proceed (ambiguous requirements, choosing between approaches, scope). Blocks until answered. The user can always type a custom answer, so never add an 'Other' option. Keep headers
|
|
202
|
+
description: "Ask the user 1–4 multiple-choice questions when their decision is needed to proceed (ambiguous requirements, choosing between approaches, scope). Blocks until answered. The user can always type a custom answer, so never add an 'Other' option. Keep everything short: headers a word or two, option labels a few words. Usually leave option descriptions out; add one only when the label alone is ambiguous, as one line of at most ~60 characters.",
|
|
185
203
|
parameters: Type2.Object({
|
|
186
204
|
questions: Type2.Array(QuestionItemSchema, { minItems: 1, maxItems: 4 })
|
|
187
205
|
}),
|
|
@@ -252,393 +270,194 @@ function createCodemode() {
|
|
|
252
270
|
}
|
|
253
271
|
|
|
254
272
|
// apps/host/src/extensions/context.ts
|
|
255
|
-
import { dirname
|
|
273
|
+
import { dirname as dirname2, sep as sep4 } from "node:path";
|
|
256
274
|
import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
|
|
257
|
-
import { formatSkillsForPrompt, loadProjectContextFiles
|
|
258
|
-
var PREAMBLE = "You are PiNomad, a coding agent running on the user's own machine. You work in the project directory below with the read, write, edit, and bash tools: read files before changing them, prefer edit over rewriting whole files, and run commands to check your work. Keep answers short and concrete. When it is unclear what the user wants, ask instead of guessing.";
|
|
259
|
-
var FORK_SHARED_DIR = "This conversation is a fork: it shares this working directory with the conversation it was forked from, which may be editing the same files. If your task needs isolation, create your own git worktree (for example `git worktree add <dir> -b <branch>`) and use absolute paths inside it for every read, write, edit and bash command.";
|
|
260
|
-
function createContext(options) {
|
|
261
|
-
const { agentsHome } = options;
|
|
262
|
-
const resolveCheckout = (input, context) => options.checkout?.(input, context);
|
|
263
|
-
return {
|
|
264
|
-
extension: defineExtension2({
|
|
265
|
-
name: "context",
|
|
266
|
-
sections: [
|
|
267
|
-
section("preamble", () => PREAMBLE, { tag: false }),
|
|
268
|
-
section("environment", async (input, context) => {
|
|
269
|
-
const cwd = input.agent.cwd;
|
|
270
|
-
if (cwd === undefined)
|
|
271
|
-
return;
|
|
272
|
-
const resolved = await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context);
|
|
273
|
-
const checkout = resolved?.checkout;
|
|
274
|
-
const forkLine = resolved?.fork === true && checkout?.own !== true ? [FORK_SHARED_DIR] : [];
|
|
275
|
-
if (checkout === undefined) {
|
|
276
|
-
return [`Working directory: ${cwd}`, ...forkLine].join(`
|
|
277
|
-
`);
|
|
278
|
-
}
|
|
279
|
-
return [
|
|
280
|
-
`Working directory: ${cwd}`,
|
|
281
|
-
checkout.own ? `This directory is a git worktree PiNomad created for this conversation, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.` : `This directory is a git worktree PiNomad created for the conversation this one belongs to, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.`,
|
|
282
|
-
`The user's project directory is ${checkout.projectPath} and must not be modified.`,
|
|
283
|
-
"This is a fresh checkout: ignored files (dependencies such as node_modules, or .env) are absent — install dependencies per AGENTS.md when needed.",
|
|
284
|
-
...forkLine
|
|
285
|
-
].join(`
|
|
286
|
-
`);
|
|
287
|
-
}),
|
|
288
|
-
section("file_references", () => "When the user writes @./path in their message, it is a project file the user references; the path is relative to your current working directory. Its contents are not expanded for you — read the file yourself if the task needs them.", { tag: false }),
|
|
289
|
-
section("project_context", async (input, context) => {
|
|
290
|
-
const cwd = input.agent.cwd;
|
|
291
|
-
if (cwd === undefined)
|
|
292
|
-
return;
|
|
293
|
-
const checkout = (await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context))?.checkout;
|
|
294
|
-
let files;
|
|
295
|
-
if (checkout === undefined) {
|
|
296
|
-
files = loadProjectContextFiles({ cwd, agentDir: agentsHome });
|
|
297
|
-
} else {
|
|
298
|
-
const seen = new Set;
|
|
299
|
-
files = [
|
|
300
|
-
...loadProjectContextFiles({ cwd: dirname(checkout.repo), agentDir: agentsHome }),
|
|
301
|
-
...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot + sep))
|
|
302
|
-
].filter((file) => {
|
|
303
|
-
if (seen.has(file.path))
|
|
304
|
-
return false;
|
|
305
|
-
seen.add(file.path);
|
|
306
|
-
return true;
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
return files.length === 0 ? undefined : files.map((file) => `## ${file.path}
|
|
275
|
+
import { formatSkillsForPrompt, loadProjectContextFiles } from "@earendil-works/pi-coding-agent";
|
|
310
276
|
|
|
311
|
-
|
|
277
|
+
// apps/host/src/skills.ts
|
|
278
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
279
|
+
import { existsSync } from "node:fs";
|
|
280
|
+
import { realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
281
|
+
import { dirname, join as join4, relative as relative2, resolve as resolve3, sep as sep3 } from "node:path";
|
|
282
|
+
import { promisify as promisify2 } from "node:util";
|
|
283
|
+
import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
|
|
284
|
+
import { loadSkills } from "@earendil-works/pi-coding-agent";
|
|
312
285
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
286
|
+
// apps/host/src/checkout.ts
|
|
287
|
+
import { execFile, spawn } from "node:child_process";
|
|
288
|
+
import { randomBytes } from "node:crypto";
|
|
289
|
+
import { realpath, rm, stat } from "node:fs/promises";
|
|
290
|
+
import { tmpdir } from "node:os";
|
|
291
|
+
import { join, relative, sep } from "node:path";
|
|
292
|
+
import { promisify } from "node:util";
|
|
293
|
+
var run = promisify(execFile);
|
|
294
|
+
var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
295
|
+
function gitCapped(dir, args, maxBytes, env) {
|
|
296
|
+
return new Promise((resolve, reject) => {
|
|
297
|
+
const child = spawn("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
298
|
+
const chunks = [];
|
|
299
|
+
let size = 0;
|
|
300
|
+
let killed = false;
|
|
301
|
+
let stderr = "";
|
|
302
|
+
child.stdout.on("data", (chunk) => {
|
|
303
|
+
if (killed)
|
|
304
|
+
return;
|
|
305
|
+
chunks.push(chunk);
|
|
306
|
+
size += chunk.length;
|
|
307
|
+
if (size > maxBytes) {
|
|
308
|
+
killed = true;
|
|
309
|
+
child.kill();
|
|
310
|
+
child.stdout.destroy();
|
|
311
|
+
child.stderr.destroy();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
child.stderr.on("data", (chunk) => {
|
|
315
|
+
if (stderr.length < 8192)
|
|
316
|
+
stderr += chunk.toString("utf8");
|
|
317
|
+
});
|
|
318
|
+
child.on("error", reject);
|
|
319
|
+
child.on("close", (code) => {
|
|
320
|
+
const stdout = Buffer.concat(chunks).toString("utf8");
|
|
321
|
+
if (killed)
|
|
322
|
+
resolve({ stdout, truncated: true });
|
|
323
|
+
else if (code === 0)
|
|
324
|
+
resolve({ stdout, truncated: false });
|
|
325
|
+
else
|
|
326
|
+
reject(Object.assign(new Error(`Command failed: git ${args.join(" ")}
|
|
327
|
+
${stderr}`), { code, stderr }));
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
var worktreeRoot = (dataDir) => join(dataDir, "worktrees");
|
|
332
|
+
var worktreePath = (dataDir, id) => join(worktreeRoot(dataDir), String(id));
|
|
333
|
+
var worktreeBranch = (id, suffix) => `pinomad/${id}-${suffix}`;
|
|
334
|
+
var branchSuffix = () => randomBytes(2).toString("hex");
|
|
335
|
+
var checkoutAt = (checkouts, cwd) => checkouts?.find((checkout) => cwd === checkout.path || cwd.startsWith(checkout.path + sep));
|
|
336
|
+
async function gitBase(projectPath) {
|
|
337
|
+
const top = await git(projectPath, ["rev-parse", "--show-toplevel"]).then(({ stdout }) => stdout.trim(), () => {
|
|
338
|
+
return;
|
|
339
|
+
});
|
|
340
|
+
if (top === undefined)
|
|
341
|
+
return;
|
|
342
|
+
const repo = await realpath(top).catch(() => top);
|
|
343
|
+
const base = await git(repo, ["rev-parse", "--verify", "HEAD"]).then(({ stdout }) => stdout.trim(), () => {
|
|
344
|
+
return;
|
|
345
|
+
});
|
|
346
|
+
if (base === undefined)
|
|
347
|
+
return;
|
|
348
|
+
return { repo, subdir: relative(repo, projectPath), base };
|
|
349
|
+
}
|
|
350
|
+
async function branchOf(dir) {
|
|
351
|
+
const ref = await git(dir, ["symbolic-ref", "--quiet", "HEAD"]).then(({ stdout }) => stdout.trim(), () => {
|
|
352
|
+
return;
|
|
353
|
+
});
|
|
354
|
+
const prefix = "refs/heads/";
|
|
355
|
+
if (ref === undefined || !ref.startsWith(prefix))
|
|
356
|
+
return;
|
|
357
|
+
return ref.slice(prefix.length) || undefined;
|
|
358
|
+
}
|
|
359
|
+
var repoLocks = new Map;
|
|
360
|
+
function locked(repo, work) {
|
|
361
|
+
const next = (repoLocks.get(repo) ?? Promise.resolve()).then(work, work);
|
|
362
|
+
const settled = next.then(() => {}, () => {});
|
|
363
|
+
repoLocks.set(repo, settled);
|
|
364
|
+
settled.then(() => {
|
|
365
|
+
if (repoLocks.get(repo) === settled)
|
|
366
|
+
repoLocks.delete(repo);
|
|
367
|
+
});
|
|
368
|
+
return next;
|
|
369
|
+
}
|
|
370
|
+
var worktreeExists = async (path) => stat(join(path, ".git")).then(() => true, () => false);
|
|
371
|
+
var worktreeSettled = (record) => locked(record.repo, () => worktreeExists(record.path));
|
|
372
|
+
async function ensureWorktree(record) {
|
|
373
|
+
await locked(record.repo, async () => {
|
|
374
|
+
if (await worktreeExists(record.path))
|
|
375
|
+
return;
|
|
376
|
+
await git(record.repo, ["worktree", "prune"]);
|
|
377
|
+
const branchExists = await git(record.repo, ["show-ref", "--verify", "--quiet", `refs/heads/${record.branch}`]).then(() => true, () => false);
|
|
378
|
+
if (branchExists) {
|
|
379
|
+
await git(record.repo, ["worktree", "add", record.path, record.branch]);
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
await git(record.repo, ["worktree", "add", "-b", record.branch, record.path, record.base]);
|
|
383
|
+
if (record.snapshot !== undefined) {
|
|
384
|
+
await git(record.path, ["read-tree", "-u", "--reset", record.snapshot]);
|
|
385
|
+
await git(record.path, ["reset", "-q"]);
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
async function removeWorktree(record) {
|
|
390
|
+
return await locked(record.repo, async () => {
|
|
391
|
+
if (!await worktreeExists(record.path))
|
|
392
|
+
return;
|
|
393
|
+
return await git(record.repo, ["worktree", "remove", record.path]).then(() => {
|
|
394
|
+
return;
|
|
395
|
+
}, (error) => reasonOf(error));
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
function reasonOf(error) {
|
|
399
|
+
const stderr = error?.stderr;
|
|
400
|
+
const text = typeof stderr === "string" && stderr.trim() !== "" ? stderr.trim() : error instanceof Error ? error.message : String(error);
|
|
401
|
+
return text.split(`
|
|
402
|
+
`).filter((line) => line.trim() !== "").at(-1) ?? text;
|
|
403
|
+
}
|
|
404
|
+
async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
405
|
+
const indexFile = join(tmpdir(), `pinomad-index-${randomBytes(8).toString("hex")}`);
|
|
406
|
+
const env = { GIT_INDEX_FILE: indexFile };
|
|
407
|
+
try {
|
|
408
|
+
await git(dir, ["read-tree", "HEAD"], env);
|
|
409
|
+
await git(dir, ["add", "-A"], env);
|
|
410
|
+
const { stdout, truncated } = await gitCapped(dir, ["diff", "--cached", "--no-color", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", base], maxBytes, env);
|
|
411
|
+
if (!truncated && Buffer.byteLength(stdout) <= maxBytes)
|
|
412
|
+
return { base, patch: stdout, truncated: false };
|
|
413
|
+
const cut = Buffer.from(stdout, "utf8").subarray(0, maxBytes);
|
|
414
|
+
const patch = cut.subarray(0, cut.lastIndexOf(10) + 1).toString("utf8");
|
|
415
|
+
return { base, patch, truncated: true };
|
|
416
|
+
} finally {
|
|
417
|
+
await rm(indexFile, { force: true });
|
|
418
|
+
}
|
|
331
419
|
}
|
|
332
420
|
|
|
333
|
-
// apps/host/src/
|
|
421
|
+
// apps/host/src/files.ts
|
|
422
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
423
|
+
import { opendir, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
424
|
+
import { join as join3, resolve as resolve2, sep as sep2 } from "node:path";
|
|
425
|
+
import { AgentDoc as AgentDoc2 } from "@earendil-works/pi-durable";
|
|
426
|
+
|
|
427
|
+
// apps/host/src/organization.ts
|
|
428
|
+
import { mkdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
429
|
+
import { basename, join as join2, resolve } from "node:path";
|
|
334
430
|
import { clampThinkingLevel } from "@earendil-works/pi-ai";
|
|
335
431
|
import {
|
|
336
432
|
AgentDoc,
|
|
337
|
-
|
|
338
|
-
configure
|
|
339
|
-
defineExtension as defineExtension3,
|
|
340
|
-
defineTool as defineTool2,
|
|
341
|
-
section as section2
|
|
433
|
+
defineDoc as defineDoc3,
|
|
434
|
+
configure
|
|
342
435
|
} from "@earendil-works/pi-durable";
|
|
343
|
-
import { Type as Type3 } from "typebox";
|
|
344
436
|
|
|
345
|
-
// apps/host/src/
|
|
437
|
+
// apps/host/src/message-content.ts
|
|
438
|
+
function messageContent(text, images, attachmentText) {
|
|
439
|
+
if ((images === undefined || images.length === 0) && attachmentText === undefined)
|
|
440
|
+
return text;
|
|
441
|
+
return [
|
|
442
|
+
...text === "" ? [] : [{ type: "text", text }],
|
|
443
|
+
...attachmentText === undefined ? [] : [{ type: "text", text: attachmentText }],
|
|
444
|
+
...(images ?? []).map(({ data, mimeType }) => ({ type: "image", data, mimeType }))
|
|
445
|
+
];
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// apps/host/src/role-doc.ts
|
|
346
449
|
import { defineDoc as defineDoc2 } from "@earendil-works/pi-durable";
|
|
347
|
-
var
|
|
348
|
-
kind: "pinomad.
|
|
450
|
+
var RoleDoc = defineDoc2({
|
|
451
|
+
kind: "pinomad.role",
|
|
349
452
|
version: 1,
|
|
350
453
|
scope: "conversation",
|
|
351
|
-
history: "
|
|
352
|
-
fork: "
|
|
454
|
+
history: "rewindable",
|
|
455
|
+
fork: "asOf",
|
|
353
456
|
initial: () => ({})
|
|
354
457
|
});
|
|
355
458
|
|
|
356
|
-
// apps/host/src/extensions/subagent.ts
|
|
357
|
-
var INSTRUCTIONS = "You are a subagent working on one delegated task. You cannot ask the user questions; if you need a decision or clarification, stop and state exactly what you need in your final answer. Your final message is returned to the delegating agent as the result, so make it a complete, self-contained report.";
|
|
358
|
-
var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
359
|
-
function createSubagent(options) {
|
|
360
|
-
const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
|
|
361
|
-
const roles = options.roles ?? (() => []);
|
|
362
|
-
const roleNamed = (name) => {
|
|
363
|
-
const found = roles().find((each) => each.name === name);
|
|
364
|
-
if (found !== undefined)
|
|
365
|
-
return found;
|
|
366
|
-
const defined = roles();
|
|
367
|
-
throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
|
|
368
|
-
};
|
|
369
|
-
const tool = defineTool2({
|
|
370
|
-
name: "subagent",
|
|
371
|
-
callers: ["model"],
|
|
372
|
-
description: "Delegate a self-contained task to a subagent that runs in its own conversation and returns its final answer. " + "The subagent starts with none of this conversation's context, so put everything it needs in `task`. " + "It works in the same files as you: subagents called in the same turn run concurrently, so give parallel subagents read-only work or non-overlapping files. " + "It cannot ask the user questions. `model` and `thinkingLevel` override what it inherits from you; `role` delegates to a named agent; a model or thinking level it pins cannot be overridden; `description` only labels the call. " + `Available models: ${available()}.`,
|
|
373
|
-
parameters: Type3.Object({
|
|
374
|
-
task: Type3.String({
|
|
375
|
-
minLength: 1,
|
|
376
|
-
description: "Complete, self-contained instructions. The subagent sees none of this conversation."
|
|
377
|
-
}),
|
|
378
|
-
role: Type3.Optional(Type3.String({
|
|
379
|
-
description: "Optional named agent to delegate to, from the Named agents list in your instructions. Its prompt applies, plus any model or thinking level it pins."
|
|
380
|
-
})),
|
|
381
|
-
description: Type3.Optional(Type3.String({ description: 'A short 3-6 word label for the user, e.g. "Audit auth middleware".' })),
|
|
382
|
-
model: Type3.Optional(Type3.String({ description: "Optional `provider/modelId` to use instead of yours." })),
|
|
383
|
-
thinkingLevel: Type3.Optional(Type3.Union(THINKING_LEVELS2.map((level) => Type3.Literal(level)), { description: "Optional thinking level instead of yours." }))
|
|
384
|
-
}),
|
|
385
|
-
replay: "safe",
|
|
386
|
-
execute: async (args, api, context) => {
|
|
387
|
-
const child = await api.commit(async (tx) => {
|
|
388
|
-
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
389
|
-
if (existing !== undefined) {
|
|
390
|
-
const doc2 = await tx.doc(SubagentDoc, existing.id);
|
|
391
|
-
return {
|
|
392
|
-
id: existing.id,
|
|
393
|
-
...doc2?.model === undefined ? {} : { model: doc2.model },
|
|
394
|
-
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
395
|
-
};
|
|
396
|
-
}
|
|
397
|
-
const role = args.role === undefined ? undefined : roleNamed(args.role);
|
|
398
|
-
if (role?.model !== undefined && args.model !== undefined) {
|
|
399
|
-
throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
|
|
400
|
-
}
|
|
401
|
-
if (role?.thinkingLevel !== undefined && args.thinkingLevel !== undefined) {
|
|
402
|
-
throw new Error(`Agent "${role.name}" pins thinking level ${role.thinkingLevel}; omit \`thinkingLevel\``);
|
|
403
|
-
}
|
|
404
|
-
const modelString = role?.model ?? args.model;
|
|
405
|
-
const levelString = role?.thinkingLevel ?? args.thinkingLevel;
|
|
406
|
-
let model;
|
|
407
|
-
let thinkingLevel;
|
|
408
|
-
let override;
|
|
409
|
-
if (modelString !== undefined) {
|
|
410
|
-
const slash = modelString.indexOf("/");
|
|
411
|
-
const provider = slash === -1 ? "" : modelString.slice(0, slash);
|
|
412
|
-
const modelId = slash === -1 ? "" : modelString.slice(slash + 1);
|
|
413
|
-
override = provider === "" || modelId === "" ? undefined : options.models.getModel(provider, modelId);
|
|
414
|
-
const listed = options.modelSummaries().some((each) => each.provider === provider && each.modelId === modelId);
|
|
415
|
-
if (override === undefined || !listed) {
|
|
416
|
-
throw new Error(role === undefined ? `Unknown model "${modelString}". Available models: ${available()}` : `Agent "${role.name}" uses model "${modelString}", which is not available. Available models: ${available()}`);
|
|
417
|
-
}
|
|
418
|
-
model = { provider, modelId };
|
|
419
|
-
}
|
|
420
|
-
if (levelString !== undefined || override !== undefined) {
|
|
421
|
-
const parent = await tx.doc(AgentDoc, api.conversationId);
|
|
422
|
-
const effective = override ?? (parent.model === undefined ? undefined : options.models.getModel(parent.model.provider, parent.model.modelId));
|
|
423
|
-
if (levelString !== undefined) {
|
|
424
|
-
thinkingLevel = effective === undefined ? levelString : clampThinkingLevel(effective, levelString);
|
|
425
|
-
} else if (parent.thinkingLevel !== undefined) {
|
|
426
|
-
thinkingLevel = clampThinkingLevel(override, parent.thinkingLevel);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
const change = {
|
|
430
|
-
extensions: { remove: [extension, ...options.exclude] },
|
|
431
|
-
instructions: role === undefined || role.prompt === "" ? INSTRUCTIONS : `${INSTRUCTIONS}
|
|
432
|
-
|
|
433
|
-
${role.prompt}`,
|
|
434
|
-
...model === undefined ? {} : { model },
|
|
435
|
-
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
436
|
-
};
|
|
437
|
-
const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
438
|
-
await configure(tx, created.id, change);
|
|
439
|
-
const childAgent = await tx.doc(AgentDoc, created.id);
|
|
440
|
-
const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc, api.conversationId)).model;
|
|
441
|
-
const startedAt = Date.now();
|
|
442
|
-
const doc = await tx.doc(SubagentDoc, created.id);
|
|
443
|
-
doc.startedAt = startedAt;
|
|
444
|
-
if (ref !== undefined)
|
|
445
|
-
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
446
|
-
if (role !== undefined)
|
|
447
|
-
doc.role = role.name;
|
|
448
|
-
const label = args.description?.trim();
|
|
449
|
-
if (label !== undefined && label !== "")
|
|
450
|
-
doc.label = label;
|
|
451
|
-
return { id: created.id, ...ref === undefined ? {} : { model: doc.model }, startedAt };
|
|
452
|
-
}, context);
|
|
453
|
-
await api.details({ conversationId: child.id, ...child.model === undefined ? {} : { model: child.model }, ...child.startedAt === undefined ? {} : { startedAt: child.startedAt } }, context);
|
|
454
|
-
const handle = await api.conversation(child.id, context);
|
|
455
|
-
if (handle === undefined)
|
|
456
|
-
throw new Error(`Subagent conversation ${child.id} does not exist`);
|
|
457
|
-
const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
|
|
458
|
-
const settled = await (await handle.submit(request, context)).wait(context);
|
|
459
|
-
if (settled.status !== "done" || settled.type !== "input")
|
|
460
|
-
throw new Error(`Subagent failed: ${settled.status}`);
|
|
461
|
-
const answer = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
|
|
462
|
-
const message = answer?.model?.[0];
|
|
463
|
-
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
464
|
-
const result2 = {
|
|
465
|
-
output: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
|
|
466
|
-
details: {
|
|
467
|
-
conversationId: child.id,
|
|
468
|
-
...child.model === undefined ? {} : { model: child.model },
|
|
469
|
-
...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
|
|
470
|
-
}
|
|
471
|
-
};
|
|
472
|
-
return result2;
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
const extension = defineExtension3({
|
|
476
|
-
name: "subagent",
|
|
477
|
-
tools: [tool],
|
|
478
|
-
sections: [
|
|
479
|
-
section2("agent_roles", () => {
|
|
480
|
-
const defined = roles();
|
|
481
|
-
if (defined.length === 0)
|
|
482
|
-
return;
|
|
483
|
-
return [
|
|
484
|
-
"Named agents: pass one as `role` to `subagent` to delegate to it. When the user writes @name for one of these agents, delegate the relevant work to that agent with `subagent`, putting everything it needs in `task`.",
|
|
485
|
-
...defined.map((each) => `- ${each.name}: ${each.description.replace(/\s+/g, " ")}`)
|
|
486
|
-
].join(`
|
|
487
|
-
`);
|
|
488
|
-
})
|
|
489
|
-
]
|
|
490
|
-
});
|
|
491
|
-
return { extension };
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// apps/host/src/extensions/todo.ts
|
|
495
|
-
import { defineDoc as defineDoc3, defineExtension as defineExtension4, defineTool as defineTool3 } from "@earendil-works/pi-durable";
|
|
496
|
-
import { Type as Type4 } from "typebox";
|
|
497
|
-
var TodoDoc = defineDoc3({
|
|
498
|
-
kind: "todo.list",
|
|
499
|
-
version: 1,
|
|
500
|
-
scope: "conversation",
|
|
501
|
-
history: "rewindable",
|
|
502
|
-
fork: "asOf",
|
|
503
|
-
initial: () => ({ items: [] })
|
|
504
|
-
});
|
|
505
|
-
var todoWrite = defineTool3({
|
|
506
|
-
name: "todo_write",
|
|
507
|
-
description: "Replace the todo list shown to the user with the given items.",
|
|
508
|
-
parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
|
|
509
|
-
replay: "safe",
|
|
510
|
-
execute: async (args, api, context) => {
|
|
511
|
-
await api.commit(async (tx) => {
|
|
512
|
-
const doc = await tx.doc(TodoDoc, api.conversationId);
|
|
513
|
-
doc.items = args.items.map((item) => ({ ...item }));
|
|
514
|
-
}, context);
|
|
515
|
-
return { output: [{ type: "text", text: `Todo list updated: ${args.items.length} item(s).` }] };
|
|
516
|
-
}
|
|
517
|
-
});
|
|
518
|
-
var todo = {
|
|
519
|
-
extension: defineExtension4({ name: "todo", tools: [todoWrite] }),
|
|
520
|
-
docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
|
|
521
|
-
};
|
|
522
|
-
|
|
523
|
-
// apps/host/src/host.ts
|
|
524
|
-
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
525
|
-
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile3 } from "node:fs/promises";
|
|
526
|
-
import { basename as basename2, join as join8, sep as sep5 } from "node:path";
|
|
527
|
-
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
528
|
-
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
529
|
-
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
530
|
-
|
|
531
|
-
// apps/host/src/organization.ts
|
|
532
|
-
import { mkdir, realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
533
|
-
import { basename, join as join3, resolve } from "node:path";
|
|
534
|
-
import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
|
|
535
|
-
import {
|
|
536
|
-
AgentDoc as AgentDoc2,
|
|
537
|
-
defineDoc as defineDoc4
|
|
538
|
-
} from "@earendil-works/pi-durable";
|
|
539
|
-
|
|
540
|
-
// apps/host/src/checkout.ts
|
|
541
|
-
import { execFile } from "node:child_process";
|
|
542
|
-
import { randomBytes } from "node:crypto";
|
|
543
|
-
import { realpath, rm, stat } from "node:fs/promises";
|
|
544
|
-
import { tmpdir } from "node:os";
|
|
545
|
-
import { join as join2, relative, sep as sep2 } from "node:path";
|
|
546
|
-
import { promisify } from "node:util";
|
|
547
|
-
var run = promisify(execFile);
|
|
548
|
-
var git = (dir, args, env) => run("git", ["-C", dir, ...args], env === undefined ? {} : { env: { ...process.env, ...env } });
|
|
549
|
-
var worktreeRoot = (dataDir) => join2(dataDir, "worktrees");
|
|
550
|
-
var worktreePath = (dataDir, id) => join2(worktreeRoot(dataDir), String(id));
|
|
551
|
-
var worktreeBranch = (id, suffix) => `pinomad/${id}-${suffix}`;
|
|
552
|
-
var branchSuffix = () => randomBytes(2).toString("hex");
|
|
553
|
-
var checkoutAt = (checkouts, cwd) => checkouts?.find((checkout) => cwd === checkout.path || cwd.startsWith(checkout.path + sep2));
|
|
554
|
-
async function gitBase(projectPath) {
|
|
555
|
-
const top = await git(projectPath, ["rev-parse", "--show-toplevel"]).then(({ stdout }) => stdout.trim(), () => {
|
|
556
|
-
return;
|
|
557
|
-
});
|
|
558
|
-
if (top === undefined)
|
|
559
|
-
return;
|
|
560
|
-
const repo = await realpath(top).catch(() => top);
|
|
561
|
-
const base = await git(repo, ["rev-parse", "--verify", "HEAD"]).then(({ stdout }) => stdout.trim(), () => {
|
|
562
|
-
return;
|
|
563
|
-
});
|
|
564
|
-
if (base === undefined)
|
|
565
|
-
return;
|
|
566
|
-
return { repo, subdir: relative(repo, projectPath), base };
|
|
567
|
-
}
|
|
568
|
-
var repoLocks = new Map;
|
|
569
|
-
function locked(repo, work) {
|
|
570
|
-
const next = (repoLocks.get(repo) ?? Promise.resolve()).then(work, work);
|
|
571
|
-
const settled = next.then(() => {}, () => {});
|
|
572
|
-
repoLocks.set(repo, settled);
|
|
573
|
-
settled.then(() => {
|
|
574
|
-
if (repoLocks.get(repo) === settled)
|
|
575
|
-
repoLocks.delete(repo);
|
|
576
|
-
});
|
|
577
|
-
return next;
|
|
578
|
-
}
|
|
579
|
-
var worktreeExists = async (path) => stat(join2(path, ".git")).then(() => true, () => false);
|
|
580
|
-
async function ensureWorktree(record) {
|
|
581
|
-
await locked(record.repo, async () => {
|
|
582
|
-
if (await worktreeExists(record.path))
|
|
583
|
-
return;
|
|
584
|
-
await git(record.repo, ["worktree", "prune"]);
|
|
585
|
-
const branchExists = await git(record.repo, ["show-ref", "--verify", "--quiet", `refs/heads/${record.branch}`]).then(() => true, () => false);
|
|
586
|
-
if (branchExists) {
|
|
587
|
-
await git(record.repo, ["worktree", "add", record.path, record.branch]);
|
|
588
|
-
return;
|
|
589
|
-
}
|
|
590
|
-
await git(record.repo, ["worktree", "add", "-b", record.branch, record.path, record.base]);
|
|
591
|
-
if (record.snapshot !== undefined) {
|
|
592
|
-
await git(record.path, ["read-tree", "-u", "--reset", record.snapshot]);
|
|
593
|
-
await git(record.path, ["reset", "-q"]);
|
|
594
|
-
}
|
|
595
|
-
});
|
|
596
|
-
}
|
|
597
|
-
async function removeWorktree(record) {
|
|
598
|
-
return await locked(record.repo, async () => {
|
|
599
|
-
if (!await worktreeExists(record.path))
|
|
600
|
-
return;
|
|
601
|
-
return await git(record.repo, ["worktree", "remove", record.path]).then(() => {
|
|
602
|
-
return;
|
|
603
|
-
}, (error) => reasonOf(error));
|
|
604
|
-
});
|
|
605
|
-
}
|
|
606
|
-
function reasonOf(error) {
|
|
607
|
-
const stderr = error?.stderr;
|
|
608
|
-
const text = typeof stderr === "string" && stderr.trim() !== "" ? stderr.trim() : error instanceof Error ? error.message : String(error);
|
|
609
|
-
return text.split(`
|
|
610
|
-
`).filter((line) => line.trim() !== "").at(-1) ?? text;
|
|
611
|
-
}
|
|
612
|
-
async function changesOf(dir, base, maxBytes = 1024 * 1024) {
|
|
613
|
-
const indexFile = join2(tmpdir(), `pinomad-index-${randomBytes(8).toString("hex")}`);
|
|
614
|
-
const env = { GIT_INDEX_FILE: indexFile };
|
|
615
|
-
try {
|
|
616
|
-
await git(dir, ["read-tree", "HEAD"], env);
|
|
617
|
-
await git(dir, ["add", "-A"], env);
|
|
618
|
-
const { stdout } = await git(dir, ["diff", "--cached", "--no-color", "--find-renames", "--src-prefix=a/", "--dst-prefix=b/", base], env);
|
|
619
|
-
if (Buffer.byteLength(stdout) <= maxBytes)
|
|
620
|
-
return { base, patch: stdout, truncated: false };
|
|
621
|
-
const cut = stdout.slice(0, maxBytes);
|
|
622
|
-
const patch = cut.slice(0, cut.lastIndexOf(`
|
|
623
|
-
`) + 1);
|
|
624
|
-
return { base, patch, truncated: true };
|
|
625
|
-
} finally {
|
|
626
|
-
await rm(indexFile, { force: true });
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// apps/host/src/message-content.ts
|
|
631
|
-
function messageContent(text, images) {
|
|
632
|
-
if (images === undefined || images.length === 0)
|
|
633
|
-
return text;
|
|
634
|
-
return [
|
|
635
|
-
...text === "" ? [] : [{ type: "text", text }],
|
|
636
|
-
...images.map(({ data, mimeType }) => ({ type: "image", data, mimeType }))
|
|
637
|
-
];
|
|
638
|
-
}
|
|
639
|
-
|
|
640
459
|
// apps/host/src/organization.ts
|
|
641
|
-
var IndexDoc =
|
|
460
|
+
var IndexDoc = defineDoc3({
|
|
642
461
|
kind: "pinomad.index",
|
|
643
462
|
version: 1,
|
|
644
463
|
scope: "session",
|
|
@@ -705,7 +524,7 @@ async function createConversation(harness, dataDir, input, defaults, models, mod
|
|
|
705
524
|
if (prior !== undefined) {
|
|
706
525
|
const existing = await harness.conversation(prior.id, context);
|
|
707
526
|
if (existing !== undefined) {
|
|
708
|
-
await existing.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
527
|
+
await existing.submit({ type: "input", content: messageContent(input.text, input.images, input.attachmentText), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
709
528
|
}
|
|
710
529
|
return prior.id;
|
|
711
530
|
}
|
|
@@ -726,7 +545,7 @@ async function createConversation(harness, dataDir, input, defaults, models, mod
|
|
|
726
545
|
throw new Error(`Model ${modelRef.provider}/${modelRef.modelId} is not available${suffix2}`);
|
|
727
546
|
}
|
|
728
547
|
}
|
|
729
|
-
const thinkingLevel = model === undefined ? input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel :
|
|
548
|
+
const thinkingLevel = model === undefined ? input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel : clampThinkingLevel(model, input.thinkingLevel ?? role?.thinkingLevel ?? defaults.thinkingLevel ?? "off");
|
|
730
549
|
const home = input.home;
|
|
731
550
|
const projectPath = home.kind === "project" ? await normalizePath(home.path).catch(() => home.path) : undefined;
|
|
732
551
|
const repo = home.kind === "project" && input.checkout !== "project" && projectPath !== undefined ? await gitBase(projectPath) : undefined;
|
|
@@ -756,6 +575,7 @@ async function createConversation(harness, dataDir, input, defaults, models, mod
|
|
|
756
575
|
requestId: input.requestId,
|
|
757
576
|
...role === undefined ? {} : { role: role.name }
|
|
758
577
|
});
|
|
578
|
+
(await tx.doc(RoleDoc, id)).name = role?.name ?? null;
|
|
759
579
|
if (repo !== undefined && suffix !== undefined) {
|
|
760
580
|
const path = worktreePath(dataDir, id);
|
|
761
581
|
if (doc.checkouts === undefined)
|
|
@@ -768,26 +588,59 @@ async function createConversation(harness, dataDir, input, defaults, models, mod
|
|
|
768
588
|
branch: worktreeBranch(id, suffix),
|
|
769
589
|
base: repo.base
|
|
770
590
|
});
|
|
771
|
-
(await tx.doc(
|
|
591
|
+
(await tx.doc(AgentDoc, id)).cwd = join2(path, repo.subdir);
|
|
772
592
|
} else if (home.kind === "chat") {
|
|
773
|
-
(await tx.doc(
|
|
593
|
+
(await tx.doc(AgentDoc, id)).cwd = join2(dataDir, "chats", String(id));
|
|
774
594
|
}
|
|
775
595
|
}
|
|
776
596
|
}, context);
|
|
777
597
|
if (home.kind === "chat")
|
|
778
|
-
await mkdir(
|
|
779
|
-
await conversation.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
598
|
+
await mkdir(join2(dataDir, "chats", String(conversation.id)), { recursive: true });
|
|
599
|
+
await conversation.submit({ type: "input", content: messageContent(input.text, input.images, input.attachmentText), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
780
600
|
return conversation.id;
|
|
781
601
|
} catch (error) {
|
|
782
602
|
if (!(error instanceof DuplicateRequest))
|
|
783
603
|
throw error;
|
|
784
604
|
const existing = await harness.conversation(error.conversationId, context);
|
|
785
605
|
if (existing !== undefined) {
|
|
786
|
-
await existing.submit({ type: "input", content: messageContent(input.text, input.images), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
606
|
+
await existing.submit({ type: "input", content: messageContent(input.text, input.images, input.attachmentText), whenBusy: "followUp", requestId: input.requestId }, context);
|
|
787
607
|
}
|
|
788
608
|
return error.conversationId;
|
|
789
609
|
}
|
|
790
610
|
}
|
|
611
|
+
async function setAgent(harness, input, models, modelSummaries, agents, context) {
|
|
612
|
+
await harness.commit(async (tx) => {
|
|
613
|
+
const record = await tx.conversation(input.conversationId);
|
|
614
|
+
if (record === undefined)
|
|
615
|
+
throw new Error(`Conversation ${input.conversationId} does not exist`);
|
|
616
|
+
if (record.owner !== undefined)
|
|
617
|
+
throw new Error("Subagent conversations cannot switch agent");
|
|
618
|
+
const role = input.agent === null ? undefined : requireRole(agents, input.agent);
|
|
619
|
+
const state = await tx.doc(AgentDoc, input.conversationId);
|
|
620
|
+
let change;
|
|
621
|
+
if (role === undefined) {
|
|
622
|
+
change = { instructions: null };
|
|
623
|
+
} else {
|
|
624
|
+
const modelRef = roleModelRef(role) ?? state.model;
|
|
625
|
+
const listed = modelSummaries().map((model2) => `${model2.provider}/${model2.modelId}`).join(", ");
|
|
626
|
+
const suffix = listed === "" ? "" : `. Available models: ${listed}`;
|
|
627
|
+
if (modelRef === undefined) {
|
|
628
|
+
throw new Error(`Agent "${role.name}" has no model: the role pins none and the conversation has none${suffix}`);
|
|
629
|
+
}
|
|
630
|
+
const model = models.getModel(modelRef.provider, modelRef.modelId);
|
|
631
|
+
if (model === undefined || !modelSummaries().some((each) => each.provider === modelRef.provider && each.modelId === modelRef.modelId)) {
|
|
632
|
+
throw new Error(`Model ${modelRef.provider}/${modelRef.modelId} is not available${suffix}`);
|
|
633
|
+
}
|
|
634
|
+
change = {
|
|
635
|
+
model: modelRef,
|
|
636
|
+
thinkingLevel: clampThinkingLevel(model, role.thinkingLevel ?? state.thinkingLevel ?? "off"),
|
|
637
|
+
instructions: role.prompt === "" ? null : role.prompt
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
await configure(tx, input.conversationId, change);
|
|
641
|
+
(await tx.doc(RoleDoc, input.conversationId)).name = role?.name ?? null;
|
|
642
|
+
}, context);
|
|
643
|
+
}
|
|
791
644
|
async function archive(harness, id, archived, context) {
|
|
792
645
|
await harness.commit(async (tx) => {
|
|
793
646
|
const doc = await tx.doc(IndexDoc);
|
|
@@ -812,7 +665,7 @@ async function forkFlagOf(harness, id, context) {
|
|
|
812
665
|
return record.parent !== undefined;
|
|
813
666
|
}
|
|
814
667
|
async function checkoutInfo(harness, input, context) {
|
|
815
|
-
const agent = await input.read.snapshot(
|
|
668
|
+
const agent = await input.read.snapshot(AgentDoc, input.conversationId, context);
|
|
816
669
|
const cwd = agent?.cwd;
|
|
817
670
|
if (cwd === undefined)
|
|
818
671
|
return;
|
|
@@ -827,7 +680,7 @@ async function checkoutInfo(harness, input, context) {
|
|
|
827
680
|
checkout: {
|
|
828
681
|
branch: checkout.branch,
|
|
829
682
|
base: checkout.base,
|
|
830
|
-
projectPath:
|
|
683
|
+
projectPath: join2(checkout.repo, checkout.subdir),
|
|
831
684
|
repo: checkout.repo,
|
|
832
685
|
worktreeRoot: checkout.path,
|
|
833
686
|
own: checkout.conversationId === input.conversationId
|
|
@@ -875,48 +728,752 @@ async function cleanupArchivedWorktrees(harness, rootId, context) {
|
|
|
875
728
|
return kept;
|
|
876
729
|
}
|
|
877
730
|
|
|
878
|
-
// apps/host/src/
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
const
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
}
|
|
904
|
-
function expandAll(values, field, error) {
|
|
905
|
-
if (values === undefined)
|
|
906
|
-
return;
|
|
907
|
-
if (!isObject(values)) {
|
|
908
|
-
error(`"${field}" must be an object of strings`);
|
|
909
|
-
return;
|
|
910
|
-
}
|
|
911
|
-
const expanded = {};
|
|
912
|
-
for (const [key, value] of Object.entries(values)) {
|
|
913
|
-
if (typeof value !== "string") {
|
|
914
|
-
error(`"${field}.${key}" must be a string`);
|
|
915
|
-
return;
|
|
731
|
+
// apps/host/src/files.ts
|
|
732
|
+
var CACHE_TTL_MS = 3000;
|
|
733
|
+
var MAX_CACHED_ROOTS = 64;
|
|
734
|
+
var MAX_CONCURRENT_SCANS = 4;
|
|
735
|
+
var SCAN_BUDGET_MS = 5000;
|
|
736
|
+
var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
737
|
+
var MAX_WALK_ENTRIES = 20000;
|
|
738
|
+
var MAX_RESULTS = 50;
|
|
739
|
+
var UNUSABLE_PATH = /[\s`\x00-\x1f\x7f-\x9f]/;
|
|
740
|
+
function createCandidateLister(enumerate, options = {}) {
|
|
741
|
+
const now = options.now ?? Date.now;
|
|
742
|
+
const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
|
|
743
|
+
const maxConcurrent = options.maxConcurrent ?? MAX_CONCURRENT_SCANS;
|
|
744
|
+
const maxCached = options.maxCached ?? MAX_CACHED_ROOTS;
|
|
745
|
+
const cache = new Map;
|
|
746
|
+
const inflight = new Map;
|
|
747
|
+
let running = 0;
|
|
748
|
+
const waiting = [];
|
|
749
|
+
return (root, mode) => {
|
|
750
|
+
const key = `${mode}${root}`;
|
|
751
|
+
const hit = cache.get(key);
|
|
752
|
+
if (hit !== undefined) {
|
|
753
|
+
if (hit.expiresAt > now())
|
|
754
|
+
return Promise.resolve(hit.value);
|
|
755
|
+
cache.delete(key);
|
|
916
756
|
}
|
|
917
|
-
const
|
|
918
|
-
if (
|
|
919
|
-
|
|
757
|
+
const pending = inflight.get(key);
|
|
758
|
+
if (pending !== undefined)
|
|
759
|
+
return pending;
|
|
760
|
+
const scan = (async () => {
|
|
761
|
+
if (running >= maxConcurrent)
|
|
762
|
+
await new Promise((release) => waiting.push(release));
|
|
763
|
+
running += 1;
|
|
764
|
+
try {
|
|
765
|
+
return await enumerate(root, mode);
|
|
766
|
+
} finally {
|
|
767
|
+
running -= 1;
|
|
768
|
+
waiting.shift()?.();
|
|
769
|
+
}
|
|
770
|
+
})();
|
|
771
|
+
inflight.set(key, scan);
|
|
772
|
+
scan.then((value) => {
|
|
773
|
+
const at = now();
|
|
774
|
+
for (const [cached, entry] of cache) {
|
|
775
|
+
if (entry.expiresAt <= at)
|
|
776
|
+
cache.delete(cached);
|
|
777
|
+
}
|
|
778
|
+
while (cache.size >= maxCached)
|
|
779
|
+
cache.delete(cache.keys().next().value);
|
|
780
|
+
cache.set(key, { value, expiresAt: at + ttlMs });
|
|
781
|
+
}, () => {});
|
|
782
|
+
return scan.finally(() => inflight.delete(key));
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
function gitLines(cwd, args, limits) {
|
|
786
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
787
|
+
const child = spawn2("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
788
|
+
const chunks = [];
|
|
789
|
+
let bytes = 0;
|
|
790
|
+
let truncated = false;
|
|
791
|
+
let settled = false;
|
|
792
|
+
const kill = (markTruncated) => {
|
|
793
|
+
truncated ||= markTruncated;
|
|
794
|
+
child.kill("SIGKILL");
|
|
795
|
+
};
|
|
796
|
+
const timer = setTimeout(() => kill(true), Math.max(0, limits.deadline - limits.now()));
|
|
797
|
+
const finish = (error) => {
|
|
798
|
+
if (settled)
|
|
799
|
+
return;
|
|
800
|
+
settled = true;
|
|
801
|
+
clearTimeout(timer);
|
|
802
|
+
if (error !== undefined) {
|
|
803
|
+
rejectPromise(error);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
const output = Buffer.concat(chunks).toString("utf8");
|
|
807
|
+
const parts = output.split("\x00");
|
|
808
|
+
if (truncated)
|
|
809
|
+
parts.pop();
|
|
810
|
+
resolvePromise({ lines: parts.filter((part) => part !== ""), truncated });
|
|
811
|
+
};
|
|
812
|
+
child.stdout.on("data", (chunk) => {
|
|
813
|
+
bytes += chunk.length;
|
|
814
|
+
if (bytes > limits.maxBytes) {
|
|
815
|
+
kill(true);
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
chunks.push(chunk);
|
|
819
|
+
if (limits.now() > limits.deadline)
|
|
820
|
+
kill(true);
|
|
821
|
+
});
|
|
822
|
+
child.on("error", (error) => finish(error));
|
|
823
|
+
child.stderr.resume();
|
|
824
|
+
child.on("close", (code) => {
|
|
825
|
+
if (code === 0 || truncated) {
|
|
826
|
+
finish(undefined);
|
|
827
|
+
} else {
|
|
828
|
+
finish(new Error(`git ${args.join(" ")} exited with code ${code}`));
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
async function inWorkTree(root) {
|
|
834
|
+
return await new Promise((resolvePromise) => {
|
|
835
|
+
const child = spawn2("git", ["-C", root, "rev-parse", "--is-inside-work-tree"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
836
|
+
let out = "";
|
|
837
|
+
child.stdout.on("data", (chunk) => out += chunk.toString("utf8"));
|
|
838
|
+
child.on("error", () => resolvePromise(false));
|
|
839
|
+
child.on("close", (code) => resolvePromise(code === 0 && out.trim() === "true"));
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
async function hasHead(root) {
|
|
843
|
+
return await new Promise((resolvePromise) => {
|
|
844
|
+
const child = spawn2("git", ["-C", root, "rev-parse", "--verify", "-q", "HEAD"], { stdio: ["ignore", "ignore", "ignore"] });
|
|
845
|
+
child.on("error", () => resolvePromise(false));
|
|
846
|
+
child.on("close", (code) => resolvePromise(code === 0));
|
|
847
|
+
});
|
|
848
|
+
}
|
|
849
|
+
var STAGED_RECORD = /^([0-7]{6}) [0-9a-f]{40,64} [0-7]\t/;
|
|
850
|
+
async function gitWorkingFiles(root, limits) {
|
|
851
|
+
const listing = await gitLines(root, ["ls-files", "-z", "-s", "--cached", "--others", "--exclude-standard"], limits);
|
|
852
|
+
const deleted = await gitLines(root, ["ls-files", "-z", "--deleted"], limits);
|
|
853
|
+
const gone = new Set(deleted.lines);
|
|
854
|
+
const files = new Set;
|
|
855
|
+
for (const line of listing.lines) {
|
|
856
|
+
const staged = STAGED_RECORD.exec(line);
|
|
857
|
+
const path = staged === null ? line : line.slice(staged[0].length);
|
|
858
|
+
if (staged !== null && staged[1] === "160000")
|
|
859
|
+
continue;
|
|
860
|
+
if (path === "" || path.endsWith("/") || gone.has(path))
|
|
861
|
+
continue;
|
|
862
|
+
files.add(path);
|
|
863
|
+
}
|
|
864
|
+
return { files: [...files], truncated: listing.truncated || deleted.truncated };
|
|
865
|
+
}
|
|
866
|
+
async function gitHeadFiles(root, limits) {
|
|
867
|
+
const listing = await gitLines(root, ["ls-tree", "-r", "-z", "HEAD"], limits);
|
|
868
|
+
const files = [];
|
|
869
|
+
for (const line of listing.lines) {
|
|
870
|
+
const tab = line.indexOf("\t");
|
|
871
|
+
if (tab === -1)
|
|
872
|
+
continue;
|
|
873
|
+
const kind = line.slice(0, tab).split(" ")[1];
|
|
874
|
+
if (kind === "blob")
|
|
875
|
+
files.push(line.slice(tab + 1));
|
|
876
|
+
}
|
|
877
|
+
return { files, truncated: listing.truncated };
|
|
878
|
+
}
|
|
879
|
+
async function walkFiles(root, limits) {
|
|
880
|
+
const files = [];
|
|
881
|
+
let entries = 0;
|
|
882
|
+
let truncated = false;
|
|
883
|
+
const dirs = [{ dir: root, prefix: "" }];
|
|
884
|
+
while (dirs.length > 0) {
|
|
885
|
+
if (limits.now() > limits.deadline) {
|
|
886
|
+
truncated = true;
|
|
887
|
+
break;
|
|
888
|
+
}
|
|
889
|
+
const { dir, prefix } = dirs.pop();
|
|
890
|
+
let handle;
|
|
891
|
+
try {
|
|
892
|
+
handle = await opendir(dir);
|
|
893
|
+
} catch (error) {
|
|
894
|
+
if (prefix === "")
|
|
895
|
+
throw error;
|
|
896
|
+
truncated = true;
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
for await (const entry of handle) {
|
|
900
|
+
if (entries++ >= limits.maxEntries || limits.now() > limits.deadline) {
|
|
901
|
+
truncated = true;
|
|
902
|
+
break;
|
|
903
|
+
}
|
|
904
|
+
if (entry.isDirectory()) {
|
|
905
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules")
|
|
906
|
+
continue;
|
|
907
|
+
dirs.push({ dir: join3(dir, entry.name), prefix: `${prefix}${entry.name}/` });
|
|
908
|
+
} else if (entry.isFile() || entry.isSymbolicLink()) {
|
|
909
|
+
files.push(`${prefix}${entry.name}`);
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
if (truncated)
|
|
913
|
+
break;
|
|
914
|
+
}
|
|
915
|
+
return { files, truncated };
|
|
916
|
+
}
|
|
917
|
+
async function enumerateRoot(root, mode, limits = {}) {
|
|
918
|
+
const now = limits.now ?? Date.now;
|
|
919
|
+
const bounded = {
|
|
920
|
+
now,
|
|
921
|
+
deadline: now() + (limits.budgetMs ?? SCAN_BUDGET_MS),
|
|
922
|
+
maxBytes: limits.maxBytes ?? MAX_OUTPUT_BYTES,
|
|
923
|
+
maxEntries: limits.maxEntries ?? MAX_WALK_ENTRIES
|
|
924
|
+
};
|
|
925
|
+
if (!await inWorkTree(root)) {
|
|
926
|
+
return walkFiles(root, bounded);
|
|
927
|
+
}
|
|
928
|
+
if (mode === "head" && !await hasHead(root)) {
|
|
929
|
+
return walkFiles(root, bounded);
|
|
930
|
+
}
|
|
931
|
+
return mode === "head" ? gitHeadFiles(root, bounded) : gitWorkingFiles(root, bounded);
|
|
932
|
+
}
|
|
933
|
+
function matchFiles(candidates, query, max = MAX_RESULTS) {
|
|
934
|
+
const usable = candidates.filter((path) => !UNUSABLE_PATH.test(path));
|
|
935
|
+
if (query === "") {
|
|
936
|
+
const sorted = [...usable].sort((a, b) => a < b ? -1 : a > b ? 1 : 0);
|
|
937
|
+
return { files: sorted.slice(0, max), truncated: sorted.length > max };
|
|
938
|
+
}
|
|
939
|
+
const needle = query.toLowerCase();
|
|
940
|
+
const tiers = [[], [], []];
|
|
941
|
+
for (const path of usable) {
|
|
942
|
+
const base = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
|
|
943
|
+
const lower = path.toLowerCase();
|
|
944
|
+
if (base.startsWith(needle))
|
|
945
|
+
tiers[0].push(path);
|
|
946
|
+
else if (base.includes(needle))
|
|
947
|
+
tiers[1].push(path);
|
|
948
|
+
else if (lower.includes(needle))
|
|
949
|
+
tiers[2].push(path);
|
|
950
|
+
}
|
|
951
|
+
const shorter = (a, b) => a.length - b.length || (a < b ? -1 : a > b ? 1 : 0);
|
|
952
|
+
const matched = [...tiers[0].sort(shorter), ...tiers[1].sort(shorter), ...tiers[2].sort(shorter)];
|
|
953
|
+
return { files: matched.slice(0, max), truncated: matched.length > max };
|
|
954
|
+
}
|
|
955
|
+
async function listFiles(harness, dataDir, enumerate, scope, query, context) {
|
|
956
|
+
let root;
|
|
957
|
+
let mode = "live";
|
|
958
|
+
if (scope.kind === "conversation") {
|
|
959
|
+
const agent = await harness.snapshot(AgentDoc2, scope.conversationId, context);
|
|
960
|
+
if (agent === undefined)
|
|
961
|
+
throw new Error(`Conversation ${scope.conversationId} does not exist`);
|
|
962
|
+
const cwd = agent.cwd;
|
|
963
|
+
if (cwd === undefined)
|
|
964
|
+
throw new Error(`Conversation ${scope.conversationId} has no checkout`);
|
|
965
|
+
if (cwd.startsWith(join3(dataDir, "chats") + sep2))
|
|
966
|
+
return { files: [], truncated: false };
|
|
967
|
+
const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
|
|
968
|
+
const ready = record === undefined || await worktreeSettled(record);
|
|
969
|
+
const info = ready ? await stat3(cwd).catch(() => {
|
|
970
|
+
return;
|
|
971
|
+
}) : undefined;
|
|
972
|
+
if (info === undefined || !info.isDirectory()) {
|
|
973
|
+
throw new Error(`Conversation ${scope.conversationId}'s checkout is unavailable: ${cwd}`);
|
|
974
|
+
}
|
|
975
|
+
root = cwd;
|
|
976
|
+
} else {
|
|
977
|
+
const normalized = await realpath3(resolve2(scope.path)).catch(() => resolve2(scope.path));
|
|
978
|
+
const index = await harness.snapshot(IndexDoc, context);
|
|
979
|
+
if (!index?.projects.some((project) => project.path === normalized)) {
|
|
980
|
+
throw new Error(`Unknown project: ${scope.path}`);
|
|
981
|
+
}
|
|
982
|
+
const info = await stat3(normalized).catch(() => {
|
|
983
|
+
return;
|
|
984
|
+
});
|
|
985
|
+
if (info === undefined || !info.isDirectory()) {
|
|
986
|
+
throw new Error(`Not a directory: ${normalized}`);
|
|
987
|
+
}
|
|
988
|
+
root = normalized;
|
|
989
|
+
mode = scope.checkout === "worktree" ? "head" : "live";
|
|
990
|
+
}
|
|
991
|
+
const candidates = await enumerate(root, mode);
|
|
992
|
+
const matched = matchFiles(candidates.files, query);
|
|
993
|
+
return { files: matched.files, truncated: matched.truncated || candidates.truncated };
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// apps/host/src/skills.ts
|
|
997
|
+
var exec = promisify2(execFile2);
|
|
998
|
+
function invocableSkillName(name) {
|
|
999
|
+
return name.length <= 64 && /^[a-z0-9-]+$/.test(name) && !name.startsWith("-") && !name.endsWith("-") && !name.includes("--");
|
|
1000
|
+
}
|
|
1001
|
+
var reportedDiagnostics = new Set;
|
|
1002
|
+
function reportDiagnostics(diagnostics) {
|
|
1003
|
+
for (const diagnostic of diagnostics) {
|
|
1004
|
+
const key = `${diagnostic.path ?? ""}\x00${diagnostic.message}`;
|
|
1005
|
+
if (reportedDiagnostics.has(key))
|
|
1006
|
+
continue;
|
|
1007
|
+
reportedDiagnostics.add(key);
|
|
1008
|
+
console.error(JSON.stringify({
|
|
1009
|
+
event: "skill",
|
|
1010
|
+
level: diagnostic.type === "error" ? "error" : "warning",
|
|
1011
|
+
...diagnostic.path === undefined ? {} : { path: diagnostic.path },
|
|
1012
|
+
message: diagnostic.message
|
|
1013
|
+
}));
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
function entrySkills(dir, source, agentsHome) {
|
|
1017
|
+
if (!existsSync(dir))
|
|
1018
|
+
return [];
|
|
1019
|
+
const { skills, diagnostics } = loadSkills({ cwd: dir, agentDir: agentsHome, includeDefaults: false, skillPaths: [dir] });
|
|
1020
|
+
reportDiagnostics(diagnostics);
|
|
1021
|
+
return skills.map((skill) => ({ skill, source }));
|
|
1022
|
+
}
|
|
1023
|
+
function projectSkillLayers(root) {
|
|
1024
|
+
const start = resolve3(root);
|
|
1025
|
+
const dirs = [start];
|
|
1026
|
+
let dir = start;
|
|
1027
|
+
for (;; ) {
|
|
1028
|
+
if (existsSync(join4(dir, ".git")))
|
|
1029
|
+
return { dirs, gitRoot: dir };
|
|
1030
|
+
const parent = dirname(dir);
|
|
1031
|
+
if (parent === dir)
|
|
1032
|
+
return { dirs: [start] };
|
|
1033
|
+
dir = parent;
|
|
1034
|
+
dirs.push(dir);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
function projectSkills(dirs, agentsHome) {
|
|
1038
|
+
return dirs.flatMap((dir) => entrySkills(join4(dir, ".agents", "skills"), "project", agentsHome));
|
|
1039
|
+
}
|
|
1040
|
+
function mergeSkills(entries) {
|
|
1041
|
+
const seen = new Set;
|
|
1042
|
+
const merged = [];
|
|
1043
|
+
for (const entry of entries) {
|
|
1044
|
+
for (const scoped of entry) {
|
|
1045
|
+
if (seen.has(scoped.skill.name))
|
|
1046
|
+
continue;
|
|
1047
|
+
seen.add(scoped.skill.name);
|
|
1048
|
+
merged.push(scoped);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return merged;
|
|
1052
|
+
}
|
|
1053
|
+
function scopedSkills(root, agentsHome) {
|
|
1054
|
+
const project = root === undefined ? [] : projectSkills(projectSkillLayers(root).dirs, agentsHome);
|
|
1055
|
+
return mergeSkills([project, entrySkills(join4(agentsHome, "skills"), "user", agentsHome)]);
|
|
1056
|
+
}
|
|
1057
|
+
async function committedSkillFiles(gitRoot, layers) {
|
|
1058
|
+
if (!await inWorkTree(gitRoot) || !await hasHead(gitRoot))
|
|
1059
|
+
return null;
|
|
1060
|
+
const pathspecs = layers.map((dir) => {
|
|
1061
|
+
const rel = relative2(gitRoot, dir).split(sep3).join("/");
|
|
1062
|
+
return rel === "" ? ".agents/skills" : `${rel}/.agents/skills`;
|
|
1063
|
+
});
|
|
1064
|
+
const { stdout } = await exec("git", ["-C", gitRoot, "ls-tree", "-r", "-z", "--full-name", "--name-only", "HEAD", "--", ...pathspecs], { maxBuffer: 4 * 1024 * 1024 });
|
|
1065
|
+
return new Set(stdout.split("\x00").filter((path) => path !== ""));
|
|
1066
|
+
}
|
|
1067
|
+
async function enumerateSkills(root, mode, agentsHome) {
|
|
1068
|
+
let project = [];
|
|
1069
|
+
if (root !== undefined) {
|
|
1070
|
+
const layers = projectSkillLayers(root);
|
|
1071
|
+
project = projectSkills(layers.dirs, agentsHome);
|
|
1072
|
+
const gitRoot = layers.gitRoot;
|
|
1073
|
+
if (mode === "head" && gitRoot !== undefined) {
|
|
1074
|
+
const committed = await committedSkillFiles(gitRoot, layers.dirs);
|
|
1075
|
+
if (committed !== null) {
|
|
1076
|
+
project = project.filter((scoped) => committed.has(relative2(gitRoot, scoped.skill.filePath).split(sep3).join("/")));
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const merged = mergeSkills([project, entrySkills(join4(agentsHome, "skills"), "user", agentsHome)]);
|
|
1081
|
+
return merged.filter((scoped) => invocableSkillName(scoped.skill.name)).map(({ skill, source }) => ({
|
|
1082
|
+
name: skill.name,
|
|
1083
|
+
description: skill.description,
|
|
1084
|
+
source,
|
|
1085
|
+
filePath: skill.filePath,
|
|
1086
|
+
baseDir: skill.baseDir
|
|
1087
|
+
}));
|
|
1088
|
+
}
|
|
1089
|
+
var CACHE_TTL_MS2 = 3000;
|
|
1090
|
+
var MAX_CACHED_SCOPES = 64;
|
|
1091
|
+
function createSkillLister(enumerate, options = {}) {
|
|
1092
|
+
const now = options.now ?? Date.now;
|
|
1093
|
+
const ttlMs = options.ttlMs ?? CACHE_TTL_MS2;
|
|
1094
|
+
const maxCached = options.maxCached ?? MAX_CACHED_SCOPES;
|
|
1095
|
+
const cache = new Map;
|
|
1096
|
+
const inflight = new Map;
|
|
1097
|
+
return (root, mode) => {
|
|
1098
|
+
const key = `${mode}${root ?? ""}`;
|
|
1099
|
+
const hit = cache.get(key);
|
|
1100
|
+
if (hit !== undefined) {
|
|
1101
|
+
if (hit.expiresAt > now())
|
|
1102
|
+
return Promise.resolve(hit.value);
|
|
1103
|
+
cache.delete(key);
|
|
1104
|
+
}
|
|
1105
|
+
const pending = inflight.get(key);
|
|
1106
|
+
if (pending !== undefined)
|
|
1107
|
+
return pending;
|
|
1108
|
+
const scan = enumerate(root, mode);
|
|
1109
|
+
inflight.set(key, scan);
|
|
1110
|
+
scan.then((value) => {
|
|
1111
|
+
const at = now();
|
|
1112
|
+
for (const [cached, entry] of cache) {
|
|
1113
|
+
if (entry.expiresAt <= at)
|
|
1114
|
+
cache.delete(cached);
|
|
1115
|
+
}
|
|
1116
|
+
while (cache.size >= maxCached)
|
|
1117
|
+
cache.delete(cache.keys().next().value);
|
|
1118
|
+
cache.set(key, { value, expiresAt: at + ttlMs });
|
|
1119
|
+
}, () => {});
|
|
1120
|
+
return scan.finally(() => inflight.delete(key));
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
async function scopeSkills(harness, lister, scope, context) {
|
|
1124
|
+
if (scope.kind === "chat")
|
|
1125
|
+
return lister(undefined, "live");
|
|
1126
|
+
let root;
|
|
1127
|
+
let mode = "live";
|
|
1128
|
+
if (scope.kind === "conversation") {
|
|
1129
|
+
const agent = await harness.snapshot(AgentDoc3, scope.conversationId, context);
|
|
1130
|
+
if (agent === undefined)
|
|
1131
|
+
throw new Error(`Conversation ${scope.conversationId} does not exist`);
|
|
1132
|
+
const cwd = agent.cwd;
|
|
1133
|
+
if (cwd === undefined)
|
|
1134
|
+
throw new Error(`Conversation ${scope.conversationId} has no checkout`);
|
|
1135
|
+
const record = checkoutAt((await harness.snapshot(IndexDoc, context))?.checkouts, cwd);
|
|
1136
|
+
const ready = record === undefined || await worktreeSettled(record);
|
|
1137
|
+
const info = ready ? await stat4(cwd).catch(() => {
|
|
1138
|
+
return;
|
|
1139
|
+
}) : undefined;
|
|
1140
|
+
if (info === undefined || !info.isDirectory()) {
|
|
1141
|
+
throw new Error(`Conversation ${scope.conversationId}'s checkout is unavailable: ${cwd}`);
|
|
1142
|
+
}
|
|
1143
|
+
root = cwd;
|
|
1144
|
+
} else {
|
|
1145
|
+
const normalized = await realpath4(resolve3(scope.path)).catch(() => resolve3(scope.path));
|
|
1146
|
+
const index = await harness.snapshot(IndexDoc, context);
|
|
1147
|
+
if (!index?.projects.some((project) => project.path === normalized)) {
|
|
1148
|
+
throw new Error(`Unknown project: ${scope.path}`);
|
|
1149
|
+
}
|
|
1150
|
+
const info = await stat4(normalized).catch(() => {
|
|
1151
|
+
return;
|
|
1152
|
+
});
|
|
1153
|
+
if (info === undefined || !info.isDirectory()) {
|
|
1154
|
+
throw new Error(`Not a directory: ${normalized}`);
|
|
1155
|
+
}
|
|
1156
|
+
root = normalized;
|
|
1157
|
+
mode = scope.checkout === "worktree" ? "head" : "live";
|
|
1158
|
+
}
|
|
1159
|
+
return lister(root, mode);
|
|
1160
|
+
}
|
|
1161
|
+
async function listSkills(harness, lister, scope, context) {
|
|
1162
|
+
const skills = await scopeSkills(harness, lister, scope, context);
|
|
1163
|
+
return { skills: skills.map(({ name, description, source }) => ({ name, description, source })) };
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// apps/host/src/extensions/context.ts
|
|
1167
|
+
var PREAMBLE = "You are PiNomad, a coding agent running on the user's own machine. You work in the project directory below with the read, write, edit, and bash tools: read files before changing them, prefer edit over rewriting whole files, and run commands to check your work. Keep answers short and concrete. When it is unclear what the user wants, ask instead of guessing.";
|
|
1168
|
+
var FORK_SHARED_DIR = "This conversation is a fork: it shares this working directory with the conversation it was forked from, which may be editing the same files. If your task needs isolation, create your own git worktree (for example `git worktree add <dir> -b <branch>`) and use absolute paths inside it for every read, write, edit and bash command.";
|
|
1169
|
+
function createContext(options) {
|
|
1170
|
+
const { agentsHome } = options;
|
|
1171
|
+
const resolveCheckout = (input, context) => options.checkout?.(input, context);
|
|
1172
|
+
return {
|
|
1173
|
+
extension: defineExtension2({
|
|
1174
|
+
name: "context",
|
|
1175
|
+
sections: [
|
|
1176
|
+
section("preamble", () => PREAMBLE, { tag: false }),
|
|
1177
|
+
section("environment", async (input, context) => {
|
|
1178
|
+
const cwd = input.agent.cwd;
|
|
1179
|
+
if (cwd === undefined)
|
|
1180
|
+
return;
|
|
1181
|
+
const resolved = await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context);
|
|
1182
|
+
const checkout = resolved?.checkout;
|
|
1183
|
+
const forkLine = resolved?.fork === true && checkout?.own !== true ? [FORK_SHARED_DIR] : [];
|
|
1184
|
+
if (checkout === undefined) {
|
|
1185
|
+
return [`Working directory: ${cwd}`, ...forkLine].join(`
|
|
1186
|
+
`);
|
|
1187
|
+
}
|
|
1188
|
+
return [
|
|
1189
|
+
`Working directory: ${cwd}`,
|
|
1190
|
+
checkout.own ? `This directory is a git worktree PiNomad created for this conversation, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.` : `This directory is a git worktree PiNomad created for the conversation this one belongs to, on branch ${checkout.branch} starting from commit ${checkout.base.slice(0, 12)}.`,
|
|
1191
|
+
`The user's project directory is ${checkout.projectPath} and must not be modified.`,
|
|
1192
|
+
"This is a fresh checkout: ignored files (dependencies such as node_modules, or .env) are absent — install dependencies per AGENTS.md when needed.",
|
|
1193
|
+
...forkLine
|
|
1194
|
+
].join(`
|
|
1195
|
+
`);
|
|
1196
|
+
}),
|
|
1197
|
+
section("file_references", () => "When the user writes @./path in their message, it is a project file the user references; the path is relative to your current working directory. Its contents are not expanded for you — read the file yourself if the task needs them.", { tag: false }),
|
|
1198
|
+
section("project_context", async (input, context) => {
|
|
1199
|
+
const cwd = input.agent.cwd;
|
|
1200
|
+
if (cwd === undefined)
|
|
1201
|
+
return;
|
|
1202
|
+
const checkout = (await resolveCheckout({ conversationId: input.conversationId, read: input.read }, context))?.checkout;
|
|
1203
|
+
let files;
|
|
1204
|
+
if (checkout === undefined) {
|
|
1205
|
+
files = loadProjectContextFiles({ cwd, agentDir: agentsHome });
|
|
1206
|
+
} else {
|
|
1207
|
+
const seen = new Set;
|
|
1208
|
+
files = [
|
|
1209
|
+
...loadProjectContextFiles({ cwd: dirname2(checkout.repo), agentDir: agentsHome }),
|
|
1210
|
+
...loadProjectContextFiles({ cwd, agentDir: agentsHome }).filter((file) => file.path.startsWith(checkout.worktreeRoot + sep4))
|
|
1211
|
+
].filter((file) => {
|
|
1212
|
+
if (seen.has(file.path))
|
|
1213
|
+
return false;
|
|
1214
|
+
seen.add(file.path);
|
|
1215
|
+
return true;
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
return files.length === 0 ? undefined : files.map((file) => `## ${file.path}
|
|
1219
|
+
|
|
1220
|
+
${file.content}`).join(`
|
|
1221
|
+
|
|
1222
|
+
`);
|
|
1223
|
+
}),
|
|
1224
|
+
section("skills", (input) => {
|
|
1225
|
+
const cwd = input.agent.cwd;
|
|
1226
|
+
if (cwd === undefined)
|
|
1227
|
+
return;
|
|
1228
|
+
const skills = scopedSkills(cwd, agentsHome).map((scoped) => scoped.skill);
|
|
1229
|
+
const text = formatSkillsForPrompt(skills, "read");
|
|
1230
|
+
return text === "" ? undefined : text;
|
|
1231
|
+
}, { tag: false })
|
|
1232
|
+
]
|
|
1233
|
+
})
|
|
1234
|
+
};
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// apps/host/src/extensions/subagent.ts
|
|
1238
|
+
import { clampThinkingLevel as clampThinkingLevel2 } from "@earendil-works/pi-ai";
|
|
1239
|
+
import {
|
|
1240
|
+
AgentDoc as AgentDoc4,
|
|
1241
|
+
AssistantEntry,
|
|
1242
|
+
configure as configure2,
|
|
1243
|
+
defineExtension as defineExtension3,
|
|
1244
|
+
defineTool as defineTool2,
|
|
1245
|
+
section as section2
|
|
1246
|
+
} from "@earendil-works/pi-durable";
|
|
1247
|
+
import { Type as Type3 } from "typebox";
|
|
1248
|
+
|
|
1249
|
+
// apps/host/src/subagent-doc.ts
|
|
1250
|
+
import { defineDoc as defineDoc4 } from "@earendil-works/pi-durable";
|
|
1251
|
+
var SubagentDoc = defineDoc4({
|
|
1252
|
+
kind: "pinomad.subagent",
|
|
1253
|
+
version: 1,
|
|
1254
|
+
scope: "conversation",
|
|
1255
|
+
history: "latest",
|
|
1256
|
+
fork: "initial",
|
|
1257
|
+
initial: () => ({})
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
// apps/host/src/extensions/subagent.ts
|
|
1261
|
+
var INSTRUCTIONS = "You are a subagent working on one delegated task. You cannot ask the user questions; if you need a decision or clarification, stop and state exactly what you need in your final answer. Your final message is returned to the delegating agent as the result, so make it a complete, self-contained report.";
|
|
1262
|
+
var THINKING_LEVELS2 = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
1263
|
+
function createSubagent(options) {
|
|
1264
|
+
const available = () => options.modelSummaries().map((model) => `${model.provider}/${model.modelId}`).join(", ");
|
|
1265
|
+
const roles = options.roles ?? (() => []);
|
|
1266
|
+
const roleNamed = (name) => {
|
|
1267
|
+
const found = roles().find((each) => each.name === name);
|
|
1268
|
+
if (found !== undefined)
|
|
1269
|
+
return found;
|
|
1270
|
+
const defined = roles();
|
|
1271
|
+
throw new Error(defined.length === 0 ? "No named agents are defined" : `Unknown agent "${name}". Available agents: ${defined.map((each) => each.name).join(", ")}`);
|
|
1272
|
+
};
|
|
1273
|
+
const tool = defineTool2({
|
|
1274
|
+
name: "subagent",
|
|
1275
|
+
callers: ["model"],
|
|
1276
|
+
description: "Delegate a self-contained task to a subagent that runs in its own conversation and returns its final answer. " + "The subagent starts with none of this conversation's context, so put everything it needs in `task`. " + "It works in the same files as you: subagents called in the same turn run concurrently, so give parallel subagents read-only work or non-overlapping files. " + "It cannot ask the user questions. `model` and `thinkingLevel` override what it inherits from you; `role` delegates to a named agent; a model or thinking level it pins cannot be overridden; `description` only labels the call. " + `Available models: ${available()}.`,
|
|
1277
|
+
parameters: Type3.Object({
|
|
1278
|
+
task: Type3.String({
|
|
1279
|
+
minLength: 1,
|
|
1280
|
+
description: "Complete, self-contained instructions. The subagent sees none of this conversation."
|
|
1281
|
+
}),
|
|
1282
|
+
role: Type3.Optional(Type3.String({
|
|
1283
|
+
description: "Optional named agent to delegate to, from the Named agents list in your instructions. Its prompt applies, plus any model or thinking level it pins."
|
|
1284
|
+
})),
|
|
1285
|
+
description: Type3.Optional(Type3.String({ description: 'A short 3-6 word label for the user, e.g. "Audit auth middleware".' })),
|
|
1286
|
+
model: Type3.Optional(Type3.String({ description: "Optional `provider/modelId` to use instead of yours." })),
|
|
1287
|
+
thinkingLevel: Type3.Optional(Type3.Union(THINKING_LEVELS2.map((level) => Type3.Literal(level)), { description: "Optional thinking level instead of yours." }))
|
|
1288
|
+
}),
|
|
1289
|
+
replay: "safe",
|
|
1290
|
+
execute: async (args, api, context) => {
|
|
1291
|
+
const child = await api.commit(async (tx) => {
|
|
1292
|
+
const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
|
|
1293
|
+
if (existing !== undefined) {
|
|
1294
|
+
const doc2 = await tx.doc(SubagentDoc, existing.id);
|
|
1295
|
+
return {
|
|
1296
|
+
id: existing.id,
|
|
1297
|
+
...doc2?.model === undefined ? {} : { model: doc2.model },
|
|
1298
|
+
...doc2?.startedAt === undefined ? {} : { startedAt: doc2.startedAt }
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
const role = args.role === undefined ? undefined : roleNamed(args.role);
|
|
1302
|
+
if (role?.model !== undefined && args.model !== undefined) {
|
|
1303
|
+
throw new Error(`Agent "${role.name}" pins model ${role.model}; omit \`model\``);
|
|
1304
|
+
}
|
|
1305
|
+
if (role?.thinkingLevel !== undefined && args.thinkingLevel !== undefined) {
|
|
1306
|
+
throw new Error(`Agent "${role.name}" pins thinking level ${role.thinkingLevel}; omit \`thinkingLevel\``);
|
|
1307
|
+
}
|
|
1308
|
+
const modelString = role?.model ?? args.model;
|
|
1309
|
+
const levelString = role?.thinkingLevel ?? args.thinkingLevel;
|
|
1310
|
+
let model;
|
|
1311
|
+
let thinkingLevel;
|
|
1312
|
+
let override;
|
|
1313
|
+
if (modelString !== undefined) {
|
|
1314
|
+
const slash = modelString.indexOf("/");
|
|
1315
|
+
const provider = slash === -1 ? "" : modelString.slice(0, slash);
|
|
1316
|
+
const modelId = slash === -1 ? "" : modelString.slice(slash + 1);
|
|
1317
|
+
override = provider === "" || modelId === "" ? undefined : options.models.getModel(provider, modelId);
|
|
1318
|
+
const listed = options.modelSummaries().some((each) => each.provider === provider && each.modelId === modelId);
|
|
1319
|
+
if (override === undefined || !listed) {
|
|
1320
|
+
throw new Error(role === undefined ? `Unknown model "${modelString}". Available models: ${available()}` : `Agent "${role.name}" uses model "${modelString}", which is not available. Available models: ${available()}`);
|
|
1321
|
+
}
|
|
1322
|
+
model = { provider, modelId };
|
|
1323
|
+
}
|
|
1324
|
+
if (levelString !== undefined || override !== undefined) {
|
|
1325
|
+
const parent = await tx.doc(AgentDoc4, api.conversationId);
|
|
1326
|
+
const effective = override ?? (parent.model === undefined ? undefined : options.models.getModel(parent.model.provider, parent.model.modelId));
|
|
1327
|
+
if (levelString !== undefined) {
|
|
1328
|
+
thinkingLevel = effective === undefined ? levelString : clampThinkingLevel2(effective, levelString);
|
|
1329
|
+
} else if (parent.thinkingLevel !== undefined) {
|
|
1330
|
+
thinkingLevel = clampThinkingLevel2(override, parent.thinkingLevel);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
const change = {
|
|
1334
|
+
extensions: { remove: [extension, ...options.exclude] },
|
|
1335
|
+
instructions: role === undefined || role.prompt === "" ? INSTRUCTIONS : `${INSTRUCTIONS}
|
|
1336
|
+
|
|
1337
|
+
${role.prompt}`,
|
|
1338
|
+
...model === undefined ? {} : { model },
|
|
1339
|
+
...thinkingLevel === undefined ? {} : { thinkingLevel }
|
|
1340
|
+
};
|
|
1341
|
+
const created = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
|
|
1342
|
+
await configure2(tx, created.id, change);
|
|
1343
|
+
const childAgent = await tx.doc(AgentDoc4, created.id);
|
|
1344
|
+
const ref = childAgent.model ?? model ?? (await tx.doc(AgentDoc4, api.conversationId)).model;
|
|
1345
|
+
const startedAt = Date.now();
|
|
1346
|
+
const doc = await tx.doc(SubagentDoc, created.id);
|
|
1347
|
+
doc.startedAt = startedAt;
|
|
1348
|
+
if (ref !== undefined)
|
|
1349
|
+
doc.model = `${ref.provider}/${ref.modelId}`;
|
|
1350
|
+
if (role !== undefined)
|
|
1351
|
+
doc.role = role.name;
|
|
1352
|
+
const label = args.description?.trim();
|
|
1353
|
+
if (label !== undefined && label !== "")
|
|
1354
|
+
doc.label = label;
|
|
1355
|
+
return { id: created.id, ...ref === undefined ? {} : { model: doc.model }, startedAt };
|
|
1356
|
+
}, context);
|
|
1357
|
+
await api.details({ conversationId: child.id, ...child.model === undefined ? {} : { model: child.model }, ...child.startedAt === undefined ? {} : { startedAt: child.startedAt } }, context);
|
|
1358
|
+
const handle = await api.conversation(child.id, context);
|
|
1359
|
+
if (handle === undefined)
|
|
1360
|
+
throw new Error(`Subagent conversation ${child.id} does not exist`);
|
|
1361
|
+
const request = { type: "input", content: args.task, requestId: `subagent:${api.taskId}` };
|
|
1362
|
+
const settled = await (await handle.submit(request, context)).wait(context);
|
|
1363
|
+
if (settled.status !== "done" || settled.type !== "input")
|
|
1364
|
+
throw new Error(`Subagent failed: ${settled.status}`);
|
|
1365
|
+
const answer = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
|
|
1366
|
+
const message = answer?.model?.[0];
|
|
1367
|
+
const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
|
|
1368
|
+
const result2 = {
|
|
1369
|
+
output: [{ type: "text", text: text === "" ? "(The subagent returned no text.)" : text }],
|
|
1370
|
+
details: {
|
|
1371
|
+
conversationId: child.id,
|
|
1372
|
+
...child.model === undefined ? {} : { model: child.model },
|
|
1373
|
+
...child.startedAt === undefined ? {} : { startedAt: child.startedAt }
|
|
1374
|
+
}
|
|
1375
|
+
};
|
|
1376
|
+
return result2;
|
|
1377
|
+
}
|
|
1378
|
+
});
|
|
1379
|
+
const extension = defineExtension3({
|
|
1380
|
+
name: "subagent",
|
|
1381
|
+
tools: [tool],
|
|
1382
|
+
sections: [
|
|
1383
|
+
section2("agent_roles", () => {
|
|
1384
|
+
const defined = roles();
|
|
1385
|
+
if (defined.length === 0)
|
|
1386
|
+
return;
|
|
1387
|
+
return [
|
|
1388
|
+
"Named agents: pass one as `role` to `subagent` to delegate to it. When the user writes @name for one of these agents, delegate the relevant work to that agent with `subagent`, putting everything it needs in `task`.",
|
|
1389
|
+
...defined.map((each) => `- ${each.name}: ${each.description.replace(/\s+/g, " ")}`)
|
|
1390
|
+
].join(`
|
|
1391
|
+
`);
|
|
1392
|
+
})
|
|
1393
|
+
]
|
|
1394
|
+
});
|
|
1395
|
+
return { extension };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// apps/host/src/extensions/todo.ts
|
|
1399
|
+
import { defineDoc as defineDoc5, defineExtension as defineExtension4, defineTool as defineTool3 } from "@earendil-works/pi-durable";
|
|
1400
|
+
import { Type as Type4 } from "typebox";
|
|
1401
|
+
var TodoDoc = defineDoc5({
|
|
1402
|
+
kind: "todo.list",
|
|
1403
|
+
version: 1,
|
|
1404
|
+
scope: "conversation",
|
|
1405
|
+
history: "rewindable",
|
|
1406
|
+
fork: "asOf",
|
|
1407
|
+
initial: () => ({ items: [] })
|
|
1408
|
+
});
|
|
1409
|
+
var todoWrite = defineTool3({
|
|
1410
|
+
name: "todo_write",
|
|
1411
|
+
description: "Replace the todo list shown to the user with the given items.",
|
|
1412
|
+
parameters: Type4.Object({ items: Type4.Array(TodoItemSchema) }),
|
|
1413
|
+
replay: "safe",
|
|
1414
|
+
execute: async (args, api, context) => {
|
|
1415
|
+
await api.commit(async (tx) => {
|
|
1416
|
+
const doc = await tx.doc(TodoDoc, api.conversationId);
|
|
1417
|
+
doc.items = args.items.map((item) => ({ ...item }));
|
|
1418
|
+
}, context);
|
|
1419
|
+
return { output: [{ type: "text", text: `Todo list updated: ${args.items.length} item(s).` }] };
|
|
1420
|
+
}
|
|
1421
|
+
});
|
|
1422
|
+
var todo = {
|
|
1423
|
+
extension: defineExtension4({ name: "todo", tools: [todoWrite] }),
|
|
1424
|
+
docs: [{ token: TodoDoc, presentation: "pinomad.todo" }]
|
|
1425
|
+
};
|
|
1426
|
+
|
|
1427
|
+
// apps/host/src/host.ts
|
|
1428
|
+
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
1429
|
+
import { mkdir as mkdir4, readFile as readFile5, writeFile as writeFile3 } from "node:fs/promises";
|
|
1430
|
+
import { basename as basename3, join as join10, sep as sep6 } from "node:path";
|
|
1431
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
|
|
1432
|
+
import { createRegistry, Harness } from "@earendil-works/pi-durable";
|
|
1433
|
+
import { NodeExecutionEnv } from "@earendil-works/pi-durable/env/node";
|
|
1434
|
+
|
|
1435
|
+
// apps/host/src/mcp.ts
|
|
1436
|
+
import { readFile } from "node:fs/promises";
|
|
1437
|
+
import { homedir } from "node:os";
|
|
1438
|
+
import { isAbsolute, resolve as resolve4 } from "node:path";
|
|
1439
|
+
import {
|
|
1440
|
+
McpAuthRequiredError,
|
|
1441
|
+
McpClient,
|
|
1442
|
+
McpHttpError,
|
|
1443
|
+
StdioTransport,
|
|
1444
|
+
StreamableHttpTransport,
|
|
1445
|
+
toLlmContent
|
|
1446
|
+
} from "@earendil-works/pi-mcp";
|
|
1447
|
+
import { defineExtension as defineExtension5, defineTool as defineTool4 } from "@earendil-works/pi-durable";
|
|
1448
|
+
import { Type as Type5 } from "typebox";
|
|
1449
|
+
var ENV_VAR = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
1450
|
+
var isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1451
|
+
function expand(value) {
|
|
1452
|
+
let missing;
|
|
1453
|
+
const expanded = value.replace(ENV_VAR, (_all, name) => {
|
|
1454
|
+
const found = process.env[name];
|
|
1455
|
+
if (found === undefined)
|
|
1456
|
+
missing = name;
|
|
1457
|
+
return found ?? "";
|
|
1458
|
+
});
|
|
1459
|
+
return missing === undefined ? expanded : { missing };
|
|
1460
|
+
}
|
|
1461
|
+
function expandAll(values, field, error) {
|
|
1462
|
+
if (values === undefined)
|
|
1463
|
+
return;
|
|
1464
|
+
if (!isObject(values)) {
|
|
1465
|
+
error(`"${field}" must be an object of strings`);
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
const expanded = {};
|
|
1469
|
+
for (const [key, value] of Object.entries(values)) {
|
|
1470
|
+
if (typeof value !== "string") {
|
|
1471
|
+
error(`"${field}.${key}" must be a string`);
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
const result2 = expand(value);
|
|
1475
|
+
if (typeof result2 !== "string") {
|
|
1476
|
+
error(`"${field}.${key}" references unset environment variable ${result2.missing}`);
|
|
920
1477
|
return;
|
|
921
1478
|
}
|
|
922
1479
|
expanded[key] = result2;
|
|
@@ -953,7 +1510,7 @@ function parseServer(name, raw, error) {
|
|
|
953
1510
|
const env = expandAll(raw.env, "env", (message) => error(`${name}: ${message}`));
|
|
954
1511
|
if (raw.env !== undefined && env === undefined)
|
|
955
1512
|
return;
|
|
956
|
-
const cwd = raw.cwd === undefined ? homedir() : isAbsolute(raw.cwd) ? raw.cwd :
|
|
1513
|
+
const cwd = raw.cwd === undefined ? homedir() : isAbsolute(raw.cwd) ? raw.cwd : resolve4(homedir(), raw.cwd);
|
|
957
1514
|
return {
|
|
958
1515
|
name,
|
|
959
1516
|
spec: { kind: "stdio", command: raw.command, args: raw.args, env, cwd },
|
|
@@ -1164,9 +1721,9 @@ ${stderr}`;
|
|
|
1164
1721
|
|
|
1165
1722
|
// apps/host/src/agent-roles.ts
|
|
1166
1723
|
import { randomBytes as randomBytes2 } from "node:crypto";
|
|
1167
|
-
import { existsSync, watch } from "node:fs";
|
|
1724
|
+
import { existsSync as existsSync2, watch } from "node:fs";
|
|
1168
1725
|
import { mkdir as mkdir2, readdir, readFile as readFile2, rename, rm as rm2, writeFile } from "node:fs/promises";
|
|
1169
|
-
import { join as
|
|
1726
|
+
import { join as join6 } from "node:path";
|
|
1170
1727
|
import { parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
1171
1728
|
|
|
1172
1729
|
// packages/protocol/src/agents.ts
|
|
@@ -1196,39 +1753,6 @@ function agentRoleProblem(role) {
|
|
|
1196
1753
|
return;
|
|
1197
1754
|
}
|
|
1198
1755
|
|
|
1199
|
-
// apps/host/src/host-value.ts
|
|
1200
|
-
function constantValue(value) {
|
|
1201
|
-
return {
|
|
1202
|
-
value,
|
|
1203
|
-
subscribe: () => () => {}
|
|
1204
|
-
};
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
class Published {
|
|
1208
|
-
#listeners = new Set;
|
|
1209
|
-
#equal;
|
|
1210
|
-
#value;
|
|
1211
|
-
constructor(value, equal) {
|
|
1212
|
-
this.#value = value;
|
|
1213
|
-
this.#equal = equal;
|
|
1214
|
-
}
|
|
1215
|
-
get value() {
|
|
1216
|
-
return this.#value;
|
|
1217
|
-
}
|
|
1218
|
-
subscribe(listener) {
|
|
1219
|
-
this.#listeners.add(listener);
|
|
1220
|
-
return () => this.#listeners.delete(listener);
|
|
1221
|
-
}
|
|
1222
|
-
set(next) {
|
|
1223
|
-
if (this.#equal(this.#value, next))
|
|
1224
|
-
return;
|
|
1225
|
-
this.#value = next;
|
|
1226
|
-
for (const listener of this.#listeners)
|
|
1227
|
-
listener(next);
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
var sameList = (a, b, same) => a.length === b.length && a.every((entry, index) => same(entry, b[index]));
|
|
1231
|
-
|
|
1232
1756
|
// apps/host/src/agent-roles.ts
|
|
1233
1757
|
var KNOWN_FIELDS = new Set(["description", "model", "thinkingLevel"]);
|
|
1234
1758
|
async function load(dir) {
|
|
@@ -1243,7 +1767,7 @@ async function load(dir) {
|
|
|
1243
1767
|
const problem = (reason2) => void problems.push({ file, reason: reason2 });
|
|
1244
1768
|
let parsed;
|
|
1245
1769
|
try {
|
|
1246
|
-
parsed = parseFrontmatter(await readFile2(
|
|
1770
|
+
parsed = parseFrontmatter(await readFile2(join6(dir, file), "utf8"));
|
|
1247
1771
|
} catch (error) {
|
|
1248
1772
|
problem(`Couldn't parse the file: ${error instanceof Error ? error.message : String(error)}`);
|
|
1249
1773
|
continue;
|
|
@@ -1301,11 +1825,14 @@ async function openAgentRoles(options) {
|
|
|
1301
1825
|
const dir = options.dir;
|
|
1302
1826
|
await mkdir2(dir, { recursive: true });
|
|
1303
1827
|
const debounceMs = options.debounceMs ?? 150;
|
|
1828
|
+
const rescanMs = options.rescanMs ?? 1000;
|
|
1304
1829
|
const published = new Published({ roles: [], problems: [] }, (a, b) => sameList(a.roles, b.roles, sameRole) && sameList(a.problems, b.problems, sameProblem));
|
|
1305
1830
|
let closed = false;
|
|
1306
1831
|
let reported = false;
|
|
1307
1832
|
let timer;
|
|
1308
|
-
|
|
1833
|
+
let queued = false;
|
|
1834
|
+
let chain = Promise.resolve();
|
|
1835
|
+
const loadOnce = async () => {
|
|
1309
1836
|
try {
|
|
1310
1837
|
published.set(await load(dir));
|
|
1311
1838
|
reported = false;
|
|
@@ -1315,7 +1842,18 @@ async function openAgentRoles(options) {
|
|
|
1315
1842
|
reported = true;
|
|
1316
1843
|
}
|
|
1317
1844
|
};
|
|
1318
|
-
|
|
1845
|
+
const reload = () => {
|
|
1846
|
+
if (closed)
|
|
1847
|
+
return Promise.resolve();
|
|
1848
|
+
queued = true;
|
|
1849
|
+
chain = chain.then(async () => {
|
|
1850
|
+
if (!queued || closed)
|
|
1851
|
+
return;
|
|
1852
|
+
queued = false;
|
|
1853
|
+
await loadOnce();
|
|
1854
|
+
});
|
|
1855
|
+
return chain;
|
|
1856
|
+
};
|
|
1319
1857
|
const watcher = watch(dir, () => {
|
|
1320
1858
|
if (closed)
|
|
1321
1859
|
return;
|
|
@@ -1326,7 +1864,10 @@ async function openAgentRoles(options) {
|
|
|
1326
1864
|
if (!reported)
|
|
1327
1865
|
console.error(`agent-roles watch failed: ${error.message}`);
|
|
1328
1866
|
reported = true;
|
|
1867
|
+
reload();
|
|
1329
1868
|
});
|
|
1869
|
+
await reload();
|
|
1870
|
+
const rescan = setInterval(() => void reload(), rescanMs);
|
|
1330
1871
|
let pending = Promise.resolve();
|
|
1331
1872
|
const enqueue = (operation) => {
|
|
1332
1873
|
const run2 = pending.then(operation, operation);
|
|
@@ -1338,13 +1879,13 @@ async function openAgentRoles(options) {
|
|
|
1338
1879
|
if (problem !== undefined)
|
|
1339
1880
|
throw new Error(problem);
|
|
1340
1881
|
await enqueue(async () => {
|
|
1341
|
-
const target =
|
|
1342
|
-
const exists =
|
|
1882
|
+
const target = join6(dir, `${role.name}.md`);
|
|
1883
|
+
const exists = existsSync2(target);
|
|
1343
1884
|
if (mustExist && !exists)
|
|
1344
1885
|
throw new Error(`No such agent: "${role.name}"`);
|
|
1345
1886
|
if (!mustExist && exists)
|
|
1346
1887
|
throw new Error(`Agent "${role.name}" already exists`);
|
|
1347
|
-
const temp =
|
|
1888
|
+
const temp = join6(dir, `.${role.name}.${randomBytes2(6).toString("hex")}.tmp`);
|
|
1348
1889
|
try {
|
|
1349
1890
|
await writeFile(temp, serialize(role));
|
|
1350
1891
|
await rename(temp, target);
|
|
@@ -1365,485 +1906,618 @@ async function openAgentRoles(options) {
|
|
|
1365
1906
|
async remove(name) {
|
|
1366
1907
|
const problem = agentNameProblem(name);
|
|
1367
1908
|
if (problem !== undefined)
|
|
1368
|
-
throw new Error(problem);
|
|
1369
|
-
await enqueue(async () => {
|
|
1370
|
-
const target =
|
|
1371
|
-
if (!
|
|
1372
|
-
throw new Error(`No such agent: "${name}"`);
|
|
1373
|
-
await rm2(target);
|
|
1374
|
-
await reload();
|
|
1375
|
-
});
|
|
1376
|
-
},
|
|
1377
|
-
close() {
|
|
1378
|
-
closed = true;
|
|
1379
|
-
clearTimeout(timer);
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
// apps/host/src/host-settings.ts
|
|
1386
|
-
import { mkdir as mkdir3, readFile as readFile3, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1387
|
-
import { dirname as dirname2 } from "node:path";
|
|
1388
|
-
function settingsDefaults(settings) {
|
|
1389
|
-
return {
|
|
1390
|
-
...settings.defaultModel === null ? {} : { model: settings.defaultModel },
|
|
1391
|
-
...settings.defaultThinkingLevel === null ? {} : { thinkingLevel: settings.defaultThinkingLevel }
|
|
1392
|
-
};
|
|
1393
|
-
}
|
|
1394
|
-
var isModelRef = (value) => typeof value === "object" && value !== null && !Array.isArray(value) && typeof value.provider === "string" && value.provider !== "" && typeof value.modelId === "string" && value.modelId !== "";
|
|
1395
|
-
var fileValueError = (key, value, models) => {
|
|
1396
|
-
switch (key) {
|
|
1397
|
-
case "defaultModel":
|
|
1398
|
-
if (!isModelRef(value))
|
|
1399
|
-
return "malformed";
|
|
1400
|
-
return models.some((each) => each.provider === value.provider && each.modelId === value.modelId) ? undefined : "unavailable";
|
|
1401
|
-
case "defaultThinkingLevel":
|
|
1402
|
-
return isThinkingLevel(value) ? undefined : "malformed";
|
|
1403
|
-
case "steeringMode":
|
|
1404
|
-
case "followUpMode":
|
|
1405
|
-
return isQueueMode(value) ? undefined : "malformed";
|
|
1406
|
-
case "acceptAccountDevices":
|
|
1407
|
-
return typeof value === "boolean" ? undefined : "malformed";
|
|
1408
|
-
}
|
|
1409
|
-
};
|
|
1410
|
-
var sameModel = (a, b) => a === b || a !== null && b !== null && a.provider === b.provider && a.modelId === b.modelId;
|
|
1411
|
-
var sameSettings = (a, b) => a.steeringMode === b.steeringMode && a.followUpMode === b.followUpMode && a.acceptAccountDevices === b.acceptAccountDevices && a.defaultThinkingLevel === b.defaultThinkingLevel && sameModel(a.defaultModel, b.defaultModel) && sameList(a.custom, b.custom, (x, y) => x === y);
|
|
1412
|
-
var parse = (path, text) => {
|
|
1413
|
-
let parsed;
|
|
1414
|
-
try {
|
|
1415
|
-
parsed = JSON.parse(text);
|
|
1416
|
-
} catch (error) {
|
|
1417
|
-
throw new Error(`Invalid settings file ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1418
|
-
}
|
|
1419
|
-
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1420
|
-
throw new Error(`Invalid settings file ${path}: expected a JSON object`);
|
|
1421
|
-
}
|
|
1422
|
-
return parsed;
|
|
1423
|
-
};
|
|
1424
|
-
async function openHostSettings(options) {
|
|
1425
|
-
let file = await readFile3(options.path, "utf8").then((text) => parse(options.path, text), (error) => {
|
|
1426
|
-
if (error.code === "ENOENT")
|
|
1427
|
-
return {};
|
|
1428
|
-
throw new Error(`Cannot read settings file ${options.path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1429
|
-
});
|
|
1430
|
-
const warned = new Set;
|
|
1431
|
-
const compute = () => {
|
|
1432
|
-
const fileValues = {};
|
|
1433
|
-
const custom = [];
|
|
1434
|
-
for (const key of HOST_SETTING_KEYS) {
|
|
1435
|
-
if (!Object.hasOwn(file, key))
|
|
1436
|
-
continue;
|
|
1437
|
-
const value = file[key];
|
|
1438
|
-
const invalid = fileValueError(key, value, options.models.value);
|
|
1439
|
-
if (invalid === "malformed" && !warned.has(key)) {
|
|
1440
|
-
warned.add(key);
|
|
1441
|
-
console.error(`${options.path}: ignoring invalid value for settings key "${key}": ${JSON.stringify(value)}`);
|
|
1442
|
-
}
|
|
1443
|
-
if (invalid !== undefined)
|
|
1444
|
-
continue;
|
|
1445
|
-
fileValues[key] = value;
|
|
1446
|
-
custom.push(key);
|
|
1447
|
-
}
|
|
1448
|
-
const { fallback } = options;
|
|
1449
|
-
return {
|
|
1450
|
-
defaultModel: fileValues.defaultModel ?? fallback.defaultModel(),
|
|
1451
|
-
defaultThinkingLevel: fileValues.defaultThinkingLevel ?? fallback.defaultThinkingLevel(),
|
|
1452
|
-
steeringMode: fileValues.steeringMode ?? fallback.steeringMode(),
|
|
1453
|
-
followUpMode: fileValues.followUpMode ?? fallback.followUpMode(),
|
|
1454
|
-
acceptAccountDevices: fileValues.acceptAccountDevices ?? true,
|
|
1455
|
-
custom
|
|
1456
|
-
};
|
|
1457
|
-
};
|
|
1458
|
-
const published = new Published(compute(), sameSettings);
|
|
1459
|
-
options.models.subscribe(() => published.set(compute()));
|
|
1460
|
-
let tail = Promise.resolve();
|
|
1461
|
-
const write = async (patch) => {
|
|
1462
|
-
const model = patch.defaultModel;
|
|
1463
|
-
if (model !== undefined && model !== null && !options.models.value.some((each) => each.provider === model.provider && each.modelId === model.modelId)) {
|
|
1464
|
-
throw new Error(`Unknown model: ${model.provider}/${model.modelId}`);
|
|
1465
|
-
}
|
|
1466
|
-
const next = { ...file };
|
|
1467
|
-
for (const key of HOST_SETTING_KEYS) {
|
|
1468
|
-
const value = patch[key];
|
|
1469
|
-
if (value === undefined)
|
|
1470
|
-
continue;
|
|
1471
|
-
if (value === null)
|
|
1472
|
-
delete next[key];
|
|
1473
|
-
else
|
|
1474
|
-
next[key] = value;
|
|
1475
|
-
}
|
|
1476
|
-
const tmp = `${options.path}.tmp`;
|
|
1477
|
-
await mkdir3(dirname2(options.path), { recursive: true });
|
|
1478
|
-
await writeFile2(tmp, `${JSON.stringify(next, null, 2)}
|
|
1479
|
-
`, { mode: 384 });
|
|
1480
|
-
await rename2(tmp, options.path);
|
|
1481
|
-
file = next;
|
|
1482
|
-
published.set(compute());
|
|
1483
|
-
};
|
|
1484
|
-
return {
|
|
1485
|
-
get value() {
|
|
1486
|
-
return published.value;
|
|
1487
|
-
},
|
|
1488
|
-
subscribe: (listener) => published.subscribe(listener),
|
|
1489
|
-
update: (patch) => tail = tail.then(() => write(patch), () => write(patch))
|
|
1490
|
-
};
|
|
1491
|
-
}
|
|
1492
|
-
|
|
1493
|
-
// apps/host/src/host.ts
|
|
1494
|
-
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
1495
|
-
import lockfile from "proper-lockfile";
|
|
1496
|
-
|
|
1497
|
-
// apps/host/src/gateway.ts
|
|
1498
|
-
import { createServer } from "node:http";
|
|
1499
|
-
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1500
|
-
import { join as join7, sep as sep4 } from "node:path";
|
|
1501
|
-
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1502
|
-
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1503
|
-
import {
|
|
1504
|
-
AgentDoc as AgentDoc4,
|
|
1505
|
-
LiveDoc
|
|
1506
|
-
} from "@earendil-works/pi-durable";
|
|
1507
|
-
import { WebSocket, WebSocketServer } from "ws";
|
|
1508
|
-
import { Value as Value2 } from "typebox/value";
|
|
1509
|
-
|
|
1510
|
-
// apps/host/src/folders.ts
|
|
1511
|
-
import { readdir as readdir2, realpath as realpath3, stat as stat3 } from "node:fs/promises";
|
|
1512
|
-
import { isAbsolute as isAbsolute2, join as join5 } from "node:path";
|
|
1513
|
-
var denied = (error) => {
|
|
1514
|
-
const code = error?.code;
|
|
1515
|
-
return code === "EACCES" || code === "EPERM";
|
|
1516
|
-
};
|
|
1517
|
-
async function listFolders(path, home) {
|
|
1518
|
-
if (path !== undefined && !isAbsolute2(path))
|
|
1519
|
-
throw new Error(`Not an absolute path: ${path}`);
|
|
1520
|
-
const given = path ?? home;
|
|
1521
|
-
const dir = await realpath3(given).catch((error) => {
|
|
1522
|
-
if (denied(error))
|
|
1523
|
-
throw new Error(`Permission denied: ${given}`);
|
|
1524
|
-
throw new Error(`No such directory: ${given}`);
|
|
1525
|
-
});
|
|
1526
|
-
if (!(await stat3(dir)).isDirectory())
|
|
1527
|
-
throw new Error(`Not a directory: ${given}`);
|
|
1528
|
-
let entries;
|
|
1529
|
-
try {
|
|
1530
|
-
entries = await readdir2(dir, { withFileTypes: true });
|
|
1531
|
-
} catch (error) {
|
|
1532
|
-
if (denied(error))
|
|
1533
|
-
throw new Error(`Permission denied: ${given}`);
|
|
1534
|
-
throw error;
|
|
1535
|
-
}
|
|
1536
|
-
const folders = [];
|
|
1537
|
-
for (const entry of entries) {
|
|
1538
|
-
if (entry.name.startsWith("."))
|
|
1539
|
-
continue;
|
|
1540
|
-
let isDirectory = entry.isDirectory();
|
|
1541
|
-
if (!isDirectory && entry.isSymbolicLink()) {
|
|
1542
|
-
isDirectory = await stat3(join5(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
1543
|
-
}
|
|
1544
|
-
if (isDirectory)
|
|
1545
|
-
folders.push({ name: entry.name, path: join5(dir, entry.name) });
|
|
1546
|
-
}
|
|
1547
|
-
folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
|
|
1548
|
-
return { path: dir, home: await realpath3(home).catch(() => home), folders };
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
// apps/host/src/files.ts
|
|
1552
|
-
import { spawn } from "node:child_process";
|
|
1553
|
-
import { opendir, realpath as realpath4, stat as stat4 } from "node:fs/promises";
|
|
1554
|
-
import { join as join6, resolve as resolve3, sep as sep3 } from "node:path";
|
|
1555
|
-
import { AgentDoc as AgentDoc3 } from "@earendil-works/pi-durable";
|
|
1556
|
-
var CACHE_TTL_MS = 3000;
|
|
1557
|
-
var MAX_CACHED_ROOTS = 64;
|
|
1558
|
-
var MAX_CONCURRENT_SCANS = 4;
|
|
1559
|
-
var SCAN_BUDGET_MS = 5000;
|
|
1560
|
-
var MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
|
|
1561
|
-
var MAX_WALK_ENTRIES = 20000;
|
|
1562
|
-
var MAX_RESULTS = 50;
|
|
1563
|
-
var UNUSABLE_PATH = /[\s`\x00-\x1f\x7f-\x9f]/;
|
|
1564
|
-
function createCandidateLister(enumerate, options = {}) {
|
|
1565
|
-
const now = options.now ?? Date.now;
|
|
1566
|
-
const ttlMs = options.ttlMs ?? CACHE_TTL_MS;
|
|
1567
|
-
const maxConcurrent = options.maxConcurrent ?? MAX_CONCURRENT_SCANS;
|
|
1568
|
-
const maxCached = options.maxCached ?? MAX_CACHED_ROOTS;
|
|
1569
|
-
const cache = new Map;
|
|
1570
|
-
const inflight = new Map;
|
|
1571
|
-
let running = 0;
|
|
1572
|
-
const waiting = [];
|
|
1573
|
-
return (root, mode) => {
|
|
1574
|
-
const key = `${mode}${root}`;
|
|
1575
|
-
const hit = cache.get(key);
|
|
1576
|
-
if (hit !== undefined) {
|
|
1577
|
-
if (hit.expiresAt > now())
|
|
1578
|
-
return Promise.resolve(hit.value);
|
|
1579
|
-
cache.delete(key);
|
|
1580
|
-
}
|
|
1581
|
-
const pending = inflight.get(key);
|
|
1582
|
-
if (pending !== undefined)
|
|
1583
|
-
return pending;
|
|
1584
|
-
const scan = (async () => {
|
|
1585
|
-
if (running >= maxConcurrent)
|
|
1586
|
-
await new Promise((release) => waiting.push(release));
|
|
1587
|
-
running += 1;
|
|
1588
|
-
try {
|
|
1589
|
-
return await enumerate(root, mode);
|
|
1590
|
-
} finally {
|
|
1591
|
-
running -= 1;
|
|
1592
|
-
waiting.shift()?.();
|
|
1593
|
-
}
|
|
1594
|
-
})();
|
|
1595
|
-
inflight.set(key, scan);
|
|
1596
|
-
scan.then((value) => {
|
|
1597
|
-
const at = now();
|
|
1598
|
-
for (const [cached, entry] of cache) {
|
|
1599
|
-
if (entry.expiresAt <= at)
|
|
1600
|
-
cache.delete(cached);
|
|
1601
|
-
}
|
|
1602
|
-
while (cache.size >= maxCached)
|
|
1603
|
-
cache.delete(cache.keys().next().value);
|
|
1604
|
-
cache.set(key, { value, expiresAt: at + ttlMs });
|
|
1605
|
-
}, () => {});
|
|
1606
|
-
return scan.finally(() => inflight.delete(key));
|
|
1607
|
-
};
|
|
1608
|
-
}
|
|
1609
|
-
function gitLines(cwd, args, limits) {
|
|
1610
|
-
return new Promise((resolvePromise, rejectPromise) => {
|
|
1611
|
-
const child = spawn("git", ["-C", cwd, ...args], { stdio: ["ignore", "pipe", "pipe"] });
|
|
1612
|
-
const chunks = [];
|
|
1613
|
-
let bytes = 0;
|
|
1614
|
-
let truncated = false;
|
|
1615
|
-
let settled = false;
|
|
1616
|
-
const kill = (markTruncated) => {
|
|
1617
|
-
truncated ||= markTruncated;
|
|
1618
|
-
child.kill("SIGKILL");
|
|
1619
|
-
};
|
|
1620
|
-
const timer = setTimeout(() => kill(true), Math.max(0, limits.deadline - limits.now()));
|
|
1621
|
-
const finish = (error) => {
|
|
1622
|
-
if (settled)
|
|
1623
|
-
return;
|
|
1624
|
-
settled = true;
|
|
1625
|
-
clearTimeout(timer);
|
|
1626
|
-
if (error !== undefined) {
|
|
1627
|
-
rejectPromise(error);
|
|
1628
|
-
return;
|
|
1629
|
-
}
|
|
1630
|
-
const output = Buffer.concat(chunks).toString("utf8");
|
|
1631
|
-
const parts = output.split("\x00");
|
|
1632
|
-
if (truncated)
|
|
1633
|
-
parts.pop();
|
|
1634
|
-
resolvePromise({ lines: parts.filter((part) => part !== ""), truncated });
|
|
1635
|
-
};
|
|
1636
|
-
child.stdout.on("data", (chunk) => {
|
|
1637
|
-
bytes += chunk.length;
|
|
1638
|
-
if (bytes > limits.maxBytes) {
|
|
1639
|
-
kill(true);
|
|
1640
|
-
return;
|
|
1641
|
-
}
|
|
1642
|
-
chunks.push(chunk);
|
|
1643
|
-
if (limits.now() > limits.deadline)
|
|
1644
|
-
kill(true);
|
|
1645
|
-
});
|
|
1646
|
-
child.on("error", (error) => finish(error));
|
|
1647
|
-
child.stderr.resume();
|
|
1648
|
-
child.on("close", (code) => {
|
|
1649
|
-
if (code === 0 || truncated) {
|
|
1650
|
-
finish(undefined);
|
|
1651
|
-
} else {
|
|
1652
|
-
finish(new Error(`git ${args.join(" ")} exited with code ${code}`));
|
|
1653
|
-
}
|
|
1654
|
-
});
|
|
1655
|
-
});
|
|
1909
|
+
throw new Error(problem);
|
|
1910
|
+
await enqueue(async () => {
|
|
1911
|
+
const target = join6(dir, `${name}.md`);
|
|
1912
|
+
if (!existsSync2(target))
|
|
1913
|
+
throw new Error(`No such agent: "${name}"`);
|
|
1914
|
+
await rm2(target);
|
|
1915
|
+
await reload();
|
|
1916
|
+
});
|
|
1917
|
+
},
|
|
1918
|
+
close() {
|
|
1919
|
+
closed = true;
|
|
1920
|
+
clearTimeout(timer);
|
|
1921
|
+
clearInterval(rescan);
|
|
1922
|
+
watcher.close();
|
|
1923
|
+
}
|
|
1924
|
+
};
|
|
1656
1925
|
}
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1926
|
+
|
|
1927
|
+
// apps/host/src/host.ts
|
|
1928
|
+
import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
|
|
1929
|
+
import lockfile from "proper-lockfile";
|
|
1930
|
+
|
|
1931
|
+
// apps/host/src/gateway.ts
|
|
1932
|
+
import { createServer } from "node:http";
|
|
1933
|
+
import { homedir as homedir2, networkInterfaces } from "node:os";
|
|
1934
|
+
import { join as join8, sep as sep5 } from "node:path";
|
|
1935
|
+
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
|
|
1936
|
+
import { clampThinkingLevel as clampThinkingLevel3, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
1937
|
+
import {
|
|
1938
|
+
AgentDoc as AgentDoc5,
|
|
1939
|
+
configure as configure3,
|
|
1940
|
+
InboxDoc,
|
|
1941
|
+
LiveDoc
|
|
1942
|
+
} from "@earendil-works/pi-durable";
|
|
1943
|
+
import { WebSocket, WebSocketServer } from "ws";
|
|
1944
|
+
import { Value as Value2 } from "typebox/value";
|
|
1945
|
+
|
|
1946
|
+
// apps/host/src/folders.ts
|
|
1947
|
+
import { readdir as readdir2, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
1948
|
+
import { isAbsolute as isAbsolute2, join as join7 } from "node:path";
|
|
1949
|
+
var denied = (error) => {
|
|
1950
|
+
const code = error?.code;
|
|
1951
|
+
return code === "EACCES" || code === "EPERM";
|
|
1952
|
+
};
|
|
1953
|
+
async function listFolders(path, home) {
|
|
1954
|
+
if (path !== undefined && !isAbsolute2(path))
|
|
1955
|
+
throw new Error(`Not an absolute path: ${path}`);
|
|
1956
|
+
const given = path ?? home;
|
|
1957
|
+
const dir = await realpath5(given).catch((error) => {
|
|
1958
|
+
if (denied(error))
|
|
1959
|
+
throw new Error(`Permission denied: ${given}`);
|
|
1960
|
+
throw new Error(`No such directory: ${given}`);
|
|
1664
1961
|
});
|
|
1962
|
+
if (!(await stat5(dir)).isDirectory())
|
|
1963
|
+
throw new Error(`Not a directory: ${given}`);
|
|
1964
|
+
let entries;
|
|
1965
|
+
try {
|
|
1966
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
1967
|
+
} catch (error) {
|
|
1968
|
+
if (denied(error))
|
|
1969
|
+
throw new Error(`Permission denied: ${given}`);
|
|
1970
|
+
throw error;
|
|
1971
|
+
}
|
|
1972
|
+
const folders = [];
|
|
1973
|
+
for (const entry of entries) {
|
|
1974
|
+
if (entry.name.startsWith("."))
|
|
1975
|
+
continue;
|
|
1976
|
+
let isDirectory = entry.isDirectory();
|
|
1977
|
+
if (!isDirectory && entry.isSymbolicLink()) {
|
|
1978
|
+
isDirectory = await stat5(join7(dir, entry.name)).then((target) => target.isDirectory(), () => false);
|
|
1979
|
+
}
|
|
1980
|
+
if (isDirectory)
|
|
1981
|
+
folders.push({ name: entry.name, path: join7(dir, entry.name) });
|
|
1982
|
+
}
|
|
1983
|
+
folders.sort((a, b) => a.name.localeCompare(b.name, "en", { numeric: true, sensitivity: "base" }));
|
|
1984
|
+
return { path: dir, home: await realpath5(home).catch(() => home), folders };
|
|
1665
1985
|
}
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1986
|
+
|
|
1987
|
+
// apps/host/src/skill-expansion.ts
|
|
1988
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1989
|
+
import { stripFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
1990
|
+
|
|
1991
|
+
// packages/client/src/skill-blocks.ts
|
|
1992
|
+
var OPEN = '<skill name="';
|
|
1993
|
+
var LOCATION = '" location="';
|
|
1994
|
+
var REFERENCES = `">
|
|
1995
|
+
References are relative to `;
|
|
1996
|
+
var CLOSE = `
|
|
1997
|
+
</skill>`;
|
|
1998
|
+
var SEPARATOR = `
|
|
1999
|
+
|
|
2000
|
+
`;
|
|
2001
|
+
function withSkillBlocks(blocks, text) {
|
|
2002
|
+
if (blocks.length === 0)
|
|
2003
|
+
return text;
|
|
2004
|
+
const written = blocks.map((block) => `${OPEN}${block.name}${LOCATION}${block.location}${REFERENCES}${block.baseDir}.
|
|
2005
|
+
|
|
2006
|
+
${block.body}${CLOSE}`);
|
|
2007
|
+
return [...written, text].join(SEPARATOR);
|
|
1672
2008
|
}
|
|
1673
|
-
var
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
2009
|
+
var isNameUnit = (unit) => unit >= 48 && unit <= 57 || unit >= 97 && unit <= 122 || unit === 45;
|
|
2010
|
+
function blockEnd(text, at) {
|
|
2011
|
+
if (!text.startsWith(OPEN, at))
|
|
2012
|
+
return -1;
|
|
2013
|
+
let i = at + OPEN.length;
|
|
2014
|
+
const name = i;
|
|
2015
|
+
while (i < text.length && isNameUnit(text.charCodeAt(i)))
|
|
2016
|
+
i += 1;
|
|
2017
|
+
if (i === name || !text.startsWith(LOCATION, i))
|
|
2018
|
+
return -1;
|
|
2019
|
+
i += LOCATION.length;
|
|
2020
|
+
const location = i;
|
|
2021
|
+
while (i < text.length && text[i] !== '"' && text[i] !== `
|
|
2022
|
+
`)
|
|
2023
|
+
i += 1;
|
|
2024
|
+
if (i === location || !text.startsWith(REFERENCES, i))
|
|
2025
|
+
return -1;
|
|
2026
|
+
i += REFERENCES.length;
|
|
2027
|
+
const lineEnd = text.indexOf(`
|
|
2028
|
+
`, i);
|
|
2029
|
+
if (lineEnd - i < 2 || text[lineEnd - 1] !== "." || text[lineEnd + 1] !== `
|
|
2030
|
+
`)
|
|
2031
|
+
return -1;
|
|
2032
|
+
const close = text.indexOf(CLOSE, lineEnd + 1);
|
|
2033
|
+
return close === -1 ? -1 : close + CLOSE.length;
|
|
2034
|
+
}
|
|
2035
|
+
function stripSkillPrefix(text) {
|
|
2036
|
+
const skills = [];
|
|
2037
|
+
let at = 0;
|
|
2038
|
+
for (;; ) {
|
|
2039
|
+
const end = blockEnd(text, at);
|
|
2040
|
+
if (end === -1 || !text.startsWith(SEPARATOR, end) || end + SEPARATOR.length === text.length) {
|
|
2041
|
+
return { text: text.slice(at), skills };
|
|
2042
|
+
}
|
|
2043
|
+
skills.push(text.slice(at + OPEN.length, text.indexOf('"', at + OPEN.length)));
|
|
2044
|
+
at = end + SEPARATOR.length;
|
|
1687
2045
|
}
|
|
1688
|
-
return { files: [...files], truncated: listing.truncated || deleted.truncated };
|
|
1689
2046
|
}
|
|
1690
|
-
|
|
1691
|
-
const
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
2047
|
+
function stripUserBlocks(content) {
|
|
2048
|
+
const attachments = [];
|
|
2049
|
+
let joined = "";
|
|
2050
|
+
if (typeof content === "string") {
|
|
2051
|
+
joined = content;
|
|
2052
|
+
} else {
|
|
2053
|
+
for (const block of content) {
|
|
2054
|
+
if (block.type !== "text" || typeof block.text !== "string")
|
|
2055
|
+
continue;
|
|
2056
|
+
const files = parseAttachmentBlock(block.text);
|
|
2057
|
+
if (files !== undefined)
|
|
2058
|
+
attachments.push(...files);
|
|
2059
|
+
else
|
|
2060
|
+
joined += block.text;
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
const typed = stripSkillPrefix(joined);
|
|
2064
|
+
return { text: typed.text, skills: typed.skills, attachments };
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
// packages/client/src/skill-mentions.ts
|
|
2068
|
+
var WHITESPACE = /\p{White_Space}/u;
|
|
2069
|
+
var WORD_CHAR = /[\p{Alphabetic}\p{N}_$]/u;
|
|
2070
|
+
var isNameUnit2 = (unit) => unit >= 48 && unit <= 57 || unit >= 97 && unit <= 122 || unit === 45;
|
|
2071
|
+
function skillMentionRanges(text, names) {
|
|
2072
|
+
const known = new Set(names);
|
|
2073
|
+
const ranges = [];
|
|
2074
|
+
for (let i = 0;i < text.length; i += 1) {
|
|
2075
|
+
if (text.charCodeAt(i) !== 36)
|
|
1696
2076
|
continue;
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
2077
|
+
if (i > 0 && !WHITESPACE.test(text[i - 1]))
|
|
2078
|
+
continue;
|
|
2079
|
+
let end = i + 1;
|
|
2080
|
+
while (end < text.length && isNameUnit2(text.charCodeAt(end)))
|
|
2081
|
+
end += 1;
|
|
2082
|
+
if (!known.has(text.slice(i + 1, end)))
|
|
2083
|
+
continue;
|
|
2084
|
+
if (end < text.length && WORD_CHAR.test(String.fromCodePoint(text.codePointAt(end))))
|
|
2085
|
+
continue;
|
|
2086
|
+
ranges.push({ start: i, end });
|
|
1700
2087
|
}
|
|
1701
|
-
return
|
|
2088
|
+
return ranges;
|
|
1702
2089
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
2090
|
+
|
|
2091
|
+
// apps/host/src/skill-expansion.ts
|
|
2092
|
+
function mentionedSkillNames(text, names) {
|
|
2093
|
+
return [...new Set(skillMentionRanges(text, names).map((range) => text.slice(range.start + 1, range.end)))];
|
|
2094
|
+
}
|
|
2095
|
+
async function expandSkills(text, names, skills) {
|
|
2096
|
+
const byName = new Map(skills.map((skill) => [skill.name, skill]));
|
|
2097
|
+
const blocks = [];
|
|
2098
|
+
const missing = [];
|
|
2099
|
+
const failures = [];
|
|
2100
|
+
for (const name of new Set(names)) {
|
|
2101
|
+
const skill = byName.get(name);
|
|
2102
|
+
if (skill === undefined) {
|
|
2103
|
+
missing.push(name);
|
|
2104
|
+
continue;
|
|
1712
2105
|
}
|
|
1713
|
-
const { dir, prefix } = dirs.pop();
|
|
1714
|
-
let handle;
|
|
1715
2106
|
try {
|
|
1716
|
-
|
|
2107
|
+
const body = stripFrontmatter(await readFile3(skill.filePath, "utf8")).trim();
|
|
2108
|
+
blocks.push({ name, location: skill.filePath, baseDir: skill.baseDir, body });
|
|
1717
2109
|
} catch (error) {
|
|
1718
|
-
|
|
1719
|
-
throw error;
|
|
1720
|
-
truncated = true;
|
|
1721
|
-
continue;
|
|
2110
|
+
failures.push({ name, filePath: skill.filePath, message: error instanceof Error ? error.message : String(error) });
|
|
1722
2111
|
}
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
2112
|
+
}
|
|
2113
|
+
return { text: withSkillBlocks(blocks, text), missing, failures };
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
// apps/host/src/commands.ts
|
|
2117
|
+
var BUILTIN_COMMANDS = [
|
|
2118
|
+
{
|
|
2119
|
+
info: {
|
|
2120
|
+
name: "compact",
|
|
2121
|
+
description: "Summarize older context to free up space",
|
|
2122
|
+
argumentHint: "[instructions]",
|
|
2123
|
+
source: "builtin"
|
|
2124
|
+
},
|
|
2125
|
+
run: async (target, args) => {
|
|
2126
|
+
await target.compact(args?.trim() || undefined);
|
|
1735
2127
|
}
|
|
1736
|
-
if (truncated)
|
|
1737
|
-
break;
|
|
1738
2128
|
}
|
|
1739
|
-
|
|
2129
|
+
];
|
|
2130
|
+
function listCommands() {
|
|
2131
|
+
return { commands: BUILTIN_COMMANDS.map((command) => command.info) };
|
|
1740
2132
|
}
|
|
1741
|
-
async function
|
|
1742
|
-
const
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
2133
|
+
async function runCommand(target, name, args) {
|
|
2134
|
+
const command = BUILTIN_COMMANDS.find((candidate) => candidate.info.name === name);
|
|
2135
|
+
if (command === undefined)
|
|
2136
|
+
throw new Error(`Unknown command: /${name}`);
|
|
2137
|
+
await command.run(target, args);
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
// packages/protocol/src/images.ts
|
|
2141
|
+
var IMAGE_MIME_TYPES = ["image/jpeg", "image/png", "image/gif", "image/webp"];
|
|
2142
|
+
var REF_ENCODER = new TextEncoder;
|
|
2143
|
+
var MAX_IMAGES_PER_MESSAGE = 4;
|
|
2144
|
+
var MAX_IMAGE_BYTES = 5 * 1024 * 1024;
|
|
2145
|
+
var BASE64 = /^[A-Za-z0-9+/]*={0,2}$/;
|
|
2146
|
+
function imageProblem(images) {
|
|
2147
|
+
if (images.length > MAX_IMAGES_PER_MESSAGE)
|
|
2148
|
+
return `At most ${MAX_IMAGES_PER_MESSAGE} images per message`;
|
|
2149
|
+
for (let i = 0;i < images.length; i++) {
|
|
2150
|
+
const image = images[i];
|
|
2151
|
+
if (!IMAGE_MIME_TYPES.includes(image.mimeType)) {
|
|
2152
|
+
return `Unsupported image type: ${image.mimeType} (use JPEG, PNG, GIF or WebP)`;
|
|
2153
|
+
}
|
|
2154
|
+
if (image.data === "" || image.data.length % 4 !== 0 || !BASE64.test(image.data)) {
|
|
2155
|
+
return `Image ${i + 1} is not valid base64`;
|
|
2156
|
+
}
|
|
2157
|
+
const padding = image.data.endsWith("==") ? 2 : image.data.endsWith("=") ? 1 : 0;
|
|
2158
|
+
if (image.data.length * 3 / 4 - padding > MAX_IMAGE_BYTES)
|
|
2159
|
+
return `Image ${i + 1} is larger than 5 MB`;
|
|
1751
2160
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
// apps/host/src/image-refs.ts
|
|
2165
|
+
import { createHash } from "node:crypto";
|
|
2166
|
+
import { applyImmutable } from "@earendil-works/chord/delta";
|
|
2167
|
+
var isObject2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2168
|
+
var isImageBlock = (value) => value.type === "image" && typeof value.data === "string" && typeof value.mimeType === "string";
|
|
2169
|
+
var hashed = new WeakMap;
|
|
2170
|
+
function refOf(block) {
|
|
2171
|
+
let ref = hashed.get(block);
|
|
2172
|
+
if (ref === undefined) {
|
|
2173
|
+
ref = createHash("sha256").update(block.mimeType).update(`
|
|
2174
|
+
`).update(block.data).digest("hex");
|
|
2175
|
+
hashed.set(block, ref);
|
|
1754
2176
|
}
|
|
1755
|
-
return
|
|
2177
|
+
return ref;
|
|
1756
2178
|
}
|
|
1757
|
-
function
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
2179
|
+
function slotAt(path) {
|
|
2180
|
+
if (path[0] === "entries") {
|
|
2181
|
+
if (path.length === 1)
|
|
2182
|
+
return "entries";
|
|
2183
|
+
if (path.length === 2 && typeof path[1] === "number")
|
|
2184
|
+
return "entry";
|
|
2185
|
+
if (path.length === 3 && typeof path[1] === "number" && path[2] === "model")
|
|
2186
|
+
return "messages";
|
|
2187
|
+
if (path.length === 4 && typeof path[1] === "number" && path[2] === "model" && typeof path[3] === "number")
|
|
2188
|
+
return "message";
|
|
2189
|
+
if (path.length === 5 && typeof path[1] === "number" && path[2] === "model" && typeof path[3] === "number" && path[4] === "content")
|
|
2190
|
+
return "content";
|
|
2191
|
+
if (path.length === 6 && typeof path[1] === "number" && path[2] === "model" && typeof path[3] === "number" && path[4] === "content" && typeof path[5] === "number")
|
|
2192
|
+
return "block";
|
|
2193
|
+
return "none";
|
|
1762
2194
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
if (
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
2195
|
+
if (path[0] !== "docs")
|
|
2196
|
+
return "none";
|
|
2197
|
+
if (path.length === 1)
|
|
2198
|
+
return "docs";
|
|
2199
|
+
if (path[1] === "pi.inbox") {
|
|
2200
|
+
if (path.length === 2)
|
|
2201
|
+
return "inbox";
|
|
2202
|
+
if (path.length === 3 && path[2] === "items")
|
|
2203
|
+
return "items";
|
|
2204
|
+
if (path.length === 4 && path[2] === "items" && typeof path[3] === "number")
|
|
2205
|
+
return "item";
|
|
2206
|
+
if (path.length === 5 && path[2] === "items" && typeof path[3] === "number" && path[4] === "content")
|
|
2207
|
+
return "content";
|
|
2208
|
+
if (path.length === 6 && path[2] === "items" && typeof path[3] === "number" && path[4] === "content" && typeof path[5] === "number")
|
|
2209
|
+
return "block";
|
|
2210
|
+
return "none";
|
|
1774
2211
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
2212
|
+
if (path[1] === "pi.live") {
|
|
2213
|
+
if (path.length === 2)
|
|
2214
|
+
return "live";
|
|
2215
|
+
if (path.length === 3 && path[2] === "generation")
|
|
2216
|
+
return "generation";
|
|
2217
|
+
if (path.length === 4 && path[2] === "generation" && path[3] === "message")
|
|
2218
|
+
return "message";
|
|
2219
|
+
if (path.length === 5 && path[2] === "generation" && path[3] === "message" && path[4] === "content")
|
|
2220
|
+
return "content";
|
|
2221
|
+
if (path.length === 6 && path[2] === "generation" && path[3] === "message" && path[4] === "content" && typeof path[5] === "number")
|
|
2222
|
+
return "block";
|
|
2223
|
+
return "none";
|
|
2224
|
+
}
|
|
2225
|
+
return "none";
|
|
1778
2226
|
}
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
if (
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
if (info === undefined || !info.isDirectory()) {
|
|
1795
|
-
throw new Error(`Conversation ${scope.conversationId}'s checkout is unavailable: ${cwd}`);
|
|
2227
|
+
function withRef(block) {
|
|
2228
|
+
const ref = refOf(block);
|
|
2229
|
+
const { data: _data, ...rest } = block;
|
|
2230
|
+
return { ...rest, ref };
|
|
2231
|
+
}
|
|
2232
|
+
var HASH_YIELD_BYTES = 8 * 1024 * 1024;
|
|
2233
|
+
var yieldToEventLoop = () => new Promise((resolve5) => setImmediate(resolve5));
|
|
2234
|
+
async function hashBlocks(blocks) {
|
|
2235
|
+
let bytes = 0;
|
|
2236
|
+
for (const block of blocks) {
|
|
2237
|
+
if (hashed.has(block))
|
|
2238
|
+
continue;
|
|
2239
|
+
if (bytes >= HASH_YIELD_BYTES) {
|
|
2240
|
+
bytes = 0;
|
|
2241
|
+
await yieldToEventLoop();
|
|
1796
2242
|
}
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
2243
|
+
refOf(block);
|
|
2244
|
+
bytes += block.data.length;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
function rewriteEntries(value) {
|
|
2248
|
+
if (!Array.isArray(value))
|
|
2249
|
+
return value;
|
|
2250
|
+
let out;
|
|
2251
|
+
for (let i = 0;i < value.length; i++) {
|
|
2252
|
+
const next = rewriteEntry(value[i]);
|
|
2253
|
+
if (next !== value[i])
|
|
2254
|
+
(out ??= value.slice())[i] = next;
|
|
2255
|
+
}
|
|
2256
|
+
return out ?? value;
|
|
2257
|
+
}
|
|
2258
|
+
function rewriteEntry(value) {
|
|
2259
|
+
if (!isObject2(value) || value.model === undefined)
|
|
2260
|
+
return value;
|
|
2261
|
+
const model = rewriteMessages(value.model);
|
|
2262
|
+
return model === value.model ? value : { ...value, model };
|
|
2263
|
+
}
|
|
2264
|
+
function rewriteMessages(value) {
|
|
2265
|
+
if (!Array.isArray(value))
|
|
2266
|
+
return value;
|
|
2267
|
+
let out;
|
|
2268
|
+
for (let i = 0;i < value.length; i++) {
|
|
2269
|
+
const next = rewriteMessage(value[i]);
|
|
2270
|
+
if (next !== value[i])
|
|
2271
|
+
(out ??= value.slice())[i] = next;
|
|
2272
|
+
}
|
|
2273
|
+
return out ?? value;
|
|
2274
|
+
}
|
|
2275
|
+
function rewriteMessage(value) {
|
|
2276
|
+
if (!isObject2(value) || value.content === undefined)
|
|
2277
|
+
return value;
|
|
2278
|
+
const content = rewriteContent(value.content);
|
|
2279
|
+
return content === value.content ? value : { ...value, content };
|
|
2280
|
+
}
|
|
2281
|
+
function rewriteContent(value) {
|
|
2282
|
+
if (!Array.isArray(value))
|
|
2283
|
+
return value;
|
|
2284
|
+
let out;
|
|
2285
|
+
for (let i = 0;i < value.length; i++) {
|
|
2286
|
+
const block = value[i];
|
|
2287
|
+
const next = isObject2(block) && isImageBlock(block) ? withRef(block) : block;
|
|
2288
|
+
if (next !== block)
|
|
2289
|
+
(out ??= value.slice())[i] = next;
|
|
2290
|
+
}
|
|
2291
|
+
return out ?? value;
|
|
2292
|
+
}
|
|
2293
|
+
function rewriteItems(value) {
|
|
2294
|
+
if (!Array.isArray(value))
|
|
2295
|
+
return value;
|
|
2296
|
+
let out;
|
|
2297
|
+
for (let i = 0;i < value.length; i++) {
|
|
2298
|
+
const next = rewriteItem(value[i]);
|
|
2299
|
+
if (next !== value[i])
|
|
2300
|
+
(out ??= value.slice())[i] = next;
|
|
2301
|
+
}
|
|
2302
|
+
return out ?? value;
|
|
2303
|
+
}
|
|
2304
|
+
function rewriteItem(value) {
|
|
2305
|
+
if (!isObject2(value) || value.content === undefined)
|
|
2306
|
+
return value;
|
|
2307
|
+
const content = rewriteContent(value.content);
|
|
2308
|
+
return content === value.content ? value : { ...value, content };
|
|
2309
|
+
}
|
|
2310
|
+
function rewriteInbox(value) {
|
|
2311
|
+
if (!isObject2(value) || value.items === undefined)
|
|
2312
|
+
return value;
|
|
2313
|
+
const items = rewriteItems(value.items);
|
|
2314
|
+
return items === value.items ? value : { ...value, items };
|
|
2315
|
+
}
|
|
2316
|
+
function rewriteGeneration(value) {
|
|
2317
|
+
if (!isObject2(value) || value.message === undefined)
|
|
2318
|
+
return value;
|
|
2319
|
+
const message = rewriteMessage(value.message);
|
|
2320
|
+
return message === value.message ? value : { ...value, message };
|
|
2321
|
+
}
|
|
2322
|
+
function rewriteLive(value) {
|
|
2323
|
+
if (!isObject2(value) || value.generation === undefined)
|
|
2324
|
+
return value;
|
|
2325
|
+
const generation = rewriteGeneration(value.generation);
|
|
2326
|
+
return generation === value.generation ? value : { ...value, generation };
|
|
2327
|
+
}
|
|
2328
|
+
function rewriteDocs(value) {
|
|
2329
|
+
if (!isObject2(value))
|
|
2330
|
+
return value;
|
|
2331
|
+
let out;
|
|
2332
|
+
for (const key of ["pi.inbox", "pi.live"]) {
|
|
2333
|
+
const doc = value[key];
|
|
2334
|
+
if (doc === undefined)
|
|
2335
|
+
continue;
|
|
2336
|
+
const next = key === "pi.inbox" ? rewriteInbox(doc) : rewriteLive(doc);
|
|
2337
|
+
if (next !== doc)
|
|
2338
|
+
(out ??= { ...value })[key] = next;
|
|
2339
|
+
}
|
|
2340
|
+
return out ?? value;
|
|
2341
|
+
}
|
|
2342
|
+
function rewriteView(value) {
|
|
2343
|
+
if (!isObject2(value))
|
|
2344
|
+
return value;
|
|
2345
|
+
let out;
|
|
2346
|
+
if (value.entries !== undefined) {
|
|
2347
|
+
const entries = rewriteEntries(value.entries);
|
|
2348
|
+
if (entries !== value.entries)
|
|
2349
|
+
(out ??= { ...value }).entries = entries;
|
|
2350
|
+
}
|
|
2351
|
+
if (value.docs !== undefined) {
|
|
2352
|
+
const docs = rewriteDocs(value.docs);
|
|
2353
|
+
if (docs !== value.docs)
|
|
2354
|
+
(out ??= { ...value }).docs = docs;
|
|
2355
|
+
}
|
|
2356
|
+
return out ?? value;
|
|
2357
|
+
}
|
|
2358
|
+
function rewriteSlot(slot, value) {
|
|
2359
|
+
switch (slot) {
|
|
2360
|
+
case "entries":
|
|
2361
|
+
return rewriteEntries(value);
|
|
2362
|
+
case "entry":
|
|
2363
|
+
return rewriteEntry(value);
|
|
2364
|
+
case "messages":
|
|
2365
|
+
return rewriteMessages(value);
|
|
2366
|
+
case "message":
|
|
2367
|
+
return rewriteMessage(value);
|
|
2368
|
+
case "content":
|
|
2369
|
+
return rewriteContent(value);
|
|
2370
|
+
case "block":
|
|
2371
|
+
return isObject2(value) && isImageBlock(value) ? withRef(value) : value;
|
|
2372
|
+
case "docs":
|
|
2373
|
+
return rewriteDocs(value);
|
|
2374
|
+
case "inbox":
|
|
2375
|
+
return rewriteInbox(value);
|
|
2376
|
+
case "items":
|
|
2377
|
+
return rewriteItems(value);
|
|
2378
|
+
case "item":
|
|
2379
|
+
return rewriteItem(value);
|
|
2380
|
+
case "live":
|
|
2381
|
+
return rewriteLive(value);
|
|
2382
|
+
case "generation":
|
|
2383
|
+
return rewriteGeneration(value);
|
|
2384
|
+
default:
|
|
2385
|
+
return value;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
function rewriteOp(op) {
|
|
2389
|
+
switch (op[0]) {
|
|
2390
|
+
case "r": {
|
|
2391
|
+
const value = rewriteView(op[1]);
|
|
2392
|
+
return value === op[1] ? op : ["r", value];
|
|
1803
2393
|
}
|
|
1804
|
-
|
|
2394
|
+
case "s": {
|
|
2395
|
+
const slot = slotAt(op[1]);
|
|
2396
|
+
if (slot === "none")
|
|
2397
|
+
return op;
|
|
2398
|
+
const value = rewriteSlot(slot, op[2]);
|
|
2399
|
+
return value === op[2] ? op : ["s", op[1], value];
|
|
2400
|
+
}
|
|
2401
|
+
case "p": {
|
|
2402
|
+
const path = op[1];
|
|
2403
|
+
const start = op[2];
|
|
2404
|
+
let items;
|
|
2405
|
+
for (let i = 0;i < op[4].length; i++) {
|
|
2406
|
+
const item = op[4][i];
|
|
2407
|
+
const slot = slotAt([...path, start + i]);
|
|
2408
|
+
const next = slot === "none" ? item : rewriteSlot(slot, item);
|
|
2409
|
+
if (next !== item)
|
|
2410
|
+
(items ??= op[4].slice())[i] = next;
|
|
2411
|
+
}
|
|
2412
|
+
return items === undefined ? op : ["p", op[1], op[2], op[3], items];
|
|
2413
|
+
}
|
|
2414
|
+
default:
|
|
2415
|
+
return op;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
function resolve5(root, path, length) {
|
|
2419
|
+
let node = root;
|
|
2420
|
+
for (let i = 0;i < length; i++) {
|
|
2421
|
+
if (node === null || typeof node !== "object")
|
|
1805
2422
|
return;
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
2423
|
+
node = node[path[i]];
|
|
2424
|
+
}
|
|
2425
|
+
return node;
|
|
2426
|
+
}
|
|
2427
|
+
function reachesIntoImage(op, root) {
|
|
2428
|
+
if (op[0] === "r" || op[0] === "m")
|
|
2429
|
+
return false;
|
|
2430
|
+
const path = op[1];
|
|
2431
|
+
const depth = op[0] === "p" ? path.length : path.length - 1;
|
|
2432
|
+
for (let length = 1;length <= depth; length++) {
|
|
2433
|
+
if (slotAt(path.slice(0, length)) !== "block")
|
|
2434
|
+
continue;
|
|
2435
|
+
const node = resolve5(root, path, length);
|
|
2436
|
+
if (isObject2(node) && isImageBlock(node))
|
|
2437
|
+
return true;
|
|
2438
|
+
}
|
|
2439
|
+
return false;
|
|
2440
|
+
}
|
|
2441
|
+
function* contentArrays(view) {
|
|
2442
|
+
if (!isObject2(view))
|
|
2443
|
+
return;
|
|
2444
|
+
if (Array.isArray(view.entries)) {
|
|
2445
|
+
for (const entry of view.entries) {
|
|
2446
|
+
if (!isObject2(entry) || !Array.isArray(entry.model))
|
|
2447
|
+
continue;
|
|
2448
|
+
for (const message of entry.model) {
|
|
2449
|
+
if (isObject2(message) && Array.isArray(message.content))
|
|
2450
|
+
yield message.content;
|
|
2451
|
+
}
|
|
1809
2452
|
}
|
|
1810
|
-
root = normalized;
|
|
1811
|
-
mode = scope.checkout === "worktree" ? "head" : "live";
|
|
1812
2453
|
}
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
2454
|
+
if (!isObject2(view.docs))
|
|
2455
|
+
return;
|
|
2456
|
+
const inbox = view.docs["pi.inbox"];
|
|
2457
|
+
if (isObject2(inbox) && Array.isArray(inbox.items)) {
|
|
2458
|
+
for (const item of inbox.items) {
|
|
2459
|
+
if (isObject2(item) && Array.isArray(item.content))
|
|
2460
|
+
yield item.content;
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
const live = view.docs["pi.live"];
|
|
2464
|
+
if (isObject2(live) && isObject2(live.generation) && isObject2(live.generation.message) && Array.isArray(live.generation.message.content)) {
|
|
2465
|
+
yield live.generation.message.content;
|
|
2466
|
+
}
|
|
1816
2467
|
}
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
function imageProblem(images) {
|
|
1824
|
-
if (images.length > MAX_IMAGES_PER_MESSAGE)
|
|
1825
|
-
return `At most ${MAX_IMAGES_PER_MESSAGE} images per message`;
|
|
1826
|
-
for (let i = 0;i < images.length; i++) {
|
|
1827
|
-
const image = images[i];
|
|
1828
|
-
if (!IMAGE_MIME_TYPES.includes(image.mimeType)) {
|
|
1829
|
-
return `Unsupported image type: ${image.mimeType} (use JPEG, PNG, GIF or WebP)`;
|
|
2468
|
+
function imageBlocks(view) {
|
|
2469
|
+
const blocks = [];
|
|
2470
|
+
for (const content of contentArrays(view)) {
|
|
2471
|
+
for (const block of content) {
|
|
2472
|
+
if (isObject2(block) && isImageBlock(block))
|
|
2473
|
+
blocks.push(block);
|
|
1830
2474
|
}
|
|
1831
|
-
|
|
1832
|
-
|
|
2475
|
+
}
|
|
2476
|
+
return blocks;
|
|
2477
|
+
}
|
|
2478
|
+
function findImage(view, ref) {
|
|
2479
|
+
for (const block of imageBlocks(view)) {
|
|
2480
|
+
if (refOf(block) === ref) {
|
|
2481
|
+
return { data: block.data, mimeType: block.mimeType };
|
|
1833
2482
|
}
|
|
1834
|
-
const padding = image.data.endsWith("==") ? 2 : image.data.endsWith("=") ? 1 : 0;
|
|
1835
|
-
if (image.data.length * 3 / 4 - padding > MAX_IMAGE_BYTES)
|
|
1836
|
-
return `Image ${i + 1} is larger than 5 MB`;
|
|
1837
2483
|
}
|
|
1838
2484
|
return;
|
|
1839
2485
|
}
|
|
1840
2486
|
|
|
2487
|
+
class ImageRefs {
|
|
2488
|
+
async value(view) {
|
|
2489
|
+
const blocks = imageBlocks(view);
|
|
2490
|
+
if (blocks.length > 0)
|
|
2491
|
+
await hashBlocks(blocks);
|
|
2492
|
+
return rewriteView(view);
|
|
2493
|
+
}
|
|
2494
|
+
image(view, ref) {
|
|
2495
|
+
return findImage(view, ref);
|
|
2496
|
+
}
|
|
2497
|
+
async ops(ops, before) {
|
|
2498
|
+
let state = before;
|
|
2499
|
+
let touched = false;
|
|
2500
|
+
for (const op of ops) {
|
|
2501
|
+
if (!touched && reachesIntoImage(op, state))
|
|
2502
|
+
touched = true;
|
|
2503
|
+
state = applyImmutable(state, [op]);
|
|
2504
|
+
}
|
|
2505
|
+
if (touched) {
|
|
2506
|
+
await hashBlocks(imageBlocks(state));
|
|
2507
|
+
return [["r", rewriteView(state)]];
|
|
2508
|
+
}
|
|
2509
|
+
if (ops.some((op) => op[0] === "r"))
|
|
2510
|
+
await hashBlocks(imageBlocks(state));
|
|
2511
|
+
return ops.map(rewriteOp);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
|
|
1841
2515
|
// apps/host/src/auto-title.ts
|
|
1842
2516
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
1843
2517
|
|
|
1844
2518
|
// apps/host/src/title-doc.ts
|
|
1845
|
-
import { defineDoc as
|
|
1846
|
-
var TitleDoc =
|
|
2519
|
+
import { defineDoc as defineDoc6 } from "@earendil-works/pi-durable";
|
|
2520
|
+
var TitleDoc = defineDoc6({
|
|
1847
2521
|
kind: "pinomad.title",
|
|
1848
2522
|
version: 1,
|
|
1849
2523
|
scope: "conversation",
|
|
@@ -1912,7 +2586,8 @@ function attachAutoTitle(runs, deps) {
|
|
|
1912
2586
|
const model = ref === null ? undefined : deps.models.getModel(ref.provider, ref.modelId);
|
|
1913
2587
|
if (model === undefined)
|
|
1914
2588
|
return;
|
|
1915
|
-
const
|
|
2589
|
+
const firstMessage = first.model?.[0];
|
|
2590
|
+
const firstText = (stripUserBlocks(firstMessage?.content ?? "").text || "(images or files only)").slice(0, TITLE_INPUT_LIMIT);
|
|
1916
2591
|
const answer = textOf(last.content).slice(0, TITLE_INPUT_LIMIT);
|
|
1917
2592
|
const request = {
|
|
1918
2593
|
systemPrompt: TITLE_PROMPT,
|
|
@@ -1959,9 +2634,9 @@ ${answer}`}`,
|
|
|
1959
2634
|
}
|
|
1960
2635
|
|
|
1961
2636
|
// apps/host/src/push.ts
|
|
1962
|
-
import { createHash } from "node:crypto";
|
|
2637
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
1963
2638
|
import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
|
|
1964
|
-
import { defineDoc as
|
|
2639
|
+
import { defineDoc as defineDoc7 } from "@earendil-works/pi-durable";
|
|
1965
2640
|
|
|
1966
2641
|
// packages/client/src/family.ts
|
|
1967
2642
|
var SUBAGENT_TASK_PREFIX = /^\s*in the working directory\s+\S+,\s*/i;
|
|
@@ -2019,7 +2694,7 @@ function diffRunNotices(previous, organized) {
|
|
|
2019
2694
|
|
|
2020
2695
|
// apps/host/src/push.ts
|
|
2021
2696
|
var context2 = BACKGROUND_CONTEXT2;
|
|
2022
|
-
var PushDoc =
|
|
2697
|
+
var PushDoc = defineDoc7({
|
|
2023
2698
|
kind: "pinomad.push",
|
|
2024
2699
|
version: 2,
|
|
2025
2700
|
scope: "session",
|
|
@@ -2102,7 +2777,7 @@ var realTimers = {
|
|
|
2102
2777
|
clearTimeout: (handle) => clearTimeout(handle)
|
|
2103
2778
|
};
|
|
2104
2779
|
var tokenHint = (token) => `…${token.slice(-6)}`;
|
|
2105
|
-
var collapseId = (hostKey, id) =>
|
|
2780
|
+
var collapseId = (hostKey, id) => createHash2("sha256").update(`${hostKey}:${id}`).digest("hex").slice(0, 32);
|
|
2106
2781
|
async function attachPushSender(options) {
|
|
2107
2782
|
const { harness, conversations } = options;
|
|
2108
2783
|
const transport = options.transport ?? UNAVAILABLE_TRANSPORT;
|
|
@@ -2152,7 +2827,7 @@ async function attachPushSender(options) {
|
|
|
2152
2827
|
return named;
|
|
2153
2828
|
if (notice.body !== "Finished" || disposed)
|
|
2154
2829
|
return FALLBACK_TITLE;
|
|
2155
|
-
return await new Promise((
|
|
2830
|
+
return await new Promise((resolve6) => {
|
|
2156
2831
|
const waiters = titleWaiters.get(notice.id) ?? new Set;
|
|
2157
2832
|
titleWaiters.set(notice.id, waiters);
|
|
2158
2833
|
const settle = (title) => {
|
|
@@ -2160,7 +2835,7 @@ async function attachPushSender(options) {
|
|
|
2160
2835
|
if (waiters.size === 0)
|
|
2161
2836
|
titleWaiters.delete(notice.id);
|
|
2162
2837
|
timers.clearTimeout(timer);
|
|
2163
|
-
|
|
2838
|
+
resolve6(title);
|
|
2164
2839
|
};
|
|
2165
2840
|
const timer = timers.setTimeout(() => settle(FALLBACK_TITLE), TITLE_WAIT_MS);
|
|
2166
2841
|
waiters.add(settle);
|
|
@@ -2360,6 +3035,28 @@ function toolTarget(args) {
|
|
|
2360
3035
|
// apps/host/src/gateway.ts
|
|
2361
3036
|
var context3 = BACKGROUND_CONTEXT3;
|
|
2362
3037
|
var forkTails = new WeakMap;
|
|
3038
|
+
function assertQueuedInput(record, conversationId) {
|
|
3039
|
+
if (record.conversationId !== conversationId) {
|
|
3040
|
+
throw new Error(`Submission ${record.id} belongs to another conversation`);
|
|
3041
|
+
}
|
|
3042
|
+
if (record.type !== "input")
|
|
3043
|
+
throw new Error(`Submission ${record.id} is a write, not a message`);
|
|
3044
|
+
}
|
|
3045
|
+
async function dismissPendingQuestions(tx, docs, conversationId) {
|
|
3046
|
+
let dismissed = false;
|
|
3047
|
+
for (const doc of docs) {
|
|
3048
|
+
const value = await tx.doc(doc.token, conversationId);
|
|
3049
|
+
if (!Value2.Check(QuestionSchema, value))
|
|
3050
|
+
continue;
|
|
3051
|
+
for (const request of value.requests) {
|
|
3052
|
+
if (request.resolution === undefined) {
|
|
3053
|
+
request.resolution = { outcome: "dismissed", at: Date.now() };
|
|
3054
|
+
dismissed = true;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
return dismissed;
|
|
3059
|
+
}
|
|
2363
3060
|
function serializedFork(harness, work) {
|
|
2364
3061
|
const next = (forkTails.get(harness) ?? Promise.resolve()).then(work, work);
|
|
2365
3062
|
const settled = next.then(() => {}, () => {});
|
|
@@ -2374,6 +3071,7 @@ var LOOPBACK_SECURE_PATH = "/secure";
|
|
|
2374
3071
|
async function startGateway(options) {
|
|
2375
3072
|
const conversations = await ConversationList.open(options.harness, options.docs);
|
|
2376
3073
|
const fileLister = createCandidateLister(enumerateRoot);
|
|
3074
|
+
const skillLister = createSkillLister((root, mode) => enumerateSkills(root, mode, options.agentsHome));
|
|
2377
3075
|
const detachAutoTitle = options.autoTitle === false ? undefined : attachAutoTitle(conversations, {
|
|
2378
3076
|
harness: options.harness,
|
|
2379
3077
|
models: options.models,
|
|
@@ -2386,12 +3084,12 @@ async function startGateway(options) {
|
|
|
2386
3084
|
response.end();
|
|
2387
3085
|
});
|
|
2388
3086
|
});
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
http.once("listening", resolve4);
|
|
3087
|
+
await new Promise((resolve6, reject) => {
|
|
3088
|
+
http.once("listening", resolve6);
|
|
2392
3089
|
http.once("error", reject);
|
|
2393
3090
|
http.listen(options.port, "127.0.0.1");
|
|
2394
3091
|
});
|
|
3092
|
+
const server = new WebSocketServer({ server: http });
|
|
2395
3093
|
const { port } = http.address();
|
|
2396
3094
|
const browserOrigins = [...options.browserOrigins ?? [], `http://127.0.0.1:${port}`];
|
|
2397
3095
|
const clients = new Set;
|
|
@@ -2405,7 +3103,7 @@ async function startGateway(options) {
|
|
|
2405
3103
|
if (joined?.source === "account") {
|
|
2406
3104
|
broadcast("info", `New device "${joined.name}" joined through your account`);
|
|
2407
3105
|
}
|
|
2408
|
-
const client = new GatewayClient(connection, options, conversations, fileLister, {
|
|
3106
|
+
const client = new GatewayClient(connection, options, conversations, fileLister, skillLister, {
|
|
2409
3107
|
devicePublicKey,
|
|
2410
3108
|
closed: (self) => {
|
|
2411
3109
|
clients.delete(self);
|
|
@@ -2465,7 +3163,11 @@ async function startGateway(options) {
|
|
|
2465
3163
|
}
|
|
2466
3164
|
register(tokenConnection(socket));
|
|
2467
3165
|
});
|
|
2468
|
-
remote = await startRemote(options, admission, register)
|
|
3166
|
+
remote = await startRemote(options, admission, register).catch(async (error) => {
|
|
3167
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
3168
|
+
await new Promise((resolve6) => http.close(() => resolve6()));
|
|
3169
|
+
throw error;
|
|
3170
|
+
});
|
|
2469
3171
|
const secure = options.secure;
|
|
2470
3172
|
const detachSettingsWatch = options.settings.subscribe((settings) => {
|
|
2471
3173
|
if (!settings.acceptAccountDevices)
|
|
@@ -2479,6 +3181,12 @@ async function startGateway(options) {
|
|
|
2479
3181
|
signingKey: secure.relay.signingKey,
|
|
2480
3182
|
noisePublicKey: secure.hostKey.publicKey,
|
|
2481
3183
|
accept: (socket) => secureHandshake(socket, secure, admission, register),
|
|
3184
|
+
hostName: {
|
|
3185
|
+
get value() {
|
|
3186
|
+
return options.settings.value.hostName;
|
|
3187
|
+
},
|
|
3188
|
+
subscribe: (listener) => options.settings.subscribe((settings) => listener(settings.hostName))
|
|
3189
|
+
},
|
|
2482
3190
|
accountOffer: async () => {
|
|
2483
3191
|
if (!options.settings.value.acceptAccountDevices)
|
|
2484
3192
|
return { ok: false, reason: "disabled" };
|
|
@@ -2531,8 +3239,8 @@ async function startGateway(options) {
|
|
|
2531
3239
|
for (const socket of server.clients)
|
|
2532
3240
|
socket.terminate();
|
|
2533
3241
|
await remote?.close();
|
|
2534
|
-
await new Promise((
|
|
2535
|
-
await new Promise((
|
|
3242
|
+
await new Promise((resolve6) => server.close(() => resolve6()));
|
|
3243
|
+
await new Promise((resolve6) => http.close(() => resolve6()));
|
|
2536
3244
|
}
|
|
2537
3245
|
};
|
|
2538
3246
|
}
|
|
@@ -2541,7 +3249,7 @@ function tokenConnection(socket) {
|
|
|
2541
3249
|
get open() {
|
|
2542
3250
|
return socket.readyState === WebSocket.OPEN;
|
|
2543
3251
|
},
|
|
2544
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((
|
|
3252
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve6) => socket.send(text, () => resolve6())) : Promise.resolve(),
|
|
2545
3253
|
close: (code, reason) => socket.close(code, reason),
|
|
2546
3254
|
handle(handlers) {
|
|
2547
3255
|
socket.on("message", (data) => handlers.message(String(data)));
|
|
@@ -2554,9 +3262,9 @@ function secureConnection(socket, session) {
|
|
|
2554
3262
|
get open() {
|
|
2555
3263
|
return socket.readyState === WebSocket.OPEN;
|
|
2556
3264
|
},
|
|
2557
|
-
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((
|
|
3265
|
+
send: (text) => socket.readyState === WebSocket.OPEN ? new Promise((resolve6) => {
|
|
2558
3266
|
const chunks = session.seal(text);
|
|
2559
|
-
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () =>
|
|
3267
|
+
chunks.forEach((chunk, index) => socket.send(chunk, index === chunks.length - 1 ? () => resolve6() : undefined));
|
|
2560
3268
|
}) : Promise.resolve(),
|
|
2561
3269
|
close: (code, reason) => socket.close(code, reason),
|
|
2562
3270
|
handle(handlers) {
|
|
@@ -2659,13 +3367,13 @@ async function startRemote(options, admission, register) {
|
|
|
2659
3367
|
response.end();
|
|
2660
3368
|
});
|
|
2661
3369
|
});
|
|
2662
|
-
const secure = new WebSocketServer({ server: http });
|
|
2663
|
-
secure.on("connection", (socket) => secureHandshake(socket, spec, admission, register));
|
|
2664
3370
|
await new Promise((resolveListen, reject) => {
|
|
2665
3371
|
http.once("listening", resolveListen);
|
|
2666
3372
|
http.once("error", reject);
|
|
2667
3373
|
http.listen(remote.port, "0.0.0.0");
|
|
2668
3374
|
});
|
|
3375
|
+
const secure = new WebSocketServer({ server: http });
|
|
3376
|
+
secure.on("connection", (socket) => secureHandshake(socket, spec, admission, register));
|
|
2669
3377
|
const { port } = http.address();
|
|
2670
3378
|
const url = `ws://127.0.0.1:${port}`;
|
|
2671
3379
|
const advertiseUrl = remote.publicUrl?.replace(/\/+$/, "") ?? `http://${pickLanAddress(networkInterfaces()) ?? "127.0.0.1"}:${port}`;
|
|
@@ -2675,8 +3383,8 @@ async function startRemote(options, admission, register) {
|
|
|
2675
3383
|
async close() {
|
|
2676
3384
|
for (const socket of secure.clients)
|
|
2677
3385
|
socket.terminate();
|
|
2678
|
-
await new Promise((
|
|
2679
|
-
await new Promise((
|
|
3386
|
+
await new Promise((resolve6) => secure.close(() => resolve6()));
|
|
3387
|
+
await new Promise((resolve6) => http.close(() => resolve6()));
|
|
2680
3388
|
}
|
|
2681
3389
|
};
|
|
2682
3390
|
}
|
|
@@ -2742,6 +3450,7 @@ class ConversationList {
|
|
|
2742
3450
|
if (typeof subagent?.role === "string" && subagent.role !== "")
|
|
2743
3451
|
facts.role = subagent.role;
|
|
2744
3452
|
}
|
|
3453
|
+
facts.agent = roleNameOf(await harness.snapshot(RoleDoc, record.id, context3));
|
|
2745
3454
|
const named = (await harness.snapshot(TitleDoc, record.id, context3))?.title;
|
|
2746
3455
|
if (typeof named === "string" && named !== "")
|
|
2747
3456
|
facts.named = named;
|
|
@@ -2798,6 +3507,9 @@ class ConversationList {
|
|
|
2798
3507
|
next.delete(change.record.kind);
|
|
2799
3508
|
facts.pending = next;
|
|
2800
3509
|
touched.add(id);
|
|
3510
|
+
} else if (change.record.kind === RoleDoc.definition.kind) {
|
|
3511
|
+
facts.agent = roleNameOf(change.value);
|
|
3512
|
+
touched.add(id);
|
|
2801
3513
|
} else if (change.record.kind === SubagentDoc.definition.kind) {
|
|
2802
3514
|
const value = change.value;
|
|
2803
3515
|
facts.label = typeof value?.label === "string" && value.label !== "" ? value.label : undefined;
|
|
@@ -2830,7 +3542,10 @@ class ConversationList {
|
|
|
2830
3542
|
}
|
|
2831
3543
|
#summaryOf(record) {
|
|
2832
3544
|
const facts = this.#fact(record.id);
|
|
2833
|
-
|
|
3545
|
+
if (record.owner !== undefined)
|
|
3546
|
+
return summaryOf(record, facts, facts.role, facts.role ?? null);
|
|
3547
|
+
const role = topLevelRole(record, this.#records, this.#roles);
|
|
3548
|
+
return summaryOf(record, facts, role, (facts.agent === undefined ? role : facts.agent) ?? null);
|
|
2834
3549
|
}
|
|
2835
3550
|
#refreshSummaries(touched) {
|
|
2836
3551
|
let next;
|
|
@@ -2881,6 +3596,11 @@ class ConversationList {
|
|
|
2881
3596
|
this.#listeners.clear();
|
|
2882
3597
|
}
|
|
2883
3598
|
}
|
|
3599
|
+
function roleNameOf(value) {
|
|
3600
|
+
if (value === null || value === undefined)
|
|
3601
|
+
return;
|
|
3602
|
+
return typeof value.name === "string" ? value.name : value.name === null ? null : undefined;
|
|
3603
|
+
}
|
|
2884
3604
|
function rolesOf(index) {
|
|
2885
3605
|
const roles = new Map;
|
|
2886
3606
|
for (const entry of index?.conversations ?? []) {
|
|
@@ -2900,7 +3620,7 @@ function topLevelRole(record, records, roles) {
|
|
|
2900
3620
|
}
|
|
2901
3621
|
return;
|
|
2902
3622
|
}
|
|
2903
|
-
function summaryOf(record, facts, role) {
|
|
3623
|
+
function summaryOf(record, facts, role, currentAgent) {
|
|
2904
3624
|
const parent = record.parent?.conversationId ?? record.owner?.conversationId;
|
|
2905
3625
|
const status = facts.pending.size > 0 ? "needs-answer" : facts.running === true ? "running" : facts.failed === true ? "failed" : undefined;
|
|
2906
3626
|
const runningSlot = facts.running === true ? facts.tools?.find((slot) => slot.status === "running") : undefined;
|
|
@@ -2918,18 +3638,18 @@ function summaryOf(record, facts, role) {
|
|
|
2918
3638
|
...facts.updatedAt === undefined ? {} : { updatedAt: facts.updatedAt },
|
|
2919
3639
|
...facts.label === undefined ? {} : { label: facts.label },
|
|
2920
3640
|
...role === undefined ? {} : { role },
|
|
3641
|
+
currentAgent,
|
|
2921
3642
|
...activity === undefined ? {} : { activity }
|
|
2922
3643
|
};
|
|
2923
3644
|
}
|
|
2924
3645
|
function sameSummary(a, b) {
|
|
2925
|
-
return a.id === b.id && a.kind === b.kind && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
3646
|
+
return a.id === b.id && a.kind === b.kind && a.parent === b.parent && a.forkedAt === b.forkedAt && a.title === b.title && a.status === b.status && a.updatedAt === b.updatedAt && a.label === b.label && a.role === b.role && a.currentAgent === b.currentAgent && a.activity?.tool === b.activity?.tool && a.activity?.target === b.activity?.target;
|
|
2926
3647
|
}
|
|
2927
3648
|
function titleOf(entry) {
|
|
2928
3649
|
const message = entry?.model?.[0];
|
|
2929
3650
|
if (message?.role !== "user")
|
|
2930
3651
|
return {};
|
|
2931
|
-
const
|
|
2932
|
-
const title = text.replace(/\s+/g, " ").trim();
|
|
3652
|
+
const title = stripUserBlocks(message.content).text.replace(/\s+/g, " ").trim();
|
|
2933
3653
|
return title === "" ? {} : { title };
|
|
2934
3654
|
}
|
|
2935
3655
|
async function historyFacts(harness, id) {
|
|
@@ -2974,13 +3694,15 @@ class GatewayClient {
|
|
|
2974
3694
|
#hooks;
|
|
2975
3695
|
#options;
|
|
2976
3696
|
#files;
|
|
3697
|
+
#skills;
|
|
2977
3698
|
#hostValues;
|
|
2978
3699
|
#subscriptions = new Map;
|
|
2979
3700
|
#disposed = false;
|
|
2980
|
-
constructor(connection, options, conversations, files, hooks) {
|
|
3701
|
+
constructor(connection, options, conversations, files, skills, hooks) {
|
|
2981
3702
|
this.#connection = connection;
|
|
2982
3703
|
this.#options = options;
|
|
2983
3704
|
this.#files = files;
|
|
3705
|
+
this.#skills = skills;
|
|
2984
3706
|
this.#hostValues = {
|
|
2985
3707
|
conversations,
|
|
2986
3708
|
mcp: options.mcp ?? constantValue(null),
|
|
@@ -3045,15 +3767,15 @@ class GatewayClient {
|
|
|
3045
3767
|
}
|
|
3046
3768
|
async#receive(frame) {
|
|
3047
3769
|
if (frame.type === "subscribe")
|
|
3048
|
-
await this.#subscribe(frame.stream);
|
|
3770
|
+
await this.#subscribe(frame.stream, frame.images === "ref");
|
|
3049
3771
|
else if (frame.type === "unsubscribe")
|
|
3050
3772
|
await this.#unsubscribe(frame.stream);
|
|
3051
3773
|
else
|
|
3052
3774
|
await this.#answer(frame.id, frame.method, frame.args);
|
|
3053
3775
|
}
|
|
3054
|
-
async#subscribe(stream) {
|
|
3776
|
+
async#subscribe(stream, imageRefs) {
|
|
3055
3777
|
const previous = this.#subscriptions.get(stream);
|
|
3056
|
-
const state = {};
|
|
3778
|
+
const state = imageRefs && stream.startsWith("conversation:") ? { images: new ImageRefs } : {};
|
|
3057
3779
|
this.#subscriptions.set(stream, state);
|
|
3058
3780
|
const current = () => !this.#disposed && this.#subscriptions.get(stream) === state;
|
|
3059
3781
|
const send = (frame) => current() ? this.send(frame) : Promise.resolve();
|
|
@@ -3062,7 +3784,7 @@ class GatewayClient {
|
|
|
3062
3784
|
await previous?.subscription?.stop();
|
|
3063
3785
|
if (!current())
|
|
3064
3786
|
return;
|
|
3065
|
-
subscription = await this.#open(stream, send);
|
|
3787
|
+
subscription = await this.#open(stream, send, state);
|
|
3066
3788
|
} catch (error) {
|
|
3067
3789
|
if (current()) {
|
|
3068
3790
|
await send({ type: "ended", stream, reason: error instanceof Error ? error.message : String(error) });
|
|
@@ -3077,7 +3799,7 @@ class GatewayClient {
|
|
|
3077
3799
|
}
|
|
3078
3800
|
state.subscription = subscription;
|
|
3079
3801
|
}
|
|
3080
|
-
async#open(stream, send) {
|
|
3802
|
+
async#open(stream, send, state) {
|
|
3081
3803
|
const source = this.#hostValue(stream);
|
|
3082
3804
|
if (source !== undefined) {
|
|
3083
3805
|
send({ type: "snapshot", stream, value: source.value });
|
|
@@ -3102,7 +3824,7 @@ class GatewayClient {
|
|
|
3102
3824
|
return this.#openDoc(stream, stream.slice("doc:".length, at), Number(stream.slice(at + 1)), send);
|
|
3103
3825
|
}
|
|
3104
3826
|
const conversation = await this.#conversation(Number(stream.slice("conversation:".length)));
|
|
3105
|
-
return this.#forward(stream, await conversation.watch(context3), send);
|
|
3827
|
+
return this.#forward(stream, await conversation.watch(context3), send, state);
|
|
3106
3828
|
}
|
|
3107
3829
|
#hostValue(stream) {
|
|
3108
3830
|
return Object.hasOwn(this.#hostValues, stream) ? this.#hostValues[stream] : undefined;
|
|
@@ -3115,8 +3837,8 @@ class GatewayClient {
|
|
|
3115
3837
|
await this.#conversation(id);
|
|
3116
3838
|
const { harness } = this.#options;
|
|
3117
3839
|
let signalCreation;
|
|
3118
|
-
const created = new Promise((
|
|
3119
|
-
signalCreation =
|
|
3840
|
+
const created = new Promise((resolve6) => {
|
|
3841
|
+
signalCreation = resolve6;
|
|
3120
3842
|
});
|
|
3121
3843
|
let inner;
|
|
3122
3844
|
let stopped = false;
|
|
@@ -3164,11 +3886,60 @@ class GatewayClient {
|
|
|
3164
3886
|
}
|
|
3165
3887
|
};
|
|
3166
3888
|
}
|
|
3167
|
-
async#forward(stream, watch2, send) {
|
|
3168
|
-
|
|
3169
|
-
|
|
3889
|
+
async#forward(stream, watch2, send, state) {
|
|
3890
|
+
if (state?.images === undefined) {
|
|
3891
|
+
await send({ type: "snapshot", stream, value: watch2.value });
|
|
3892
|
+
watch2.start((_value, ops) => send({ type: "ops", stream, ops }));
|
|
3893
|
+
return watch2;
|
|
3894
|
+
}
|
|
3895
|
+
const images = state.images;
|
|
3896
|
+
let before = watch2.value;
|
|
3897
|
+
state.view = before;
|
|
3898
|
+
await send({ type: "snapshot", stream, value: await images.value(before) });
|
|
3899
|
+
watch2.start(async (value, ops) => {
|
|
3900
|
+
const sent = await images.ops(ops, before);
|
|
3901
|
+
before = value;
|
|
3902
|
+
state.view = before;
|
|
3903
|
+
return send({ type: "ops", stream, ops: sent });
|
|
3904
|
+
});
|
|
3170
3905
|
return watch2;
|
|
3171
3906
|
}
|
|
3907
|
+
#shownImage(ref) {
|
|
3908
|
+
for (const state of this.#subscriptions.values()) {
|
|
3909
|
+
if (state.images === undefined || state.view === undefined)
|
|
3910
|
+
continue;
|
|
3911
|
+
const image = state.images.image(state.view, ref);
|
|
3912
|
+
if (image !== undefined)
|
|
3913
|
+
return image;
|
|
3914
|
+
}
|
|
3915
|
+
return;
|
|
3916
|
+
}
|
|
3917
|
+
#attachmentStore() {
|
|
3918
|
+
const store = this.#options.attachments;
|
|
3919
|
+
if (store === undefined)
|
|
3920
|
+
throw new Error("Attachments are not available on this host");
|
|
3921
|
+
return store;
|
|
3922
|
+
}
|
|
3923
|
+
async#attachmentText(ids) {
|
|
3924
|
+
if (ids === undefined || ids.length === 0)
|
|
3925
|
+
return;
|
|
3926
|
+
const files = await this.#attachmentStore().use(ids);
|
|
3927
|
+
return files === undefined ? undefined : attachmentBlock(files);
|
|
3928
|
+
}
|
|
3929
|
+
#attached(images) {
|
|
3930
|
+
const resolved = images?.map((image, index) => {
|
|
3931
|
+
if ("data" in image)
|
|
3932
|
+
return image;
|
|
3933
|
+
const shown = this.#shownImage(image.ref);
|
|
3934
|
+
if (shown === undefined)
|
|
3935
|
+
throw new Error(`Image ${index + 1} is no longer available`);
|
|
3936
|
+
return shown;
|
|
3937
|
+
});
|
|
3938
|
+
const problem = imageProblem(resolved ?? []);
|
|
3939
|
+
if (problem !== undefined)
|
|
3940
|
+
throw new Error(problem);
|
|
3941
|
+
return resolved;
|
|
3942
|
+
}
|
|
3172
3943
|
async#unsubscribe(stream) {
|
|
3173
3944
|
const subscription = this.#subscriptions.get(stream);
|
|
3174
3945
|
this.#subscriptions.delete(stream);
|
|
@@ -3180,6 +3951,11 @@ class GatewayClient {
|
|
|
3180
3951
|
throw new Error(`Conversation ${id} does not exist`);
|
|
3181
3952
|
return conversation;
|
|
3182
3953
|
}
|
|
3954
|
+
async#compact(conversation, instructions) {
|
|
3955
|
+
const taskId = await conversation.compact(instructions, context3);
|
|
3956
|
+
this.#options.harness.waitForTask(taskId, context3).then((receipt) => void this.send({ type: "notice", level: "info", message: `Compaction ${receipt.state.outcome.status}.` }), () => {});
|
|
3957
|
+
return taskId;
|
|
3958
|
+
}
|
|
3183
3959
|
async#answer(id, method, args) {
|
|
3184
3960
|
try {
|
|
3185
3961
|
const value = await this.#call(method, args);
|
|
@@ -3195,16 +3971,37 @@ class GatewayClient {
|
|
|
3195
3971
|
case "removeProject":
|
|
3196
3972
|
await removeProject(this.#options.harness, args.path, context3);
|
|
3197
3973
|
return null;
|
|
3974
|
+
case "submit": {
|
|
3975
|
+
const input = args;
|
|
3976
|
+
const images = this.#attached(input.images);
|
|
3977
|
+
const conversation2 = await this.#conversation(input.conversationId);
|
|
3978
|
+
const attachments = await this.#attachmentText(input.attachments);
|
|
3979
|
+
const text = await this.#expandSkills(input.text, { kind: "conversation", conversationId: conversation2.id }, input.skills);
|
|
3980
|
+
const dismissed = await this.#dismissPendingQuestions(conversation2.id, context3);
|
|
3981
|
+
const submission = await conversation2.submit({
|
|
3982
|
+
type: "input",
|
|
3983
|
+
content: messageContent(text, images, attachments),
|
|
3984
|
+
whenBusy: dismissed ? "steer" : input.whenBusy,
|
|
3985
|
+
requestId: input.requestId
|
|
3986
|
+
}, context3);
|
|
3987
|
+
submission.wait(context3).then((settled) => {
|
|
3988
|
+
if (settled.status === "unanswered" && settled.reason !== "aborted") {
|
|
3989
|
+
this.send({ type: "notice", level: "error", message: `No answer: ${settled.reason}` });
|
|
3990
|
+
}
|
|
3991
|
+
}, () => {});
|
|
3992
|
+
return { submissionId: submission.id };
|
|
3993
|
+
}
|
|
3198
3994
|
case "createConversation": {
|
|
3199
3995
|
const create = args;
|
|
3200
|
-
const
|
|
3201
|
-
|
|
3202
|
-
|
|
3996
|
+
const images = this.#attached(create.images);
|
|
3997
|
+
const attachments = await this.#attachmentText(create.attachments);
|
|
3998
|
+
const text = await this.#expandSkills(create.text, create.home.kind === "chat" ? { kind: "chat" } : { kind: "project", path: create.home.path, checkout: create.checkout ?? "worktree" }, create.skills);
|
|
3203
3999
|
const conversationId = await createConversation(this.#options.harness, this.#options.dataDir, {
|
|
3204
4000
|
home: create.home,
|
|
3205
|
-
text
|
|
4001
|
+
text,
|
|
3206
4002
|
requestId: create.requestId,
|
|
3207
|
-
...
|
|
4003
|
+
...images === undefined ? {} : { images },
|
|
4004
|
+
...attachments === undefined ? {} : { attachmentText: attachments },
|
|
3208
4005
|
...create.checkout === undefined ? {} : { checkout: create.checkout },
|
|
3209
4006
|
...create.model === undefined ? {} : { model: create.model },
|
|
3210
4007
|
...create.thinkingLevel === undefined ? {} : { thinkingLevel: create.thinkingLevel },
|
|
@@ -3283,22 +4080,64 @@ class GatewayClient {
|
|
|
3283
4080
|
const input = args;
|
|
3284
4081
|
return listFiles(this.#options.harness, this.#options.dataDir, this.#files, input.scope, input.query, context3);
|
|
3285
4082
|
}
|
|
4083
|
+
case "listSkills": {
|
|
4084
|
+
const input = args;
|
|
4085
|
+
return listSkills(this.#options.harness, this.#skills, input.scope, context3);
|
|
4086
|
+
}
|
|
4087
|
+
case "image": {
|
|
4088
|
+
const image = this.#shownImage(args.ref);
|
|
4089
|
+
if (image === undefined)
|
|
4090
|
+
throw new Error("Unknown image");
|
|
4091
|
+
return image;
|
|
4092
|
+
}
|
|
4093
|
+
case "beginAttachment": {
|
|
4094
|
+
const input = args;
|
|
4095
|
+
const store = this.#attachmentStore();
|
|
4096
|
+
return { attachmentId: await store.begin(input.name, input.size) };
|
|
4097
|
+
}
|
|
4098
|
+
case "appendAttachment": {
|
|
4099
|
+
const input = args;
|
|
4100
|
+
const store = this.#attachmentStore();
|
|
4101
|
+
return await store.append(input.attachmentId, input.offset, input.data);
|
|
4102
|
+
}
|
|
3286
4103
|
}
|
|
3287
4104
|
const conversation = await this.#conversation(args.conversationId);
|
|
3288
4105
|
switch (method) {
|
|
3289
|
-
case "
|
|
3290
|
-
const {
|
|
3291
|
-
const
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
4106
|
+
case "withdrawQueued": {
|
|
4107
|
+
const { conversationId, submissionId } = args;
|
|
4108
|
+
const id = submissionId;
|
|
4109
|
+
const submission = await this.#options.harness.submission(id, context3);
|
|
4110
|
+
if (submission === undefined)
|
|
4111
|
+
throw new Error(`Submission ${submissionId} does not exist`);
|
|
4112
|
+
assertQueuedInput(await submission.status(context3), conversationId);
|
|
4113
|
+
await this.#options.harness.abortSubmission(id, context3, conversationId);
|
|
4114
|
+
const after = await this.#options.harness.submission(id, context3);
|
|
4115
|
+
if (after === undefined)
|
|
4116
|
+
throw new Error(`Submission ${submissionId} does not exist`);
|
|
4117
|
+
return { outcome: (await after.status(context3)).entry === undefined ? "withdrawn" : "sent" };
|
|
4118
|
+
}
|
|
4119
|
+
case "steerQueued": {
|
|
4120
|
+
const { conversationId, submissionId } = args;
|
|
4121
|
+
const id = submissionId;
|
|
4122
|
+
return await this.#options.harness.commit(async (tx) => {
|
|
4123
|
+
const record = await tx.submission(id);
|
|
4124
|
+
if (record === undefined)
|
|
4125
|
+
throw new Error(`Submission ${submissionId} does not exist`);
|
|
4126
|
+
assertQueuedInput(record, conversationId);
|
|
4127
|
+
if (record.entry !== undefined)
|
|
4128
|
+
return { outcome: "sent" };
|
|
4129
|
+
if (record.status !== "queued")
|
|
4130
|
+
return { outcome: "unavailable" };
|
|
4131
|
+
const inbox = await tx.doc(InboxDoc, conversationId);
|
|
4132
|
+
const item = inbox.items.find((candidate) => candidate.id === id);
|
|
4133
|
+
if (item === undefined) {
|
|
4134
|
+
throw new Error(`Submission ${submissionId} is not queued in the inbox`);
|
|
3299
4135
|
}
|
|
3300
|
-
|
|
3301
|
-
|
|
4136
|
+
await dismissPendingQuestions(tx, this.#questionDocs(), conversationId);
|
|
4137
|
+
if (item.mode === "followUp")
|
|
4138
|
+
item.mode = "steer";
|
|
4139
|
+
return { outcome: "steered" };
|
|
4140
|
+
}, context3);
|
|
3302
4141
|
}
|
|
3303
4142
|
case "renameConversation": {
|
|
3304
4143
|
const { title } = args;
|
|
@@ -3325,29 +4164,44 @@ class GatewayClient {
|
|
|
3325
4164
|
return null;
|
|
3326
4165
|
case "compact": {
|
|
3327
4166
|
const { instructions } = args;
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
4167
|
+
return { taskId: await this.#compact(conversation, instructions) };
|
|
4168
|
+
}
|
|
4169
|
+
case "listCommands":
|
|
4170
|
+
return listCommands();
|
|
4171
|
+
case "runCommand": {
|
|
4172
|
+
const { name, args: input } = args;
|
|
4173
|
+
await runCommand({ compact: (instructions) => this.#compact(conversation, instructions) }, name, input);
|
|
4174
|
+
return null;
|
|
3331
4175
|
}
|
|
3332
4176
|
case "setModel": {
|
|
3333
4177
|
const { model: ref } = args;
|
|
3334
4178
|
const model = this.#options.models.getModel(ref.provider, ref.modelId);
|
|
3335
4179
|
if (model === undefined)
|
|
3336
4180
|
throw new Error(`Unknown model: ${ref.provider}/${ref.modelId}`);
|
|
3337
|
-
|
|
3338
|
-
|
|
4181
|
+
await this.#options.harness.commit(async (tx) => {
|
|
4182
|
+
const state = await tx.doc(AgentDoc5, conversation.id);
|
|
4183
|
+
const thinking = state.thinkingLevel ?? "off";
|
|
4184
|
+
await configure3(tx, conversation.id, { model: ref, thinkingLevel: clampThinkingLevel3(model, thinking) });
|
|
4185
|
+
}, context3);
|
|
3339
4186
|
return null;
|
|
3340
4187
|
}
|
|
3341
4188
|
case "setThinkingLevel": {
|
|
3342
4189
|
const { level } = args;
|
|
3343
|
-
|
|
3344
|
-
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3348
|
-
|
|
3349
|
-
|
|
3350
|
-
|
|
4190
|
+
await this.#options.harness.commit(async (tx) => {
|
|
4191
|
+
const state = await tx.doc(AgentDoc5, conversation.id);
|
|
4192
|
+
const model = state.model === undefined ? undefined : this.#options.models.getModel(state.model.provider, state.model.modelId);
|
|
4193
|
+
if (model === undefined)
|
|
4194
|
+
throw new Error("No model selected");
|
|
4195
|
+
if (!getSupportedThinkingLevels(model).includes(level)) {
|
|
4196
|
+
throw new Error(`Thinking level ${level} is not supported by ${state.model.provider}/${state.model.modelId}`);
|
|
4197
|
+
}
|
|
4198
|
+
await configure3(tx, conversation.id, { thinkingLevel: level });
|
|
4199
|
+
}, context3);
|
|
4200
|
+
return null;
|
|
4201
|
+
}
|
|
4202
|
+
case "setAgent": {
|
|
4203
|
+
const { conversationId, agent } = args;
|
|
4204
|
+
await setAgent(this.#options.harness, { conversationId, agent }, this.#options.models, this.#options.modelSummaries, this.#options.agents?.value ?? { roles: [], problems: [] }, context3);
|
|
3351
4205
|
return null;
|
|
3352
4206
|
}
|
|
3353
4207
|
case "fork": {
|
|
@@ -3376,7 +4230,10 @@ class GatewayClient {
|
|
|
3376
4230
|
const remove = agent.tools.filter((tool) => removeTools.includes(tool.name));
|
|
3377
4231
|
const fork = await conversation.fork(at, {
|
|
3378
4232
|
ownership: { kind: "ownerless" },
|
|
3379
|
-
...remove.length === 0 ? {} : { agent: { tools: { remove } } }
|
|
4233
|
+
...remove.length === 0 ? {} : { agent: { tools: { remove } } },
|
|
4234
|
+
init: async (tx, id) => {
|
|
4235
|
+
await tx.doc(RoleDoc, id);
|
|
4236
|
+
}
|
|
3380
4237
|
}, context3);
|
|
3381
4238
|
return { conversationId: fork.id };
|
|
3382
4239
|
});
|
|
@@ -3420,30 +4277,47 @@ class GatewayClient {
|
|
|
3420
4277
|
}
|
|
3421
4278
|
}
|
|
3422
4279
|
}
|
|
4280
|
+
async#expandSkills(text, scope, bound) {
|
|
4281
|
+
if (bound === undefined ? !text.includes("$") : bound.length === 0)
|
|
4282
|
+
return text;
|
|
4283
|
+
let skills;
|
|
4284
|
+
try {
|
|
4285
|
+
skills = await scopeSkills(this.#options.harness, this.#skills, scope, context3);
|
|
4286
|
+
} catch (error) {
|
|
4287
|
+
this.send({
|
|
4288
|
+
type: "notice",
|
|
4289
|
+
level: "warning",
|
|
4290
|
+
message: `Skills were not expanded: ${error instanceof Error ? error.message : String(error)}`
|
|
4291
|
+
});
|
|
4292
|
+
return text;
|
|
4293
|
+
}
|
|
4294
|
+
const names = bound ?? mentionedSkillNames(text, skills.map((skill) => skill.name));
|
|
4295
|
+
const expanded = await expandSkills(text, names, skills);
|
|
4296
|
+
for (const name of expanded.missing) {
|
|
4297
|
+
this.send({ type: "notice", level: "warning", message: `Skill "${name}" is not available here; sent without it` });
|
|
4298
|
+
}
|
|
4299
|
+
for (const failure of expanded.failures) {
|
|
4300
|
+
this.send({
|
|
4301
|
+
type: "notice",
|
|
4302
|
+
level: "error",
|
|
4303
|
+
message: `Could not read skill "${failure.name}" (${failure.filePath}): ${failure.message}`
|
|
4304
|
+
});
|
|
4305
|
+
}
|
|
4306
|
+
return expanded.text;
|
|
4307
|
+
}
|
|
3423
4308
|
async#dismissPendingQuestions(conversationId, context4) {
|
|
3424
|
-
const questionDocs =
|
|
4309
|
+
const questionDocs = this.#questionDocs();
|
|
3425
4310
|
const snapshots = await Promise.all(questionDocs.map((doc) => this.#options.harness.snapshot(doc.token, conversationId, context4)));
|
|
3426
4311
|
const pending = snapshots.some((value) => Value2.Check(QuestionSchema, value) && value.requests.some((request) => request.resolution === undefined));
|
|
3427
4312
|
if (!pending)
|
|
3428
4313
|
return false;
|
|
3429
|
-
return await this.#options.harness.commit(
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
if (!Value2.Check(QuestionSchema, value))
|
|
3434
|
-
continue;
|
|
3435
|
-
for (const request of value.requests) {
|
|
3436
|
-
if (request.resolution === undefined) {
|
|
3437
|
-
request.resolution = { outcome: "dismissed", at: Date.now() };
|
|
3438
|
-
dismissed = true;
|
|
3439
|
-
}
|
|
3440
|
-
}
|
|
3441
|
-
}
|
|
3442
|
-
return dismissed;
|
|
3443
|
-
}, context4);
|
|
4314
|
+
return await this.#options.harness.commit((tx) => dismissPendingQuestions(tx, questionDocs, conversationId), context4);
|
|
4315
|
+
}
|
|
4316
|
+
#questionDocs() {
|
|
4317
|
+
return (this.#options.docs ?? []).filter((doc) => doc.presentation === "pinomad.question");
|
|
3444
4318
|
}
|
|
3445
4319
|
get #chatsPrefix() {
|
|
3446
|
-
return
|
|
4320
|
+
return join8(this.#options.dataDir, "chats") + sep5;
|
|
3447
4321
|
}
|
|
3448
4322
|
async#changes(conversation, context4) {
|
|
3449
4323
|
const cwd = (await conversation.agent(context4)).cwd;
|
|
@@ -3456,15 +4330,13 @@ class GatewayClient {
|
|
|
3456
4330
|
if (record !== undefined) {
|
|
3457
4331
|
if (!await worktreeExists(record.path))
|
|
3458
4332
|
return { available: false, reason: "No changes yet" };
|
|
3459
|
-
|
|
4333
|
+
const [changes, branch2] = await Promise.all([changesOf(record.path, record.base), branchOf(record.path)]);
|
|
4334
|
+
return { available: true, ...changes, branch: branch2 };
|
|
3460
4335
|
}
|
|
3461
|
-
const base = await gitBase(cwd);
|
|
4336
|
+
const [base, branch] = await Promise.all([gitBase(cwd), branchOf(cwd)]);
|
|
3462
4337
|
if (base === undefined)
|
|
3463
|
-
return { available: false, reason: "Not in a git repository" };
|
|
3464
|
-
return { available: true, ...await changesOf(base.repo, base.base) };
|
|
3465
|
-
}
|
|
3466
|
-
async#agent(id) {
|
|
3467
|
-
return await this.#options.harness.snapshot(AgentDoc4, id, context3) ?? {};
|
|
4338
|
+
return { available: false, reason: "Not in a git repository", branch };
|
|
4339
|
+
return { available: true, ...await changesOf(base.repo, base.base), branch };
|
|
3468
4340
|
}
|
|
3469
4341
|
async dispose() {
|
|
3470
4342
|
if (this.#disposed)
|
|
@@ -3477,14 +4349,323 @@ class GatewayClient {
|
|
|
3477
4349
|
}
|
|
3478
4350
|
}
|
|
3479
4351
|
|
|
4352
|
+
// apps/host/src/attachments.ts
|
|
4353
|
+
import { randomBytes as randomBytes3 } from "node:crypto";
|
|
4354
|
+
import {
|
|
4355
|
+
chmod,
|
|
4356
|
+
lstat,
|
|
4357
|
+
mkdir as mkdir3,
|
|
4358
|
+
open,
|
|
4359
|
+
readFile as readFile4,
|
|
4360
|
+
readdir as readdir3,
|
|
4361
|
+
rename as rename2,
|
|
4362
|
+
rm as rm3,
|
|
4363
|
+
stat as stat6,
|
|
4364
|
+
writeFile as writeFile2
|
|
4365
|
+
} from "node:fs/promises";
|
|
4366
|
+
import { basename as basename2, join as join9, resolve as resolve6 } from "node:path";
|
|
4367
|
+
var META = "meta.json";
|
|
4368
|
+
var DATA = "file";
|
|
4369
|
+
var CLEANUP_INTERVAL_MS = 6 * 60 * 60 * 1000;
|
|
4370
|
+
var FILE_NAME_BYTES = 200;
|
|
4371
|
+
function metaOf(value, id) {
|
|
4372
|
+
if (value === null || typeof value !== "object")
|
|
4373
|
+
return;
|
|
4374
|
+
const meta = value;
|
|
4375
|
+
if (meta.v !== 1)
|
|
4376
|
+
return;
|
|
4377
|
+
if (typeof meta.name !== "string" || typeof meta.fileName !== "string")
|
|
4378
|
+
return;
|
|
4379
|
+
if (typeof meta.size !== "number" || !Number.isSafeInteger(meta.size) || meta.size < 0)
|
|
4380
|
+
return;
|
|
4381
|
+
if (meta.state !== "uploading" && meta.state !== "complete" && meta.state !== "used")
|
|
4382
|
+
return;
|
|
4383
|
+
if (typeof meta.createdAt !== "number" || typeof meta.expiresAt !== "number")
|
|
4384
|
+
return;
|
|
4385
|
+
if (meta.fileName === "" || basename2(meta.fileName) !== meta.fileName || meta.fileName === "." || meta.fileName === "..") {
|
|
4386
|
+
return;
|
|
4387
|
+
}
|
|
4388
|
+
return { ...meta, id };
|
|
4389
|
+
}
|
|
4390
|
+
function displayName2(name) {
|
|
4391
|
+
const chars = Array.from(name);
|
|
4392
|
+
return chars.length <= MAX_ATTACHMENT_NAME_LENGTH ? name : chars.slice(0, MAX_ATTACHMENT_NAME_LENGTH).join("");
|
|
4393
|
+
}
|
|
4394
|
+
function truncateUtf8(text, limit) {
|
|
4395
|
+
let out = "";
|
|
4396
|
+
let bytes = 0;
|
|
4397
|
+
for (const char of text) {
|
|
4398
|
+
const size = Buffer.byteLength(char, "utf8");
|
|
4399
|
+
if (bytes + size > limit)
|
|
4400
|
+
break;
|
|
4401
|
+
out += char;
|
|
4402
|
+
bytes += size;
|
|
4403
|
+
}
|
|
4404
|
+
return out;
|
|
4405
|
+
}
|
|
4406
|
+
function attachmentFileName(name) {
|
|
4407
|
+
let clean = (name.split(/[\\/]/).pop() ?? "").replace(/[\u0000-\u001f\u007f]/g, "_").trim();
|
|
4408
|
+
clean = clean.replace(/^\.+/, "");
|
|
4409
|
+
if (clean === "")
|
|
4410
|
+
return "file";
|
|
4411
|
+
const dot = clean.lastIndexOf(".");
|
|
4412
|
+
const rawExtension = dot > 0 ? clean.slice(dot) : "";
|
|
4413
|
+
const extension = Buffer.byteLength(rawExtension, "utf8") <= 32 ? rawExtension : "";
|
|
4414
|
+
const stem = extension === "" ? clean : clean.slice(0, -extension.length);
|
|
4415
|
+
const extensionBytes = Buffer.byteLength(extension, "utf8");
|
|
4416
|
+
const keptStem = truncateUtf8(stem, Math.max(0, FILE_NAME_BYTES - extensionBytes));
|
|
4417
|
+
const trimmedStem = keptStem.replace(/[\s.]+$/u, "");
|
|
4418
|
+
const candidate = trimmedStem + (trimmedStem === "" ? "" : extension);
|
|
4419
|
+
return candidate === "" ? "file" : candidate;
|
|
4420
|
+
}
|
|
4421
|
+
|
|
4422
|
+
class AttachmentStore {
|
|
4423
|
+
#root;
|
|
4424
|
+
#now;
|
|
4425
|
+
#ttlMs;
|
|
4426
|
+
#pendingLimit;
|
|
4427
|
+
#pendingBytesLimit;
|
|
4428
|
+
#cleanupIntervalMs;
|
|
4429
|
+
#lock = Promise.resolve();
|
|
4430
|
+
#pending = new Map;
|
|
4431
|
+
#timer;
|
|
4432
|
+
constructor(options) {
|
|
4433
|
+
this.#root = resolve6(options.dir);
|
|
4434
|
+
this.#now = options.now ?? Date.now;
|
|
4435
|
+
this.#ttlMs = options.ttlMs ?? ATTACHMENT_TTL_MS;
|
|
4436
|
+
this.#pendingLimit = options.pendingLimit ?? MAX_PENDING_ATTACHMENTS;
|
|
4437
|
+
this.#pendingBytesLimit = options.pendingBytesLimit ?? MAX_PENDING_ATTACHMENT_BYTES;
|
|
4438
|
+
this.#cleanupIntervalMs = options.cleanupIntervalMs ?? CLEANUP_INTERVAL_MS;
|
|
4439
|
+
}
|
|
4440
|
+
get root() {
|
|
4441
|
+
return this.#root;
|
|
4442
|
+
}
|
|
4443
|
+
async open() {
|
|
4444
|
+
await mkdir3(this.#root, { recursive: true });
|
|
4445
|
+
await this.#rebuildPending();
|
|
4446
|
+
await this.sweep();
|
|
4447
|
+
this.#timer = setInterval(() => void this.sweep().catch(() => {}), this.#cleanupIntervalMs);
|
|
4448
|
+
this.#timer.unref();
|
|
4449
|
+
}
|
|
4450
|
+
close() {
|
|
4451
|
+
if (this.#timer !== undefined)
|
|
4452
|
+
clearInterval(this.#timer);
|
|
4453
|
+
this.#timer = undefined;
|
|
4454
|
+
}
|
|
4455
|
+
#run(work) {
|
|
4456
|
+
const next = this.#lock.then(work, work);
|
|
4457
|
+
this.#lock = next.then(() => {}, () => {});
|
|
4458
|
+
return next;
|
|
4459
|
+
}
|
|
4460
|
+
#directory(id) {
|
|
4461
|
+
return join9(this.#root, id);
|
|
4462
|
+
}
|
|
4463
|
+
#dataPath(meta) {
|
|
4464
|
+
return join9(this.#directory(meta.id), DATA, meta.fileName);
|
|
4465
|
+
}
|
|
4466
|
+
async#readMeta(id) {
|
|
4467
|
+
if (!isAttachmentId(id))
|
|
4468
|
+
return;
|
|
4469
|
+
try {
|
|
4470
|
+
return metaOf(JSON.parse(await readFile4(join9(this.#directory(id), META), "utf8")), id);
|
|
4471
|
+
} catch {
|
|
4472
|
+
return;
|
|
4473
|
+
}
|
|
4474
|
+
}
|
|
4475
|
+
async#writeMeta(meta) {
|
|
4476
|
+
const { id, ...value } = meta;
|
|
4477
|
+
const directory = this.#directory(id);
|
|
4478
|
+
const temporary = join9(directory, `.meta.${randomBytes3(6).toString("hex")}.tmp`);
|
|
4479
|
+
await writeFile2(temporary, `${JSON.stringify(value)}
|
|
4480
|
+
`, { mode: 384 });
|
|
4481
|
+
await rename2(temporary, join9(directory, META));
|
|
4482
|
+
}
|
|
4483
|
+
async#rebuildPending() {
|
|
4484
|
+
this.#pending.clear();
|
|
4485
|
+
let entries;
|
|
4486
|
+
try {
|
|
4487
|
+
entries = await readdir3(this.#root, { withFileTypes: true });
|
|
4488
|
+
} catch {
|
|
4489
|
+
return;
|
|
4490
|
+
}
|
|
4491
|
+
for (const entry of entries) {
|
|
4492
|
+
if (!entry.isDirectory())
|
|
4493
|
+
continue;
|
|
4494
|
+
const meta = await this.#readMeta(entry.name);
|
|
4495
|
+
if (meta !== undefined && meta.state !== "used")
|
|
4496
|
+
this.#pending.set(meta.id, meta.size);
|
|
4497
|
+
}
|
|
4498
|
+
}
|
|
4499
|
+
begin(name, size) {
|
|
4500
|
+
return this.#run(async () => {
|
|
4501
|
+
const problem = attachmentBeginProblem(name, size);
|
|
4502
|
+
if (problem !== undefined)
|
|
4503
|
+
throw new Error(problem);
|
|
4504
|
+
let pendingBytes = 0;
|
|
4505
|
+
for (const bytes of this.#pending.values())
|
|
4506
|
+
pendingBytes += bytes;
|
|
4507
|
+
if (this.#pending.size + 1 > this.#pendingLimit) {
|
|
4508
|
+
throw new Error(`Too many pending attachments (${this.#pendingLimit} at most); try again after unused uploads expire`);
|
|
4509
|
+
}
|
|
4510
|
+
if (pendingBytes + size > this.#pendingBytesLimit) {
|
|
4511
|
+
throw new Error(`Pending attachments exceed the ${this.#pendingBytesLimit / (1024 * 1024)} MB budget; try again after unused uploads expire`);
|
|
4512
|
+
}
|
|
4513
|
+
const id = randomBytes3(16).toString("base64url");
|
|
4514
|
+
const directory = this.#directory(id);
|
|
4515
|
+
const meta = {
|
|
4516
|
+
id,
|
|
4517
|
+
v: 1,
|
|
4518
|
+
name: displayName2(name),
|
|
4519
|
+
fileName: attachmentFileName(name),
|
|
4520
|
+
size,
|
|
4521
|
+
state: size === 0 ? "complete" : "uploading",
|
|
4522
|
+
createdAt: this.#now(),
|
|
4523
|
+
expiresAt: this.#now() + this.#ttlMs
|
|
4524
|
+
};
|
|
4525
|
+
try {
|
|
4526
|
+
await mkdir3(join9(directory, DATA), { recursive: true });
|
|
4527
|
+
await writeFile2(this.#dataPath(meta), new Uint8Array(0), { mode: 384 });
|
|
4528
|
+
if (size === 0)
|
|
4529
|
+
await chmod(this.#dataPath(meta), 292);
|
|
4530
|
+
await this.#writeMeta(meta);
|
|
4531
|
+
} catch (error) {
|
|
4532
|
+
await rm3(directory, { recursive: true, force: true }).catch(() => {});
|
|
4533
|
+
throw error;
|
|
4534
|
+
}
|
|
4535
|
+
this.#pending.set(id, size);
|
|
4536
|
+
return id;
|
|
4537
|
+
});
|
|
4538
|
+
}
|
|
4539
|
+
append(id, offset, data) {
|
|
4540
|
+
return this.#run(async () => {
|
|
4541
|
+
const meta = await this.#readMeta(id);
|
|
4542
|
+
if (meta === undefined)
|
|
4543
|
+
throw new Error("Attachment not found or expired; attach it again");
|
|
4544
|
+
if (meta.state === "complete" || meta.state === "used") {
|
|
4545
|
+
return { received: meta.size, complete: true };
|
|
4546
|
+
}
|
|
4547
|
+
const problem = attachmentChunkProblem(data);
|
|
4548
|
+
if (problem !== undefined)
|
|
4549
|
+
throw new Error(problem);
|
|
4550
|
+
const decoded = base64DecodedLength(data);
|
|
4551
|
+
const path = this.#dataPath(meta);
|
|
4552
|
+
const received = (await stat6(path)).size;
|
|
4553
|
+
if (received === meta.size) {
|
|
4554
|
+
await chmod(path, 292);
|
|
4555
|
+
await this.#writeMeta({ ...meta, state: "complete" });
|
|
4556
|
+
return { received, complete: true };
|
|
4557
|
+
}
|
|
4558
|
+
if (offset !== received)
|
|
4559
|
+
return { received, complete: false };
|
|
4560
|
+
if (received + decoded > meta.size)
|
|
4561
|
+
throw new Error("Attachment chunk is larger than the remaining size");
|
|
4562
|
+
const bytes = Buffer.from(data, "base64");
|
|
4563
|
+
const handle = await open(path, "r+");
|
|
4564
|
+
try {
|
|
4565
|
+
const { bytesWritten } = await handle.write(bytes, 0, bytes.length, received);
|
|
4566
|
+
if (bytesWritten !== bytes.length)
|
|
4567
|
+
throw new Error("Could not write the whole attachment chunk");
|
|
4568
|
+
} finally {
|
|
4569
|
+
await handle.close();
|
|
4570
|
+
}
|
|
4571
|
+
const after = received + bytes.length;
|
|
4572
|
+
if (after === meta.size) {
|
|
4573
|
+
await chmod(path, 292);
|
|
4574
|
+
await this.#writeMeta({ ...meta, state: "complete" });
|
|
4575
|
+
}
|
|
4576
|
+
return { received: after, complete: after === meta.size };
|
|
4577
|
+
});
|
|
4578
|
+
}
|
|
4579
|
+
use(ids) {
|
|
4580
|
+
return this.#run(async () => {
|
|
4581
|
+
if (ids === undefined || ids.length === 0)
|
|
4582
|
+
return;
|
|
4583
|
+
if (ids.length > MAX_ATTACHMENTS_PER_MESSAGE) {
|
|
4584
|
+
throw new Error(`At most ${MAX_ATTACHMENTS_PER_MESSAGE} attachments per message`);
|
|
4585
|
+
}
|
|
4586
|
+
const checked = [];
|
|
4587
|
+
for (let index = 0;index < ids.length; index++) {
|
|
4588
|
+
const id = ids[index];
|
|
4589
|
+
const meta = await this.#readMeta(id);
|
|
4590
|
+
if (meta === undefined)
|
|
4591
|
+
throw new Error(`Attachment ${index + 1} has expired; attach it again`);
|
|
4592
|
+
if (meta.state === "uploading")
|
|
4593
|
+
throw new Error(`Attachment ${index + 1} is still uploading`);
|
|
4594
|
+
const path = this.#dataPath(meta);
|
|
4595
|
+
let info;
|
|
4596
|
+
try {
|
|
4597
|
+
info = await lstat(path);
|
|
4598
|
+
} catch {
|
|
4599
|
+
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
4600
|
+
}
|
|
4601
|
+
if (!info.isFile() || info.size !== meta.size) {
|
|
4602
|
+
throw new Error(`Attachment ${index + 1} is unavailable; attach it again`);
|
|
4603
|
+
}
|
|
4604
|
+
checked.push({ meta, path });
|
|
4605
|
+
}
|
|
4606
|
+
for (const { meta } of checked) {
|
|
4607
|
+
if (meta.state === "used") {
|
|
4608
|
+
this.#pending.delete(meta.id);
|
|
4609
|
+
continue;
|
|
4610
|
+
}
|
|
4611
|
+
await this.#writeMeta({ ...meta, state: "used" });
|
|
4612
|
+
this.#pending.delete(meta.id);
|
|
4613
|
+
}
|
|
4614
|
+
return checked.map(({ meta, path }) => ({ id: meta.id, name: meta.name, path, size: meta.size }));
|
|
4615
|
+
});
|
|
4616
|
+
}
|
|
4617
|
+
sweep() {
|
|
4618
|
+
return this.#run(async () => {
|
|
4619
|
+
let removed = 0;
|
|
4620
|
+
let entries;
|
|
4621
|
+
try {
|
|
4622
|
+
entries = await readdir3(this.#root, { withFileTypes: true });
|
|
4623
|
+
} catch {
|
|
4624
|
+
return 0;
|
|
4625
|
+
}
|
|
4626
|
+
const now = this.#now();
|
|
4627
|
+
for (const entry of entries) {
|
|
4628
|
+
if (!entry.isDirectory())
|
|
4629
|
+
continue;
|
|
4630
|
+
const directory = this.#directory(entry.name);
|
|
4631
|
+
const meta = await this.#readMeta(entry.name);
|
|
4632
|
+
if (meta !== undefined) {
|
|
4633
|
+
if (meta.state === "used") {
|
|
4634
|
+
this.#pending.delete(meta.id);
|
|
4635
|
+
continue;
|
|
4636
|
+
}
|
|
4637
|
+
if (meta.expiresAt > now) {
|
|
4638
|
+
this.#pending.set(meta.id, meta.size);
|
|
4639
|
+
continue;
|
|
4640
|
+
}
|
|
4641
|
+
} else {
|
|
4642
|
+
this.#pending.delete(entry.name);
|
|
4643
|
+
try {
|
|
4644
|
+
if (now - (await stat6(directory)).mtimeMs <= this.#ttlMs)
|
|
4645
|
+
continue;
|
|
4646
|
+
} catch {
|
|
4647
|
+
continue;
|
|
4648
|
+
}
|
|
4649
|
+
}
|
|
4650
|
+
try {
|
|
4651
|
+
await rm3(directory, { recursive: true, force: true });
|
|
4652
|
+
this.#pending.delete(entry.name);
|
|
4653
|
+
removed += 1;
|
|
4654
|
+
} catch {}
|
|
4655
|
+
}
|
|
4656
|
+
return removed;
|
|
4657
|
+
});
|
|
4658
|
+
}
|
|
4659
|
+
}
|
|
4660
|
+
|
|
3480
4661
|
// apps/host/src/host.ts
|
|
3481
4662
|
var context4 = BACKGROUND_CONTEXT4;
|
|
3482
4663
|
async function hostToken(dataDir) {
|
|
3483
|
-
const path =
|
|
4664
|
+
const path = join10(dataDir, "token");
|
|
3484
4665
|
try {
|
|
3485
|
-
return (await
|
|
4666
|
+
return (await readFile5(path, "utf8")).trim();
|
|
3486
4667
|
} catch {
|
|
3487
|
-
const token =
|
|
4668
|
+
const token = randomBytes4(24).toString("base64url");
|
|
3488
4669
|
await writeFile3(path, `${token}
|
|
3489
4670
|
`, { mode: 384 });
|
|
3490
4671
|
return token;
|
|
@@ -3503,13 +4684,16 @@ async function openHost(options) {
|
|
|
3503
4684
|
let harness;
|
|
3504
4685
|
let mcp;
|
|
3505
4686
|
let agentRoles;
|
|
4687
|
+
let attachments;
|
|
3506
4688
|
let extensions = [];
|
|
3507
4689
|
try {
|
|
3508
4690
|
const token = await hostToken(options.dataDir);
|
|
4691
|
+
attachments = new AttachmentStore({ dir: join10(options.dataDir, "attachments") });
|
|
4692
|
+
await attachments.open();
|
|
3509
4693
|
const reports = [];
|
|
3510
4694
|
let report = (error) => void reports.push(error);
|
|
3511
4695
|
const registry = createRegistry();
|
|
3512
|
-
agentRoles = await openAgentRoles({ dir:
|
|
4696
|
+
agentRoles = await openAgentRoles({ dir: join10(options.dataDir, "agents") });
|
|
3513
4697
|
extensions = typeof options.extensions === "function" ? options.extensions({
|
|
3514
4698
|
harness: () => {
|
|
3515
4699
|
if (harness === undefined)
|
|
@@ -3532,13 +4716,13 @@ async function openHost(options) {
|
|
|
3532
4716
|
}
|
|
3533
4717
|
});
|
|
3534
4718
|
const hostSettings = await openHostSettings({
|
|
3535
|
-
path:
|
|
4719
|
+
path: join10(options.dataDir, "settings.json"),
|
|
3536
4720
|
fallback: options.settingsFallback,
|
|
3537
4721
|
models: options.modelAuth?.models ?? constantValue(options.modelSummaries())
|
|
3538
4722
|
});
|
|
3539
4723
|
const environments = new Map;
|
|
3540
|
-
const chatsDir =
|
|
3541
|
-
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(
|
|
4724
|
+
const chatsDir = join10(options.dataDir, "chats");
|
|
4725
|
+
harness = await Harness.open(await (options.openStorage ?? openNodeSqliteStorage)(join10(options.dataDir, "session.sqlite")), {
|
|
3542
4726
|
models: options.models,
|
|
3543
4727
|
registry,
|
|
3544
4728
|
env: async (target) => {
|
|
@@ -3546,7 +4730,7 @@ async function openHost(options) {
|
|
|
3546
4730
|
if (cwd === undefined)
|
|
3547
4731
|
return;
|
|
3548
4732
|
const worktrees = worktreeRoot(options.dataDir);
|
|
3549
|
-
if (cwd === worktrees || cwd.startsWith(worktrees +
|
|
4733
|
+
if (cwd === worktrees || cwd.startsWith(worktrees + sep6)) {
|
|
3550
4734
|
const index = await target.read.snapshot(IndexDoc, context4);
|
|
3551
4735
|
const record = checkoutAt(index?.checkouts, cwd);
|
|
3552
4736
|
if (record !== undefined) {
|
|
@@ -3579,8 +4763,10 @@ async function openHost(options) {
|
|
|
3579
4763
|
harness,
|
|
3580
4764
|
models: options.models,
|
|
3581
4765
|
modelSummaries: options.modelSummaries,
|
|
3582
|
-
session: { id:
|
|
4766
|
+
session: { id: basename3(options.dataDir), directory: options.dataDir },
|
|
3583
4767
|
dataDir: options.dataDir,
|
|
4768
|
+
attachments,
|
|
4769
|
+
agentsHome: options.agentsHome,
|
|
3584
4770
|
settings: hostSettings,
|
|
3585
4771
|
token,
|
|
3586
4772
|
port: options.port,
|
|
@@ -3645,6 +4831,7 @@ async function openHost(options) {
|
|
|
3645
4831
|
await gateway.close();
|
|
3646
4832
|
await opened.close(context4);
|
|
3647
4833
|
await mcp?.close();
|
|
4834
|
+
attachments?.close();
|
|
3648
4835
|
agentRoles?.close();
|
|
3649
4836
|
for (const each of extensions)
|
|
3650
4837
|
await each.close?.();
|
|
@@ -3659,6 +4846,7 @@ async function openHost(options) {
|
|
|
3659
4846
|
};
|
|
3660
4847
|
} catch (error) {
|
|
3661
4848
|
options.modelAuth?.close();
|
|
4849
|
+
attachments?.close();
|
|
3662
4850
|
agentRoles?.close();
|
|
3663
4851
|
for (const each of extensions)
|
|
3664
4852
|
await each.close?.().catch(() => {});
|
|
@@ -3669,9 +4857,71 @@ async function openHost(options) {
|
|
|
3669
4857
|
}
|
|
3670
4858
|
}
|
|
3671
4859
|
|
|
4860
|
+
// apps/host/src/service/log-rotation.ts
|
|
4861
|
+
import { copyFile, rename as rename3, rm as rm4, stat as stat7, truncate } from "node:fs/promises";
|
|
4862
|
+
import { join as join11 } from "node:path";
|
|
4863
|
+
var HOST_LOG_MAX_BYTES = 10 * 1024 * 1024;
|
|
4864
|
+
var HOST_LOG_HISTORY = 3;
|
|
4865
|
+
var HOST_LOG_CHECK_INTERVAL_MS = 10 * 60 * 1000;
|
|
4866
|
+
var hostLogPath = (dataDir) => join11(dataDir, "logs", "host.log");
|
|
4867
|
+
var rotationTempPath = (log) => `${log}.rotating`;
|
|
4868
|
+
var isMissing = (error) => error.code === "ENOENT";
|
|
4869
|
+
async function rotateHostLog(dataDir, copy = copyFile) {
|
|
4870
|
+
const log = hostLogPath(dataDir);
|
|
4871
|
+
const temp = rotationTempPath(log);
|
|
4872
|
+
await rm4(temp, { force: true });
|
|
4873
|
+
let size;
|
|
4874
|
+
try {
|
|
4875
|
+
size = (await stat7(log)).size;
|
|
4876
|
+
} catch (error) {
|
|
4877
|
+
if (isMissing(error))
|
|
4878
|
+
return false;
|
|
4879
|
+
throw error;
|
|
4880
|
+
}
|
|
4881
|
+
if (size <= HOST_LOG_MAX_BYTES)
|
|
4882
|
+
return false;
|
|
4883
|
+
try {
|
|
4884
|
+
await copy(log, temp);
|
|
4885
|
+
} catch (error) {
|
|
4886
|
+
await rm4(temp, { force: true }).catch(() => {});
|
|
4887
|
+
throw error;
|
|
4888
|
+
}
|
|
4889
|
+
await rm4(`${log}.${HOST_LOG_HISTORY}`, { force: true });
|
|
4890
|
+
for (let index = HOST_LOG_HISTORY - 1;index >= 1; index -= 1) {
|
|
4891
|
+
try {
|
|
4892
|
+
await rename3(`${log}.${index}`, `${log}.${index + 1}`);
|
|
4893
|
+
} catch (error) {
|
|
4894
|
+
if (!isMissing(error))
|
|
4895
|
+
throw error;
|
|
4896
|
+
}
|
|
4897
|
+
}
|
|
4898
|
+
await rename3(temp, `${log}.1`);
|
|
4899
|
+
await truncate(log, 0);
|
|
4900
|
+
return true;
|
|
4901
|
+
}
|
|
4902
|
+
function startHostLogRotation(dataDir) {
|
|
4903
|
+
let running = false;
|
|
4904
|
+
const check = () => {
|
|
4905
|
+
if (running)
|
|
4906
|
+
return;
|
|
4907
|
+
running = true;
|
|
4908
|
+
rotateHostLog(dataDir).catch((error) => {
|
|
4909
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
4910
|
+
process.stderr.write(`${JSON.stringify({ event: "log-rotation-failed", error: message })}
|
|
4911
|
+
`);
|
|
4912
|
+
}).finally(() => {
|
|
4913
|
+
running = false;
|
|
4914
|
+
});
|
|
4915
|
+
};
|
|
4916
|
+
check();
|
|
4917
|
+
const timer = setInterval(check, HOST_LOG_CHECK_INTERVAL_MS);
|
|
4918
|
+
timer.unref();
|
|
4919
|
+
return () => clearInterval(timer);
|
|
4920
|
+
}
|
|
4921
|
+
|
|
3672
4922
|
// apps/host/src/model-auth.ts
|
|
3673
|
-
import { existsSync as
|
|
3674
|
-
import { basename as
|
|
4923
|
+
import { existsSync as existsSync3, watch as watch2 } from "node:fs";
|
|
4924
|
+
import { basename as basename4, dirname as dirname3 } from "node:path";
|
|
3675
4925
|
|
|
3676
4926
|
// apps/host/src/pi-setup.ts
|
|
3677
4927
|
import { getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai";
|
|
@@ -3786,7 +5036,7 @@ function modelSummaries(modelRuntime) {
|
|
|
3786
5036
|
}
|
|
3787
5037
|
|
|
3788
5038
|
// apps/host/src/model-auth.ts
|
|
3789
|
-
var
|
|
5039
|
+
var sameModel = (a, b) => a.provider === b.provider && a.modelId === b.modelId && a.name === b.name && a.contextWindow === b.contextWindow && sameList(a.thinkingLevels, b.thinkingLevels, (x, y) => x === y);
|
|
3790
5040
|
var sameLogin = (a, b) => a.providerId === b.providerId && a.name === b.name && a.kind === b.kind && a.subscription === b.subscription;
|
|
3791
5041
|
async function storedLogins(modelRuntime) {
|
|
3792
5042
|
const credentials = await modelRuntime.listCredentials();
|
|
@@ -3801,9 +5051,9 @@ async function storedLogins(modelRuntime) {
|
|
|
3801
5051
|
}
|
|
3802
5052
|
async function watchModelAuth(modelRuntime, options) {
|
|
3803
5053
|
const debounceMs = options.debounceMs ?? 150;
|
|
3804
|
-
const models = new Published(modelSummaries(modelRuntime), (a, b) => sameList(a, b,
|
|
5054
|
+
const models = new Published(modelSummaries(modelRuntime), (a, b) => sameList(a, b, sameModel));
|
|
3805
5055
|
const auth = new Published(await storedLogins(modelRuntime), (a, b) => sameList(a.logins, b.logins, sameLogin));
|
|
3806
|
-
const fileName =
|
|
5056
|
+
const fileName = basename4(options.authPath);
|
|
3807
5057
|
let watcher;
|
|
3808
5058
|
let timer;
|
|
3809
5059
|
let reported = false;
|
|
@@ -3821,7 +5071,7 @@ async function watchModelAuth(modelRuntime, options) {
|
|
|
3821
5071
|
}
|
|
3822
5072
|
};
|
|
3823
5073
|
const directory = dirname3(options.authPath);
|
|
3824
|
-
if (
|
|
5074
|
+
if (existsSync3(directory)) {
|
|
3825
5075
|
watcher = watch2(directory, (_event, changed) => {
|
|
3826
5076
|
if (closed || changed !== null && changed !== fileName)
|
|
3827
5077
|
return;
|
|
@@ -3871,7 +5121,7 @@ async function main(argv) {
|
|
|
3871
5121
|
relay: { type: "string" }
|
|
3872
5122
|
}
|
|
3873
5123
|
});
|
|
3874
|
-
const dataDir =
|
|
5124
|
+
const dataDir = resolve7(values["data-dir"] ?? DEFAULT_DATA_DIR);
|
|
3875
5125
|
let relayOrigin;
|
|
3876
5126
|
try {
|
|
3877
5127
|
relayOrigin = resolveRelayOrigin(values.relay, await readServerConfig(dataDir));
|
|
@@ -3879,9 +5129,10 @@ async function main(argv) {
|
|
|
3879
5129
|
console.error(error instanceof Error ? error.message : String(error));
|
|
3880
5130
|
process.exit(1);
|
|
3881
5131
|
}
|
|
5132
|
+
const agentsHome = join12(homedir3(), ".agents");
|
|
3882
5133
|
const extensionsFor = (models, summaries, options2) => (host2) => [
|
|
3883
5134
|
createContext({
|
|
3884
|
-
agentsHome
|
|
5135
|
+
agentsHome,
|
|
3885
5136
|
checkout: (input, context5) => checkoutInfo(host2.harness(), input, context5)
|
|
3886
5137
|
}),
|
|
3887
5138
|
createCoding(options2),
|
|
@@ -3897,9 +5148,10 @@ async function main(argv) {
|
|
|
3897
5148
|
];
|
|
3898
5149
|
const common = {
|
|
3899
5150
|
dataDir,
|
|
5151
|
+
agentsHome,
|
|
3900
5152
|
projects: values.project ?? [],
|
|
3901
5153
|
port: Number(values.port),
|
|
3902
|
-
mcpConfig:
|
|
5154
|
+
mcpConfig: join12(agentsHome, "mcp.json"),
|
|
3903
5155
|
browserOrigins: values["browser-origin"] ?? ["http://127.0.0.1:5199"],
|
|
3904
5156
|
webRoot,
|
|
3905
5157
|
...values["lock-stale-ms"] === undefined ? {} : { lockStaleMs: Number(values["lock-stale-ms"]) },
|
|
@@ -3943,7 +5195,7 @@ async function main(argv) {
|
|
|
3943
5195
|
}
|
|
3944
5196
|
async function piOptions() {
|
|
3945
5197
|
const modelRuntime = await ModelRuntime.create();
|
|
3946
|
-
const modelAuth = await watchModelAuth(modelRuntime, { authPath:
|
|
5198
|
+
const modelAuth = await watchModelAuth(modelRuntime, { authPath: join12(getAgentDir(), "auth.json") });
|
|
3947
5199
|
const settingsManager = SettingsManager.create(process.cwd());
|
|
3948
5200
|
configureHarnessHttp(settingsManager);
|
|
3949
5201
|
const summaries = () => modelAuth.models.value;
|
|
@@ -3969,13 +5221,14 @@ async function main(argv) {
|
|
|
3969
5221
|
}
|
|
3970
5222
|
const options = values.faux !== undefined ? fauxOptions(() => () => fauxAssistantMessage(values.faux)) : await piOptions();
|
|
3971
5223
|
const host = await openHost(options);
|
|
5224
|
+
const stopLogRotation = startHostLogRotation(dataDir);
|
|
3972
5225
|
const { defaultModel: effectiveModel } = host.settings.value;
|
|
3973
5226
|
const model = effectiveModel === null ? null : `${effectiveModel.provider}/${effectiveModel.modelId}`;
|
|
3974
5227
|
const web = packagedVersion === undefined ? "http://127.0.0.1:5199/" : new URL(host.url.replace(/^ws/, "http")).href;
|
|
3975
5228
|
console.log(JSON.stringify({
|
|
3976
5229
|
event: "ready",
|
|
3977
5230
|
url: host.url,
|
|
3978
|
-
tokenFile:
|
|
5231
|
+
tokenFile: join12(dataDir, "token"),
|
|
3979
5232
|
dataDir,
|
|
3980
5233
|
model,
|
|
3981
5234
|
web,
|
|
@@ -3987,6 +5240,7 @@ async function main(argv) {
|
|
|
3987
5240
|
if (stopping)
|
|
3988
5241
|
return;
|
|
3989
5242
|
stopping = true;
|
|
5243
|
+
stopLogRotation();
|
|
3990
5244
|
host.close().finally(() => process.exit(0));
|
|
3991
5245
|
};
|
|
3992
5246
|
process.on("SIGINT", stop);
|
|
@@ -3995,6 +5249,7 @@ async function main(argv) {
|
|
|
3995
5249
|
if (end.reason !== "failed" || stopping)
|
|
3996
5250
|
return;
|
|
3997
5251
|
stopping = true;
|
|
5252
|
+
stopLogRotation();
|
|
3998
5253
|
const error = end.error instanceof Error ? end.error.message : String(end.error);
|
|
3999
5254
|
process.stderr.write(`${JSON.stringify({ event: "fatal", error })}
|
|
4000
5255
|
`);
|