neko-harness 0.2.9 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/gateway.js CHANGED
@@ -105,11 +105,11 @@ function parseGatewayArgs(argv) {
105
105
  }
106
106
 
107
107
  // packages/gateway/src/gateway.ts
108
- import { randomUUID as randomUUID3 } from "crypto";
108
+ import { randomUUID as randomUUID4 } from "crypto";
109
109
  import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile4 } from "fs/promises";
110
110
  import { basename as basename4, dirname as dirname9, join as join10 } from "path";
111
111
  import { createHost } from "@earendil-works/chord";
112
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
112
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
113
113
 
114
114
  // packages/core/src/config.ts
115
115
  import { existsSync, readFileSync } from "fs";
@@ -516,6 +516,17 @@ function truncateMiddle(text, maxWidth) {
516
516
  function summarizeToolCall(name, args) {
517
517
  const record = args ?? {};
518
518
  switch (name) {
519
+ case "subagent":
520
+ return {
521
+ args: record.action === "list" ? "List agents" : `${record.agentId ?? record.type}: ${record.title}`,
522
+ detail: String(record.task ?? "")
523
+ };
524
+ case "find_files":
525
+ case "search_text":
526
+ return {
527
+ args: truncateMiddle(`${record.path ?? "."} ${record.pattern ?? record.query ?? ""}`, 100),
528
+ detail: JSON.stringify(args, null, 2)
529
+ };
519
530
  case "bash": {
520
531
  const command = typeof record.command === "string" ? record.command : "";
521
532
  const firstLine = command.split(`
@@ -632,6 +643,7 @@ function projectChatView(view, modelLabel, options = {}) {
632
643
  const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
633
644
  items.push({
634
645
  type: "user",
646
+ ...entry.data?.subagentRoundId ? { subagentRoundId: entry.data.subagentRoundId } : {},
635
647
  key: `e${entry.id}`,
636
648
  id: entry.id,
637
649
  text: defaultImagePrompt && Array.isArray(message.content) ? userText(message.content.slice(1)) : userText(message.content),
@@ -653,6 +665,7 @@ function projectChatView(view, modelLabel, options = {}) {
653
665
  items.push(withArgs(result, block.name, block.arguments));
654
666
  } else {
655
667
  const summary = summarizeToolCall(block.name, block.arguments);
668
+ const slot = live.tools?.find((s) => s.callId === block.id);
656
669
  items.push({
657
670
  type: "tool",
658
671
  key: `t${block.id}`,
@@ -660,9 +673,9 @@ function projectChatView(view, modelLabel, options = {}) {
660
673
  name: block.name,
661
674
  summary: summary.args,
662
675
  detail: summary.detail ?? null,
663
- phase: "done",
676
+ phase: slot?.status ?? "done",
664
677
  isError: false,
665
- output: null,
678
+ output: slot?.output ?? null,
666
679
  result: null,
667
680
  durationMs: null,
668
681
  note: ran ? null : "Not run: response interrupted."
@@ -821,23 +834,181 @@ var stripFrontmatter = (content) => extractFrontmatter(content).body;
821
834
  // packages/core/src/runtime.ts
822
835
  import { homedir as homedir4 } from "os";
823
836
  import { join as join5 } from "path";
824
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
837
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
825
838
  import { clampThinkingLevel, createModels, getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai/models";
826
839
  import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
827
840
  import {
828
- AssistantEntry,
841
+ AssistantEntry as AssistantEntry2,
829
842
  createRegistry,
830
- defineDoc as defineDoc2,
843
+ defineDoc as defineDoc4,
831
844
  Harness,
832
845
  LiveDoc as importLiveDoc,
833
846
  MemoryStorage,
834
- UsageDoc,
847
+ UsageDoc as UsageDoc2,
835
848
  watchEvents
836
849
  } from "@earendil-works/pi-durable";
837
850
  import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
838
851
  import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
839
852
  import { createCodingTools } from "@earendil-works/pi-durable/tools";
840
853
 
854
+ // packages/core/src/agent-file-search.ts
855
+ import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
856
+ import { defineExtension, defineTool } from "@earendil-works/pi-durable";
857
+ import { getOrThrow } from "@earendil-works/pi-durable/env";
858
+ import { Type } from "typebox";
859
+ var SEARCH_LIMITS = {
860
+ entries: 1e4,
861
+ fileBytes: 2 * 1024 * 1024,
862
+ scanBytes: 32 * 1024 * 1024,
863
+ outputBytes: 32 * 1024,
864
+ timeoutMs: 1e4
865
+ };
866
+ var path = Type.Optional(Type.String({ description: "Directory relative to this conversation's cwd, or absolute path." }));
867
+ var limit = Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum results (default 100)." }));
868
+ var text = (value) => ({ type: "text", text: value });
869
+ async function search(api, context, run) {
870
+ if (!api.env)
871
+ throw new Error("File environment unavailable");
872
+ const timer = new AbortController;
873
+ const timeout = setTimeout(() => timer.abort(new Error("File search timed out; narrow the directory.")), SEARCH_LIMITS.timeoutMs);
874
+ const signal = context.abortSignal ? AbortSignal.any([context.abortSignal, timer.signal]) : timer.signal;
875
+ try {
876
+ signal.throwIfAborted();
877
+ return await run(api.env, withAbortSignal(signal, context));
878
+ } finally {
879
+ clearTimeout(timeout);
880
+ }
881
+ }
882
+ async function listing(env, path, ctx) {
883
+ if (!env.listFiles)
884
+ throw new Error("File discovery unavailable. Retry.");
885
+ const base = getOrThrow(await env.absolutePath(path, ctx));
886
+ const files = getOrThrow(await env.listFiles(base, {
887
+ hidden: true,
888
+ gitignore: true,
889
+ maxEntries: SEARCH_LIMITS.entries,
890
+ maxBytes: 1024 * 1024,
891
+ timeoutMs: SEARCH_LIMITS.timeoutMs
892
+ }, ctx));
893
+ return { base, ...files };
894
+ }
895
+ function output(lines, complete, skipped = 0) {
896
+ const status = complete ? "Search complete within Pi listing; unreadable entries may be omitted." : "Partial results; narrow the directory or filter and retry.";
897
+ return {
898
+ output: [text(`${lines.join(`
899
+ `)}
900
+ ${status}${skipped ? ` Skipped/unreadable files: ${skipped}.` : ""}`)],
901
+ details: { complete, skipped }
902
+ };
903
+ }
904
+ var FindFiles = defineTool({
905
+ name: "find_files",
906
+ description: "Discover files and directories without a shell. query is a literal path/name substring (not a glob). Respects gitignore; returns absolute paths usable by read. A partial result is not a complete absence of matches.",
907
+ parameters: Type.Object({ path, query: Type.Optional(Type.String()), limit }),
908
+ replay: "safe",
909
+ execute: (args, api, context) => search(api, context, async (env, ctx) => {
910
+ const files = await listing(env, args.path ?? ".", ctx);
911
+ const lines = [];
912
+ let bytes = 0;
913
+ let complete = !files.truncated;
914
+ for (const file of files.entries) {
915
+ ctx.abortSignal?.throwIfAborted();
916
+ if (args.query && !file.path.includes(args.query))
917
+ continue;
918
+ const absolute = getOrThrow(await env.joinPath([files.base, file.path], ctx)).replace(/\\/g, "/");
919
+ const line = `${file.kind} ${absolute}`;
920
+ bytes += Buffer.byteLength(line) + 1;
921
+ if (lines.length >= (args.limit ?? 100) || bytes > SEARCH_LIMITS.outputBytes - 256) {
922
+ complete = false;
923
+ break;
924
+ }
925
+ lines.push(line);
926
+ }
927
+ return output(lines, complete);
928
+ })
929
+ });
930
+ var SearchText = defineTool({
931
+ name: "search_text",
932
+ description: "Search UTF-8 file contents without a shell. pattern and filePattern are literal substrings, not regex/globs. Returns absolute paths, 1-based line numbers and context for read. Binary/large/unreadable files and exhausted budgets are explicitly reported as partial results.",
933
+ parameters: Type.Object({
934
+ pattern: Type.String({ minLength: 1, maxLength: 4096 }),
935
+ path,
936
+ filePattern: Type.Optional(Type.String()),
937
+ caseSensitive: Type.Optional(Type.Boolean()),
938
+ contextLines: Type.Optional(Type.Integer({ minimum: 0, maximum: 5 })),
939
+ limit
940
+ }),
941
+ replay: "safe",
942
+ execute: (args, api, context) => search(api, context, async (env, ctx) => {
943
+ const files = await listing(env, args.path ?? ".", ctx);
944
+ const results = [];
945
+ let complete = !files.truncated;
946
+ let skipped = 0;
947
+ let scanned = 0;
948
+ let bytes = 0;
949
+ const needle = args.caseSensitive === false ? args.pattern.toLowerCase() : args.pattern;
950
+ outer:
951
+ for (const file of files.entries) {
952
+ ctx.abortSignal?.throwIfAborted();
953
+ if (file.kind !== "file" || args.filePattern && !file.path.includes(args.filePattern))
954
+ continue;
955
+ const absolute = getOrThrow(await env.joinPath([files.base, file.path], ctx));
956
+ const opened = await env.openBinaryReader(absolute, { noFollow: true }, ctx);
957
+ if (!opened.ok) {
958
+ ctx.abortSignal?.throwIfAborted();
959
+ skipped++;
960
+ complete = false;
961
+ continue;
962
+ }
963
+ try {
964
+ const info = getOrThrow(await opened.value.info(ctx));
965
+ if (info.size > SEARCH_LIMITS.fileBytes) {
966
+ skipped++;
967
+ complete = false;
968
+ continue;
969
+ }
970
+ if (scanned + info.size > SEARCH_LIMITS.scanBytes) {
971
+ complete = false;
972
+ break;
973
+ }
974
+ const data = getOrThrow(await opened.value.read(0, info.size + 1, ctx));
975
+ scanned += data.length;
976
+ if (data.length !== info.size || data.includes(0)) {
977
+ skipped++;
978
+ complete = false;
979
+ continue;
980
+ }
981
+ const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).split(/\r?\n/);
982
+ for (let i = 0;i < lines.length; i++) {
983
+ ctx.abortSignal?.throwIfAborted();
984
+ const value = lines[i];
985
+ if (!(args.caseSensitive === false ? value.toLowerCase() : value).includes(needle))
986
+ continue;
987
+ const n = args.contextLines ?? 0;
988
+ const block = lines.slice(Math.max(0, i - n), i + n + 1).map((line, offset) => `${absolute.replace(/\\/g, "/")}:${Math.max(0, i - n) + offset + 1}:${line}`).join(`
989
+ `);
990
+ bytes += Buffer.byteLength(block) + 1;
991
+ if (results.length >= (args.limit ?? 100) || bytes > SEARCH_LIMITS.outputBytes - 256) {
992
+ complete = false;
993
+ break outer;
994
+ }
995
+ results.push(block);
996
+ }
997
+ } catch (error) {
998
+ ctx.abortSignal?.throwIfAborted();
999
+ if (!(error instanceof Error))
1000
+ throw error;
1001
+ skipped++;
1002
+ complete = false;
1003
+ } finally {
1004
+ await opened.value.close(BACKGROUND_CONTEXT);
1005
+ }
1006
+ }
1007
+ return output(results, complete, skipped);
1008
+ })
1009
+ });
1010
+ var AgentFileSearch = defineExtension({ name: "neko-file-search", tools: [FindFiles, SearchText] });
1011
+
841
1012
  // packages/core/src/compaction.ts
842
1013
  import {
843
1014
  CompactionTask
@@ -870,9 +1041,9 @@ async function readCompaction(harness, storage, id, context) {
870
1041
  }
871
1042
 
872
1043
  // packages/core/src/image-metadata.ts
873
- import { defineExtension, hook, ToolTask } from "@earendil-works/pi-durable";
1044
+ import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
874
1045
  import sharp from "sharp";
875
- var ImageDetails = defineExtension({
1046
+ var ImageDetails = defineExtension2({
876
1047
  name: "neko-image-details",
877
1048
  hooks: [
878
1049
  hook(ToolTask, {
@@ -1045,10 +1216,575 @@ ${args}` : block;
1045
1216
  };
1046
1217
 
1047
1218
  // packages/core/src/prompt/prompt.ts
1048
- import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
1219
+ import { defineExtension as defineExtension4, section } from "@earendil-works/pi-durable";
1220
+
1221
+ // packages/core/src/subagents.ts
1222
+ import {
1223
+ AssistantEntry,
1224
+ configure,
1225
+ defineDoc,
1226
+ defineExtension as defineExtension3,
1227
+ defineTask,
1228
+ defineTool as defineTool2,
1229
+ LiveDoc,
1230
+ UsageDoc
1231
+ } from "@earendil-works/pi-durable";
1232
+ import { Type as Type2 } from "typebox";
1233
+ import { Value } from "typebox/value";
1234
+
1235
+ // packages/core/src/subagent-usage.ts
1236
+ var emptySubagentUsage = () => ({ models: {}, tools: {} });
1237
+ function subtractSubagentUsage(end, start) {
1238
+ const result = emptySubagentUsage();
1239
+ for (const bucket of ["models", "tools"]) {
1240
+ for (const [key, usage] of Object.entries(end[bucket])) {
1241
+ const before = start[bucket][key];
1242
+ const cost = usage.cost;
1243
+ const next = {
1244
+ input: usage.input - (before?.input ?? 0),
1245
+ output: usage.output - (before?.output ?? 0),
1246
+ cacheRead: usage.cacheRead - (before?.cacheRead ?? 0),
1247
+ cacheWrite: usage.cacheWrite - (before?.cacheWrite ?? 0),
1248
+ totalTokens: usage.totalTokens - (before?.totalTokens ?? 0),
1249
+ cost: {
1250
+ input: cost.input - (before?.cost.input ?? 0),
1251
+ output: cost.output - (before?.cost.output ?? 0),
1252
+ cacheRead: cost.cacheRead - (before?.cost.cacheRead ?? 0),
1253
+ cacheWrite: cost.cacheWrite - (before?.cost.cacheWrite ?? 0),
1254
+ total: cost.total - (before?.cost.total ?? 0)
1255
+ }
1256
+ };
1257
+ if (usage.reasoning !== undefined)
1258
+ next.reasoning = usage.reasoning - (before?.reasoning ?? 0);
1259
+ if (usage.cacheWrite1h !== undefined)
1260
+ next.cacheWrite1h = usage.cacheWrite1h - (before?.cacheWrite1h ?? 0);
1261
+ result[bucket][key] = next;
1262
+ }
1263
+ }
1264
+ return result;
1265
+ }
1266
+ function subagentUsageTotals(state) {
1267
+ const totals = totalUsage(state);
1268
+ return {
1269
+ input: totals.input,
1270
+ output: totals.output,
1271
+ cacheRead: totals.cacheRead,
1272
+ cacheWrite: totals.cacheWrite,
1273
+ cost: totals.cost.total
1274
+ };
1275
+ }
1276
+
1277
+ // packages/core/src/subagents.ts
1278
+ var agentIdFor = (childId) => `agent-${childId}`;
1279
+ var DelegationsDoc = defineDoc({
1280
+ kind: "app.neko.delegations",
1281
+ version: 1,
1282
+ scope: "conversation",
1283
+ history: "latest",
1284
+ fork: "initial",
1285
+ initial: () => ({ items: [] })
1286
+ });
1287
+ var SubagentsDoc = defineDoc({
1288
+ kind: "app.neko.subagents",
1289
+ version: 1,
1290
+ scope: "conversation",
1291
+ history: "latest",
1292
+ fork: "initial",
1293
+ initial: () => ({ items: [] })
1294
+ });
1295
+ var SubagentDoc = defineDoc({
1296
+ kind: "app.neko.subagent",
1297
+ version: 1,
1298
+ scope: "conversation",
1299
+ history: "latest",
1300
+ fork: "initial",
1301
+ initial: () => ({ role: null, parentId: null })
1302
+ });
1303
+ var SlotsDoc = defineDoc({
1304
+ kind: "app.neko.subagent-slots",
1305
+ version: 1,
1306
+ scope: "session",
1307
+ initial: () => ({ items: [] })
1308
+ });
1309
+ var ROLE_PROMPTS = {
1310
+ explore: "You are neko's exploration subagent. Read and search by default. Bash requires explicit user approval for each call and may have side effects. Respect the assigned constraints. After denial use another approach or report the limitation; do not bypass or repeatedly request denied operations. Return findings with paths and line numbers.",
1311
+ plan: "You are neko's read-only planning subagent. Read evidence and propose implementation steps, trade-offs and verification. Do not modify files or execute commands.",
1312
+ coder: "You are neko's coding subagent. Implement only the assigned scope and verify it. Coordinate through the main agent; do not overwrite unrelated changes. Report changed files and verification results."
1313
+ };
1314
+ var SUBAGENT_DELIVERY = " Work from the assigned task, your own prior conversation context and project resources; you cannot see the main conversation. Do not delegate or ask the user directly. Your final message must be a self-contained delivery to the main agent, including evidence, results and limitations. Skills never expand your allowed tools.";
1315
+ var READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract"]);
1316
+ async function refreshDelegations(tx, id) {
1317
+ const doc = await tx.doc(DelegationsDoc, id);
1318
+ const identities = await tx.doc(SubagentsDoc, id);
1319
+ for (const item of doc.items) {
1320
+ item.agentId ??= agentIdFor(item.childId);
1321
+ item.round ??= doc.items.filter((d) => d.childId === item.childId && d.taskId <= item.taskId).length;
1322
+ if (item.historyThrough !== undefined)
1323
+ continue;
1324
+ if (item.inputEntryId === undefined) {
1325
+ const submission = await tx.submissionByRequest(item.childId, `subagent:${item.taskId}`);
1326
+ if (submission?.type === "input" && submission.entry !== undefined)
1327
+ item.inputEntryId = submission.entry;
1328
+ }
1329
+ let identity = identities.items.find((a) => a.childId === item.childId);
1330
+ if (!identity) {
1331
+ const role = await tx.doc(SubagentDoc, item.childId);
1332
+ if (role.parentId !== id || role.role !== item.role)
1333
+ continue;
1334
+ identity = {
1335
+ agentId: item.agentId,
1336
+ parentId: id,
1337
+ childId: item.childId,
1338
+ role: item.role,
1339
+ cwd: "",
1340
+ title: item.title,
1341
+ createdAt: item.createdAt,
1342
+ activeRoundId: item.taskId
1343
+ };
1344
+ identities.items.push(identity);
1345
+ identity = identities.items.at(-1);
1346
+ }
1347
+ const task = await tx.task(item.taskId);
1348
+ const runner = item.runnerId === undefined ? undefined : await tx.task(item.runnerId);
1349
+ const live = await tx.doc(LiveDoc, item.childId);
1350
+ const idle = live.run === undefined && !live.compactions?.length;
1351
+ if (task?.state.status === "terminal") {
1352
+ if (item.status === "queued" || item.status === "running") {
1353
+ item.status = task.state.outcome.status === "aborted" ? "cancelled" : "failed";
1354
+ item.endedAt ??= Date.now();
1355
+ }
1356
+ if (identity.activeRoundId === item.taskId && idle && (!runner || runner.state.status === "terminal"))
1357
+ identity.activeRoundId = null;
1358
+ }
1359
+ }
1360
+ return JSON.parse(JSON.stringify(doc.items));
1361
+ }
1362
+ function summarizeSubagents(rounds, identities = []) {
1363
+ const groups = new Map;
1364
+ for (const round of rounds) {
1365
+ const id = round.agentId ?? agentIdFor(round.childId);
1366
+ groups.set(id, [...groups.get(id) ?? [], round]);
1367
+ }
1368
+ return [...groups].map(([agentId, entries]) => {
1369
+ const first = entries[0];
1370
+ const last = entries.at(-1);
1371
+ const identity = identities.find((a) => a.agentId === agentId);
1372
+ const active = entries.find((r) => identity ? r.taskId === identity.activeRoundId : r.status === "running" || r.status === "queued");
1373
+ return {
1374
+ agentId,
1375
+ parentId: first.parentId,
1376
+ childId: first.childId,
1377
+ role: first.role,
1378
+ title: identity?.title ?? first.title,
1379
+ status: active ? active.status === "queued" ? "queued" : "running" : "idle",
1380
+ activeRoundId: active?.taskId ?? null,
1381
+ lastRoundId: last.taskId,
1382
+ lastReport: last.report?.slice(0, 512) ?? null,
1383
+ roundCount: entries.length,
1384
+ taskTitle: (active ?? last).title,
1385
+ approvalCount: active?.approvalCount ?? 0,
1386
+ createdAt: first.createdAt,
1387
+ ...first.historyThrough !== undefined ? { historyThrough: first.historyThrough, historyUsage: first.historyUsage } : {}
1388
+ };
1389
+ });
1390
+ }
1391
+ async function acquireSlot(tx, round, cwd) {
1392
+ const slots = await tx.doc(SlotsDoc);
1393
+ const live = [];
1394
+ for (const slot of slots.items) {
1395
+ const task = await tx.task(slot.taskId);
1396
+ if (task && task.state.status !== "terminal")
1397
+ live.push(slot);
1398
+ }
1399
+ slots.items = live;
1400
+ if (live.some((slot) => slot.taskId === round.taskId))
1401
+ return true;
1402
+ if (live.filter((slot) => slot.parentId === round.parentId).length >= 3 || round.role === "coder" && live.some((slot) => slot.coder && slot.cwd === cwd))
1403
+ return false;
1404
+ slots.items.push({ taskId: round.taskId, parentId: round.parentId, cwd, coder: round.role === "coder" });
1405
+ return true;
1406
+ }
1407
+ var actionParameters = Type2.Union([
1408
+ Type2.Object({
1409
+ action: Type2.Optional(Type2.Literal("create")),
1410
+ type: Type2.Enum(["explore", "plan", "coder"]),
1411
+ title: Type2.String({ minLength: 1, maxLength: 80 }),
1412
+ task: Type2.String({ minLength: 1, maxLength: 32768 })
1413
+ }, { additionalProperties: false }),
1414
+ Type2.Object({
1415
+ action: Type2.Literal("continue"),
1416
+ agentId: Type2.String(),
1417
+ title: Type2.String({ minLength: 1, maxLength: 80 }),
1418
+ task: Type2.String({ minLength: 1, maxLength: 32768 })
1419
+ }, { additionalProperties: false }),
1420
+ Type2.Object({
1421
+ action: Type2.Literal("list"),
1422
+ cursor: Type2.Optional(Type2.Integer({ minimum: 0 })),
1423
+ limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100 }))
1424
+ }, { additionalProperties: false })
1425
+ ]);
1426
+ var parameters = Type2.Object({
1427
+ action: Type2.Optional(Type2.String({ enum: ["create", "continue", "list"], description: "Defaults to create." })),
1428
+ type: Type2.Optional(Type2.String({ enum: ["explore", "plan", "coder"], description: "Required for create only." })),
1429
+ agentId: Type2.Optional(Type2.String({ description: "Required for continue only; use an ID returned by create/list." })),
1430
+ title: Type2.Optional(Type2.String({ minLength: 1, maxLength: 80, description: "Required for create/continue." })),
1431
+ task: Type2.Optional(Type2.String({ minLength: 1, maxLength: 32768, description: "Required for create/continue." })),
1432
+ cursor: Type2.Optional(Type2.Integer({ minimum: 0, description: "List pagination cursor only." })),
1433
+ limit: Type2.Optional(Type2.Integer({ minimum: 1, maximum: 100, description: "List page size only." }))
1434
+ }, { additionalProperties: false });
1435
+ var structuredOutputSchema = Type2.Union([
1436
+ Type2.Object({
1437
+ agentId: Type2.String(),
1438
+ roundId: Type2.Integer(),
1439
+ status: Type2.Enum(["completed", "failed", "cancelled"]),
1440
+ report: Type2.String()
1441
+ }, { additionalProperties: false }),
1442
+ Type2.Object({
1443
+ items: Type2.Array(Type2.Object({
1444
+ agentId: Type2.String(),
1445
+ parentId: Type2.Integer(),
1446
+ childId: Type2.Integer(),
1447
+ role: Type2.Enum(["explore", "plan", "coder"]),
1448
+ title: Type2.String(),
1449
+ status: Type2.Enum(["idle", "queued", "running"]),
1450
+ activeRoundId: Type2.Union([Type2.Integer(), Type2.Null()]),
1451
+ lastRoundId: Type2.Integer(),
1452
+ lastReport: Type2.Optional(Type2.Union([Type2.String(), Type2.Null()])),
1453
+ roundCount: Type2.Integer(),
1454
+ taskTitle: Type2.String(),
1455
+ approvalCount: Type2.Integer(),
1456
+ createdAt: Type2.Number()
1457
+ }, { additionalProperties: false })),
1458
+ next: Type2.Union([Type2.Integer(), Type2.Null()])
1459
+ }, { additionalProperties: false })
1460
+ ]);
1461
+ function createSubagentExtension(prepare, discard) {
1462
+ const finish = async (tx, input, status, report) => {
1463
+ const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
1464
+ round.status = status;
1465
+ round.report = report;
1466
+ round.endedAt ??= Date.now();
1467
+ round.usageEnd = JSON.parse(JSON.stringify(await tx.doc(UsageDoc, input.childId)));
1468
+ round.roundUsage = round.usageStart ? subagentUsageTotals(subtractSubagentUsage(round.usageEnd, round.usageStart)) : round.startedAt === null ? subagentUsageTotals(emptySubagentUsage()) : null;
1469
+ };
1470
+ const RoundTask = defineTask({
1471
+ name: "app.neko.subagent-round",
1472
+ version: 1,
1473
+ initial: () => ({ phase: "admission" }),
1474
+ phases: {
1475
+ admission: async (task, runtime, ctx) => {
1476
+ const env = await runtime.env(ctx);
1477
+ const canonical = env && await env.canonicalPath(task.input.cwd, ctx);
1478
+ const slotCwd = canonical?.ok ? canonical.value : task.input.cwd;
1479
+ let admitted = false;
1480
+ while (!admitted) {
1481
+ await runtime.commit(async (tx) => {
1482
+ const round = (await tx.doc(DelegationsDoc, task.input.parentId)).items.find((r) => r.taskId === task.input.taskId);
1483
+ if (!await acquireSlot(tx, round, slotCwd))
1484
+ return;
1485
+ admitted = true;
1486
+ const duplicate = await tx.submissionByRequest(task.input.childId, `subagent:${round.taskId}`);
1487
+ if (!duplicate && !round.usageStart)
1488
+ round.usageStart = JSON.parse(JSON.stringify(await tx.doc(UsageDoc, task.input.childId)));
1489
+ round.status = "running";
1490
+ round.startedAt ??= Date.now();
1491
+ return { status: "running", checkpoint: { phase: "deliver" } };
1492
+ }, ctx);
1493
+ if (!admitted)
1494
+ await runtime.sleep(runtime.now() + 100, ctx);
1495
+ }
1496
+ },
1497
+ deliver: async (task, runtime, ctx) => {
1498
+ const input = task.input;
1499
+ const child = await runtime.conversation(input.childId, ctx);
1500
+ const requestId = `subagent:${input.taskId}`;
1501
+ try {
1502
+ let duplicate = false;
1503
+ await runtime.commit(async (tx) => {
1504
+ duplicate = !!await tx.submissionByRequest(input.childId, requestId);
1505
+ }, ctx);
1506
+ if (!duplicate)
1507
+ await prepare(input.childId, requestId, input.cwd);
1508
+ const submission = await child.submit({
1509
+ type: "input",
1510
+ content: input.task,
1511
+ requestId,
1512
+ whenBusy: "reject",
1513
+ entry: {
1514
+ kind: NekoInput.kind,
1515
+ data: { at: Date.now(), prompt: input.task, images: [], subagentRoundId: input.taskId }
1516
+ }
1517
+ }, ctx);
1518
+ const placed = await submission.status(ctx);
1519
+ await runtime.commit(async (tx) => {
1520
+ if (placed.type === "input" && placed.entry !== undefined)
1521
+ (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId).inputEntryId = placed.entry;
1522
+ return { status: "running", checkpoint: { phase: "settle" } };
1523
+ }, ctx);
1524
+ } catch (error) {
1525
+ discard(input.childId, requestId);
1526
+ if (ctx.abortSignal?.aborted)
1527
+ throw error;
1528
+ await child.abort(ctx);
1529
+ const report = `Subagent failed: ${String(error)}`;
1530
+ await runtime.commit(async (tx) => {
1531
+ await finish(tx, input, "failed", report);
1532
+ return { status: "terminal", outcome: { status: "completed", result: { status: "failed", report } } };
1533
+ }, ctx);
1534
+ }
1535
+ },
1536
+ settle: async (task, runtime, ctx) => {
1537
+ const input = task.input;
1538
+ const child = await runtime.conversation(input.childId, ctx);
1539
+ const submission = await child.submit({ type: "input", content: input.task, requestId: `subagent:${input.taskId}` }, ctx);
1540
+ const settled = await submission.wait(ctx);
1541
+ await child.waitForIdle(ctx);
1542
+ await runtime.commit(async (tx) => {
1543
+ let report = "";
1544
+ let status = "failed";
1545
+ const round = (await tx.doc(DelegationsDoc, input.parentId)).items.find((r) => r.taskId === input.taskId);
1546
+ if (settled.type === "input" && settled.entry !== undefined)
1547
+ round.inputEntryId = settled.entry;
1548
+ if (settled.status === "done" && settled.type === "input") {
1549
+ round.answerEntryId = settled.answer;
1550
+ const message = (await tx.entry(AssistantEntry, settled.answer))?.model?.[0];
1551
+ report = message?.role === "assistant" ? message.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("") : "";
1552
+ status = "completed";
1553
+ } else if (settled.status === "unanswered") {
1554
+ status = settled.reason === "aborted" ? "cancelled" : "failed";
1555
+ report = `Subagent ${status}: ${settled.reason}`;
1556
+ }
1557
+ await finish(tx, input, status, report);
1558
+ return { status: "terminal", outcome: { status: "completed", result: { status, report } } };
1559
+ }, ctx);
1560
+ }
1561
+ },
1562
+ abort: async (task, runtime, ctx) => {
1563
+ const input = task.input;
1564
+ let own = false;
1565
+ await runtime.commit(async (tx) => {
1566
+ own = (await tx.doc(SubagentsDoc, input.parentId)).items.some((a) => a.childId === input.childId && a.activeRoundId === input.taskId);
1567
+ }, ctx);
1568
+ if (own)
1569
+ await (await runtime.conversation(input.childId, ctx))?.abort(ctx);
1570
+ await runtime.commit(async (tx) => {
1571
+ await finish(tx, input, "cancelled", "Subagent cancelled");
1572
+ return { status: "terminal", outcome: { status: "aborted" } };
1573
+ }, ctx);
1574
+ }
1575
+ });
1576
+ const extension = defineExtension3({
1577
+ name: "neko-subagents",
1578
+ tasks: [RoundTask],
1579
+ tools: [
1580
+ defineTool2({
1581
+ name: "subagent",
1582
+ description: "Manage persistent foreground subagents. create (default): type explore/plan/coder, title, task. continue: agentId, title, task; reuses its own context, only while idle. list: discover this session's agents, optional cursor/limit. Explore Bash requires per-call user approval. Keep constraints explicit; verify and integrate final reports. Subagents cannot delegate.",
1583
+ parameters,
1584
+ structuredOutputSchema,
1585
+ replay: "safe",
1586
+ execute: async (input, api, context) => {
1587
+ if (!Value.Check(actionParameters, input))
1588
+ throw new Error("Invalid subagent arguments. create requires type/title/task; continue requires agentId/title/task; list accepts only cursor/limit. Do not mix fields from different actions.");
1589
+ const args = input;
1590
+ if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
1591
+ throw new Error("Subagents cannot delegate.");
1592
+ if (args.action === "list") {
1593
+ const list = await api.commit(async (tx) => {
1594
+ const rounds = await refreshDelegations(tx, api.conversationId);
1595
+ return summarizeSubagents(rounds, (await tx.doc(SubagentsDoc, api.conversationId)).items).filter((a) => a.historyThrough === undefined);
1596
+ }, context);
1597
+ const offset = args.cursor ?? 0;
1598
+ const items = list.slice(offset, offset + (args.limit ?? 50));
1599
+ const result = { items, next: offset + items.length < list.length ? offset + items.length : null };
1600
+ return { output: [{ type: "text", text: JSON.stringify(result) }], structuredOutput: result };
1601
+ }
1602
+ if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
1603
+ throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
1604
+ const agent = await api.agent(context);
1605
+ const round = await api.commit(async (tx) => {
1606
+ const rounds = await refreshDelegations(tx, api.conversationId);
1607
+ const receipt = rounds.find((r) => r.taskId === api.taskId);
1608
+ if (receipt?.runnerId !== undefined)
1609
+ return receipt;
1610
+ const identities = await tx.doc(SubagentsDoc, api.conversationId);
1611
+ let identity;
1612
+ if (args.action === "continue") {
1613
+ identity = identities.items.find((a) => a.agentId === args.agentId);
1614
+ if (!identity)
1615
+ throw new Error("Unknown subagent or it belongs to another conversation.");
1616
+ const info = await tx.doc(SubagentDoc, identity.childId);
1617
+ if (info.parentId !== api.conversationId || info.role !== identity.role)
1618
+ throw new Error("Subagent ownership mismatch.");
1619
+ if (identity.activeRoundId !== null)
1620
+ throw new Error("Subagent is busy; continue only when idle.");
1621
+ } else if (receipt) {
1622
+ identity = identities.items.find((a) => a.childId === receipt.childId);
1623
+ }
1624
+ const existing = identity ? undefined : (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
1625
+ const child = identity ? await tx.conversation(identity.childId) : existing;
1626
+ if (!identity) {
1627
+ const created = child ?? await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
1628
+ const role = args.action === "continue" ? "explore" : args.type;
1629
+ await configure(tx, created.id, {
1630
+ model: agent.model,
1631
+ thinkingLevel: agent.thinkingLevel,
1632
+ cwd: agent.cwd,
1633
+ extensions: agent.extensions.filter((e) => e.name !== extension.name),
1634
+ tools: agent.tools.filter((t) => t.name !== "subagent" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
1635
+ });
1636
+ Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
1637
+ identity = {
1638
+ agentId: agentIdFor(created.id),
1639
+ parentId: api.conversationId,
1640
+ childId: created.id,
1641
+ role,
1642
+ cwd: agent.cwd ?? "",
1643
+ title: args.title,
1644
+ createdAt: Date.now(),
1645
+ activeRoundId: null
1646
+ };
1647
+ identities.items.push(identity);
1648
+ }
1649
+ const record = receipt ?? {
1650
+ parentId: api.conversationId,
1651
+ taskId: api.taskId,
1652
+ callId: api.callId,
1653
+ childId: identity.childId,
1654
+ agentId: identity.agentId,
1655
+ round: rounds.filter((r) => r.childId === identity.childId).length + 1,
1656
+ role: identity.role,
1657
+ title: args.title,
1658
+ createdAt: Date.now(),
1659
+ startedAt: null,
1660
+ endedAt: null,
1661
+ status: "queued",
1662
+ report: null
1663
+ };
1664
+ identities.items.find((a) => a.agentId === identity.agentId).activeRoundId = api.taskId;
1665
+ const runner = await tx.createTask(RoundTask, {
1666
+ parentId: api.conversationId,
1667
+ childId: identity.childId,
1668
+ taskId: api.taskId,
1669
+ task: args.task,
1670
+ cwd: identity.cwd || agent.cwd || ""
1671
+ }, { ownership: { kind: "task", taskId: api.taskId } });
1672
+ record.runnerId = runner;
1673
+ const doc = await tx.doc(DelegationsDoc, api.conversationId);
1674
+ if (receipt)
1675
+ Object.assign(doc.items.find((r) => r.taskId === receipt.taskId), record);
1676
+ else
1677
+ doc.items.push(record);
1678
+ return record;
1679
+ }, context);
1680
+ await api.details({
1681
+ conversationId: round.childId,
1682
+ agentId: round.agentId,
1683
+ roundId: round.taskId,
1684
+ role: round.role,
1685
+ title: round.title
1686
+ }, context);
1687
+ const settled = await api.waitForTask(round.runnerId, context);
1688
+ const outcome = settled.state.outcome;
1689
+ const result = outcome.status === "completed" ? outcome.result : { status: outcome.status === "aborted" ? "cancelled" : "failed", report: `Subagent ${outcome.status}` };
1690
+ return {
1691
+ output: [
1692
+ {
1693
+ type: "text",
1694
+ text: `Agent: ${round.agentId}
1695
+ Status: ${result.status}
1696
+ ${result.report || "Subagent completed without a text report."}`
1697
+ }
1698
+ ],
1699
+ isError: result.status !== "completed",
1700
+ structuredOutput: { agentId: round.agentId, roundId: round.taskId, ...result },
1701
+ details: {
1702
+ conversationId: round.childId,
1703
+ agentId: round.agentId,
1704
+ roundId: round.taskId,
1705
+ role: round.role,
1706
+ title: round.title,
1707
+ status: result.status
1708
+ }
1709
+ };
1710
+ }
1711
+ })
1712
+ ]
1713
+ });
1714
+ return extension;
1715
+ }
1716
+ async function listDelegations(harness, parent, context) {
1717
+ const rounds = await harness.commit((tx) => refreshDelegations(tx, parent), context);
1718
+ for (const round of rounds) {
1719
+ if (round.historyThrough !== undefined)
1720
+ continue;
1721
+ const task = await harness.getTask(round.taskId, context);
1722
+ if (task?.state.status !== "terminal")
1723
+ continue;
1724
+ const child = await harness.conversation(round.childId, context);
1725
+ const live = await harness.snapshot(LiveDoc, round.childId, context);
1726
+ const identity = (await harness.snapshot(SubagentsDoc, parent, context))?.items.find((a) => a.childId === round.childId);
1727
+ if (identity?.activeRoundId === round.taskId && live?.run !== undefined)
1728
+ await child?.abort(context);
1729
+ }
1730
+ return harness.commit((tx) => refreshDelegations(tx, parent), context);
1731
+ }
1732
+ async function installSubagentCoordinator(harness, parents, context) {
1733
+ const known = new Set(parents);
1734
+ let running = false;
1735
+ let dirty = false;
1736
+ const reconcile = async () => {
1737
+ if (running) {
1738
+ dirty = true;
1739
+ return;
1740
+ }
1741
+ running = true;
1742
+ try {
1743
+ do {
1744
+ dirty = false;
1745
+ for (const id of known)
1746
+ await listDelegations(harness, id, context);
1747
+ } while (dirty);
1748
+ } finally {
1749
+ running = false;
1750
+ }
1751
+ };
1752
+ for (const parent of parents) {
1753
+ await harness.commit((tx) => refreshDelegations(tx, parent), context);
1754
+ for (const identity of (await harness.snapshot(SubagentsDoc, parent, context))?.items ?? []) {
1755
+ if (identity.cwd)
1756
+ continue;
1757
+ const child = await harness.conversation(identity.childId, context);
1758
+ const cwd = (await child?.agent(context))?.cwd;
1759
+ if (cwd)
1760
+ await harness.commit(async (tx) => {
1761
+ (await tx.doc(SubagentsDoc, parent)).items.find((a) => a.agentId === identity.agentId).cwd = cwd;
1762
+ }, context);
1763
+ }
1764
+ }
1765
+ const stop = harness.subscribeCommits(({ changes }) => {
1766
+ for (const c of changes)
1767
+ if (c.type === "document" && c.record.kind === "app.neko.delegations" && c.conversationId !== undefined)
1768
+ known.add(c.conversationId);
1769
+ if (changes.some((c) => c.type === "task" && c.value.state.status === "terminal"))
1770
+ reconcile().catch(() => {});
1771
+ });
1772
+ harness.closed.finally(stop).catch(() => {});
1773
+ }
1774
+
1775
+ // packages/core/src/prompt/prompt.ts
1049
1776
  var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
1050
1777
  var PREAMBLE = "You are an expert coding assistant operating inside neko, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.";
1051
1778
  var TOOL_GUIDELINES = {
1779
+ subagent: [
1780
+ "Delegate only independent, sufficiently scoped work when isolation or parallelism helps. Handle simple tasks directly. Pass necessary context and user constraints; research-only requests must not become coding tasks. For related follow-up work, list and continue a suitable idle subagent using its stable agentId; busy means no task was accepted. Do not change its role. Verify and integrate reports. Do not modify files concurrently with a coder assigned those files."
1781
+ ],
1782
+ find_files: [
1783
+ "Use find_files to discover files; query is a literal substring. Narrow the directory when results are partial."
1784
+ ],
1785
+ search_text: [
1786
+ "Use search_text for literal text search and read for surrounding evidence. Partial results do not prove absence."
1787
+ ],
1052
1788
  read: ["Use read to examine files instead of cat or sed."],
1053
1789
  write: ["Use write only for new files or complete rewrites."],
1054
1790
  edit: [
@@ -1084,11 +1820,12 @@ ${content}
1084
1820
  `);
1085
1821
  var createNekoPrompt = (loadResources) => {
1086
1822
  const built = new WeakMap;
1087
- const build = async (input) => {
1823
+ const build = async (input, context) => {
1088
1824
  const cwd = input.agent.cwd;
1825
+ const role = (await input.read.snapshot(SubagentDoc, input.conversationId, context))?.role;
1089
1826
  const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
1090
1827
  const sections = {
1091
- preamble: PREAMBLE,
1828
+ preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
1092
1829
  rules: buildRules(input.agent.tools.map((tool) => tool.name)),
1093
1830
  project_context: renderProjectContext(resources.contextFiles),
1094
1831
  skills: formatSkillsForPrompt(resources.skills) || undefined,
@@ -1096,12 +1833,12 @@ var createNekoPrompt = (loadResources) => {
1096
1833
  };
1097
1834
  return sections;
1098
1835
  };
1099
- return defineExtension2({
1836
+ return defineExtension4({
1100
1837
  name: "neko-prompt",
1101
- sections: KEYS.map((key) => section(key, async (input) => {
1838
+ sections: KEYS.map((key) => section(key, async (input, context) => {
1102
1839
  let pending = built.get(input);
1103
1840
  if (!pending) {
1104
- pending = build(input);
1841
+ pending = build(input, context);
1105
1842
  built.set(input, pending);
1106
1843
  }
1107
1844
  const text = (await pending)[key];
@@ -1248,13 +1985,13 @@ var createResourceSnapshots = (load, submission) => {
1248
1985
  import { stripVTControlCharacters } from "util";
1249
1986
  import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
1250
1987
  import {
1251
- defineDoc,
1252
- defineExtension as defineExtension3,
1253
- defineTask,
1988
+ defineDoc as defineDoc2,
1989
+ defineExtension as defineExtension5,
1990
+ defineTask as defineTask2,
1254
1991
  InboxDoc,
1255
- LiveDoc
1992
+ LiveDoc as LiveDoc2
1256
1993
  } from "@earendil-works/pi-durable";
1257
- var SessionMetadataDoc = defineDoc({
1994
+ var SessionMetadataDoc = defineDoc2({
1258
1995
  kind: "app.neko.session-metadata",
1259
1996
  version: 1,
1260
1997
  scope: "conversation",
@@ -1317,7 +2054,7 @@ function mergeUsage(base, extra) {
1317
2054
  function namingTask() {
1318
2055
  let active = 0;
1319
2056
  const waits = new Set;
1320
- return defineTask({
2057
+ return defineTask2({
1321
2058
  name: "app.neko.name",
1322
2059
  version: 1,
1323
2060
  initial: () => ({ phase: "wait", until: Date.now() + 100 }),
@@ -1441,7 +2178,7 @@ class SessionMetadataManager {
1441
2178
  this.storage = storage;
1442
2179
  this.context = context;
1443
2180
  this.report = report;
1444
- registry.install(defineExtension3({ name: "neko-naming", tasks: [this.task] }));
2181
+ registry.install(defineExtension5({ name: "neko-naming", tasks: [this.task] }));
1445
2182
  this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
1446
2183
  const ids = new Set;
1447
2184
  const submissions = new Map;
@@ -1531,7 +2268,7 @@ class SessionMetadataManager {
1531
2268
  const answer = record.status === "done" ? (await this.storage.entry(record.answer, this.context))?.entry.model?.[0] : undefined;
1532
2269
  return { record, data, answer: answer?.role === "assistant" ? answer : undefined };
1533
2270
  }));
1534
- const live = await this.harness.snapshot(LiveDoc, id, this.context);
2271
+ const live = await this.harness.snapshot(LiveDoc2, id, this.context);
1535
2272
  await this.harness.commit(async (tx) => {
1536
2273
  const d = await tx.doc(SessionMetadataDoc, id);
1537
2274
  for (const { record, data, answer } of updates) {
@@ -1580,9 +2317,192 @@ class SessionMetadataManager {
1580
2317
  }
1581
2318
  }
1582
2319
 
2320
+ // packages/core/src/tool-approvals.ts
2321
+ import { randomUUID } from "crypto";
2322
+ import { realpath } from "fs/promises";
2323
+ import {
2324
+ configure as configure2,
2325
+ defineDoc as defineDoc3,
2326
+ defineExtension as defineExtension6,
2327
+ hook as hook2,
2328
+ ToolTask as ToolTask2,
2329
+ wrapTool
2330
+ } from "@earendil-works/pi-durable";
2331
+ import { createBashTool } from "@earendil-works/pi-durable/tools";
2332
+ var ApprovalsDoc = defineDoc3({
2333
+ kind: "app.neko.approvals",
2334
+ version: 1,
2335
+ scope: "conversation",
2336
+ history: "latest",
2337
+ fork: "initial",
2338
+ initial: () => ({ items: [] })
2339
+ });
2340
+ async function live(tx, id) {
2341
+ const task = await tx.task(id);
2342
+ return !!task && !task.abortRequested && task.state.status !== "terminal" && task.state.status !== "completing";
2343
+ }
2344
+
2345
+ class ToolApprovals {
2346
+ harness;
2347
+ #parents = new Set;
2348
+ constructor(harness, context) {
2349
+ this.harness = harness;
2350
+ const stop = harness.subscribeCommits(({ changes }) => {
2351
+ if (!changes.some((c) => c.type === "task" && (c.value.abortRequested || c.value.state.status === "terminal")))
2352
+ return;
2353
+ for (const parent of this.#parents)
2354
+ this.list(parent, context).catch(() => {});
2355
+ });
2356
+ harness.closed.finally(stop).catch(() => {});
2357
+ }
2358
+ async list(parent, context) {
2359
+ this.#parents.add(parent);
2360
+ return this.harness.commit(async (tx) => {
2361
+ const doc = await tx.doc(ApprovalsDoc, parent);
2362
+ for (const item of doc.items) {
2363
+ if (item.status === "pending" && (!await live(tx, item.id) || !await live(tx, item.delegationTaskId))) {
2364
+ item.status = "cancelled";
2365
+ item.decidedAt = Date.now();
2366
+ }
2367
+ }
2368
+ return JSON.parse(JSON.stringify(doc.items));
2369
+ }, context);
2370
+ }
2371
+ async decide(parent, delegationTaskId, id, version, decision, context) {
2372
+ return this.harness.commit(async (tx) => {
2373
+ const delegation = (await tx.doc(DelegationsDoc, parent)).items.find((d) => d.taskId === delegationTaskId);
2374
+ const item = (await tx.doc(ApprovalsDoc, parent)).items.find((a) => a.id === id);
2375
+ if (!delegation || delegation.historyThrough !== undefined || !item || item.delegationTaskId !== delegationTaskId || item.childId !== delegation.childId || item.version !== version)
2376
+ throw new Error("Approval does not belong to this task or has changed");
2377
+ if (item.status !== "pending")
2378
+ return item.status;
2379
+ item.status = await live(tx, id) && await live(tx, delegationTaskId) ? decision : "cancelled";
2380
+ item.decidedAt = Date.now();
2381
+ return item.status;
2382
+ }, context);
2383
+ }
2384
+ async wait(parent, id, context) {
2385
+ const state = await this.harness.documentState(ApprovalsDoc, parent, context);
2386
+ if (!state)
2387
+ throw new Error("Approval unavailable");
2388
+ try {
2389
+ return await new Promise((resolve, reject) => {
2390
+ let stop = () => {};
2391
+ const cleanup = () => {
2392
+ stop();
2393
+ context.abortSignal?.removeEventListener("abort", abort);
2394
+ };
2395
+ const abort = () => {
2396
+ cleanup();
2397
+ reject(context.abortSignal?.reason ?? new Error("Cancelled"));
2398
+ };
2399
+ const check = () => {
2400
+ const item = state.value?.items.find((a) => a.id === id);
2401
+ if (item && item.status !== "pending") {
2402
+ cleanup();
2403
+ resolve(structuredClone(item));
2404
+ }
2405
+ };
2406
+ stop = state.subscribe(check);
2407
+ context.abortSignal?.addEventListener("abort", abort, { once: true });
2408
+ if (context.abortSignal?.aborted)
2409
+ abort();
2410
+ else
2411
+ check();
2412
+ });
2413
+ } finally {
2414
+ state.dispose();
2415
+ }
2416
+ }
2417
+ async ensure(parent, candidate, context) {
2418
+ this.#parents.add(parent);
2419
+ return this.harness.commit(async (tx) => {
2420
+ if (!await live(tx, candidate.id) || !await live(tx, candidate.delegationTaskId))
2421
+ throw new Error("Task stopped");
2422
+ const doc = await tx.doc(ApprovalsDoc, parent);
2423
+ const existing = doc.items.find((a) => a.id === candidate.id);
2424
+ if (existing) {
2425
+ if (existing.command !== candidate.command || existing.cwd !== candidate.cwd || existing.timeout !== candidate.timeout || existing.childId !== candidate.childId)
2426
+ throw new Error("Approval operation changed; a new call is required");
2427
+ return;
2428
+ }
2429
+ doc.items.push({
2430
+ ...candidate,
2431
+ version: randomUUID(),
2432
+ status: "pending",
2433
+ createdAt: Date.now(),
2434
+ decidedAt: null
2435
+ });
2436
+ }, context);
2437
+ }
2438
+ }
2439
+ function createApprovalExtension(service) {
2440
+ return defineExtension6({
2441
+ name: "neko-tool-approvals",
2442
+ hooks: [
2443
+ hook2(ToolTask2, {
2444
+ beforeTool: async (call, api, context) => {
2445
+ const role = await api.snapshot(SubagentDoc, api.conversationId, context);
2446
+ if (role?.role !== "explore" || call.name !== "bash")
2447
+ return;
2448
+ if (role.parentId === null)
2449
+ return { block: "Missing parent session" };
2450
+ const approvals = service(), parent = role.parentId;
2451
+ const child = await approvals.harness.conversation(api.conversationId, context);
2452
+ const agent = await child.agent(context);
2453
+ const cwd = await realpath(agent.cwd);
2454
+ const identity = (await api.snapshot(SubagentsDoc, parent, context))?.items.find((a) => a.childId === api.conversationId);
2455
+ const delegation = (await api.snapshot(DelegationsDoc, parent, context))?.items.find((d) => d.childId === api.conversationId && d.historyThrough === undefined && d.taskId === identity?.activeRoundId);
2456
+ if (!delegation)
2457
+ return { block: "Missing delegation" };
2458
+ await approvals.ensure(parent, {
2459
+ id: api.taskId,
2460
+ childId: api.conversationId,
2461
+ delegationTaskId: delegation.taskId,
2462
+ callId: call.id,
2463
+ command: String(call.arguments.command),
2464
+ cwd,
2465
+ timeout: typeof call.arguments.timeout === "number" ? call.arguments.timeout : null
2466
+ }, context);
2467
+ const result = await approvals.wait(parent, api.taskId, context);
2468
+ return result.status === "allowed" ? undefined : { block: "Command denied or cancelled by user. Use another approach or report the limitation." };
2469
+ }
2470
+ })
2471
+ ],
2472
+ wraps: [
2473
+ wrapTool(createBashTool(), (bash) => ({
2474
+ ...bash,
2475
+ execute: async (args, api, context) => {
2476
+ const role = await api.snapshot(SubagentDoc, api.conversationId, context);
2477
+ if (role?.role === "explore") {
2478
+ const item = role.parentId === null ? undefined : (await api.snapshot(ApprovalsDoc, role.parentId, context))?.items.find((a) => a.id === api.taskId);
2479
+ const cwd = api.env && await api.env.canonicalPath(api.env.cwd, context);
2480
+ if (!item || item.status !== "allowed" || item.command !== args.command || item.timeout !== (args.timeout ?? null) || !cwd?.ok || cwd.value !== item.cwd)
2481
+ throw new Error("Bash approval missing or operation changed");
2482
+ }
2483
+ context.abortSignal?.throwIfAborted();
2484
+ return bash.execute(args, api, context);
2485
+ }
2486
+ }))
2487
+ ]
2488
+ });
2489
+ }
2490
+ async function prepareApprovalConversations(harness, ids, extension, context) {
2491
+ for (const id of ids) {
2492
+ if ((await harness.snapshot(SubagentDoc, id, context))?.role !== "explore")
2493
+ continue;
2494
+ const child = await harness.conversation(id, context);
2495
+ const agent = await child.agent(context);
2496
+ await harness.commit((tx) => configure2(tx, id, {
2497
+ tools: [...agent.tools.filter((t) => t.name !== "bash"), createBashTool()],
2498
+ extensions: { add: [extension] }
2499
+ }), context);
2500
+ }
2501
+ }
2502
+
1583
2503
  // packages/core/src/web-search.ts
1584
- import { Type } from "@earendil-works/pi-ai";
1585
- import { defineExtension as defineExtension4, defineTool } from "@earendil-works/pi-durable";
2504
+ import { Type as Type3 } from "@earendil-works/pi-ai";
2505
+ import { defineExtension as defineExtension7, defineTool as defineTool3 } from "@earendil-works/pi-durable";
1586
2506
  var SEARCH_URL = "https://api.tavily.com/search";
1587
2507
  var EXTRACT_URL = "https://api.tavily.com/extract";
1588
2508
  var REQUEST_TIMEOUT_MS = 30000;
@@ -1700,15 +2620,15 @@ function urlKey(value) {
1700
2620
  }
1701
2621
  }
1702
2622
  function createWebSearchTools(resolveKey, fetchImpl = fetch) {
1703
- return defineExtension4({
2623
+ return defineExtension7({
1704
2624
  name: "neko-web-search",
1705
2625
  tools: [
1706
- defineTool({
2626
+ defineTool3({
1707
2627
  name: "web_search",
1708
2628
  description: "Search the live web for current or unknown information. Returns ranked sources (title, URL, bounded excerpts). Use only when up-to-date or external information is needed; use web_extract to read a known URL.",
1709
- parameters: Type.Object({
1710
- query: Type.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
1711
- maxResults: Type.Optional(Type.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
2629
+ parameters: Type3.Object({
2630
+ query: Type3.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
2631
+ maxResults: Type3.Optional(Type3.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
1712
2632
  }),
1713
2633
  async execute({ query, maxResults }, _api, context) {
1714
2634
  try {
@@ -1733,11 +2653,11 @@ ${truncate(String(item.url ?? ""), 300)}
1733
2653
  }
1734
2654
  }
1735
2655
  }),
1736
- defineTool({
2656
+ defineTool3({
1737
2657
  name: "web_extract",
1738
2658
  description: "Extract bounded markdown excerpts from web pages by URL. Every requested URL has a result or failure entry; long content is truncated.",
1739
- parameters: Type.Object({
1740
- urls: Type.Array(Type.String({ maxLength: 2048, description: "http(s) URL" }), {
2659
+ parameters: Type3.Object({
2660
+ urls: Type3.Array(Type3.String({ maxLength: 2048, description: "http(s) URL" }), {
1741
2661
  minItems: 1,
1742
2662
  maxItems: MAX_URLS,
1743
2663
  description: `URLs to extract (1-${MAX_URLS})`
@@ -1782,10 +2702,10 @@ ${truncate(String(item.url ?? ""), 300)}
1782
2702
  }
1783
2703
 
1784
2704
  // packages/core/src/workspace-files.ts
1785
- import { realpath, stat } from "fs/promises";
2705
+ import { realpath as realpath2, stat } from "fs/promises";
1786
2706
  import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
1787
- import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
1788
- import { getOrThrow } from "@earendil-works/pi-durable/env";
2707
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
2708
+ import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
1789
2709
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
1790
2710
  function relativeQuery(query) {
1791
2711
  if (query.length > 2048 || [...query].some((c) => c.charCodeAt(0) < 32 || c === '"' || c === "\\") || isAbsolute2(query) || query.startsWith("~") || query.split("/").some((part) => part === ".." || part === ".git"))
@@ -1793,8 +2713,8 @@ function relativeQuery(query) {
1793
2713
  return query.replace(/^\.\//, "");
1794
2714
  }
1795
2715
  async function workspaceTarget(root, path) {
1796
- const base = await realpath(root);
1797
- const target = await realpath(resolve3(base, relativeQuery(path)));
2716
+ const base = await realpath2(root);
2717
+ const target = await realpath2(resolve3(base, relativeQuery(path)));
1798
2718
  const local = relative(base, target);
1799
2719
  if (local === ".." || local.startsWith(`..${sep}`) || isAbsolute2(local))
1800
2720
  throw new Error("Path is outside the workspace.");
@@ -1903,7 +2823,7 @@ class WorkspaceFiles {
1903
2823
  }
1904
2824
  #disposeWatch(watch) {
1905
2825
  watch.controller.abort();
1906
- const closing = watch.work.catch(() => {}).then(() => watch.watcher?.close(BACKGROUND_CONTEXT)).then(() => {}).catch(() => {}).finally(() => this.#disposing.delete(closing));
2826
+ const closing = watch.work.catch(() => {}).then(() => watch.watcher?.close(BACKGROUND_CONTEXT2)).then(() => {}).catch(() => {}).finally(() => this.#disposing.delete(closing));
1907
2827
  this.#disposing.add(closing);
1908
2828
  }
1909
2829
  #closeWatcher(item) {
@@ -1950,11 +2870,11 @@ class WorkspaceFiles {
1950
2870
  for (const user of [...current.users])
1951
2871
  this.#closeWatcher(user);
1952
2872
  }
1953
- }, withAbortSignal(current.controller.signal, BACKGROUND_CONTEXT));
2873
+ }, withAbortSignal2(current.controller.signal, BACKGROUND_CONTEXT2));
1954
2874
  if (result.ok) {
1955
2875
  current.watcher = result.value;
1956
2876
  if (current.controller.signal.aborted)
1957
- await result.value.close(BACKGROUND_CONTEXT);
2877
+ await result.value.close(BACKGROUND_CONTEXT2);
1958
2878
  } else {
1959
2879
  forget();
1960
2880
  }
@@ -1978,7 +2898,7 @@ class WorkspaceFiles {
1978
2898
  async#scan(item) {
1979
2899
  const controller = new AbortController;
1980
2900
  item.controller = controller;
1981
- const context = withAbortSignal(controller.signal, BACKGROUND_CONTEXT);
2901
+ const context = withAbortSignal2(controller.signal, BACKGROUND_CONTEXT2);
1982
2902
  while (this.#active >= this.limits.concurrent) {
1983
2903
  await new Promise((resolve, reject) => {
1984
2904
  const wake = () => {
@@ -2023,7 +2943,7 @@ class WorkspaceFiles {
2023
2943
  i.generation++;
2024
2944
  }
2025
2945
  }
2026
- const listing = getOrThrow(result);
2946
+ const listing = getOrThrow2(result);
2027
2947
  controller.signal.throwIfAborted();
2028
2948
  if (this.#closed || item.generation !== generation)
2029
2949
  throw new Error("Workspace changed during file search. Retry.");
@@ -2043,7 +2963,7 @@ class WorkspaceFiles {
2043
2963
  throw new Error("File environment closed");
2044
2964
  query = relativeQuery(query);
2045
2965
  const slash = query.lastIndexOf("/"), prefix = slash < 0 ? "" : query.slice(0, slash + 1), needle = query.slice(slash + 1);
2046
- root = await realpath(root);
2966
+ root = await realpath2(root);
2047
2967
  const base = await workspaceTarget(root, prefix);
2048
2968
  if (!(await stat(base)).isDirectory())
2049
2969
  throw new Error("Workspace directory is unavailable.");
@@ -2144,7 +3064,7 @@ function cancellable(work, signal) {
2144
3064
  // packages/core/src/runtime.ts
2145
3065
  var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
2146
3066
  var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
2147
- var ArchivedDoc = defineDoc2({
3067
+ var ArchivedDoc = defineDoc4({
2148
3068
  kind: "app.neko.conversation",
2149
3069
  version: 1,
2150
3070
  scope: "conversation",
@@ -2152,7 +3072,7 @@ var ArchivedDoc = defineDoc2({
2152
3072
  fork: "initial",
2153
3073
  initial: () => ({ archived: false })
2154
3074
  });
2155
- var WorkspacesDoc = defineDoc2({
3075
+ var WorkspacesDoc = defineDoc4({
2156
3076
  kind: "app.neko.workspaces",
2157
3077
  version: 1,
2158
3078
  scope: "conversation",
@@ -2160,7 +3080,7 @@ var WorkspacesDoc = defineDoc2({
2160
3080
  fork: "initial",
2161
3081
  initial: () => ({ items: [] })
2162
3082
  });
2163
- var ActivityDoc = defineDoc2({
3083
+ var ActivityDoc = defineDoc4({
2164
3084
  kind: "app.neko.activity",
2165
3085
  version: 1,
2166
3086
  scope: "conversation",
@@ -2169,7 +3089,7 @@ var ActivityDoc = defineDoc2({
2169
3089
  initial: () => ({ lastUsedAt: 0 })
2170
3090
  });
2171
3091
  async function createNekoRuntime(options = {}) {
2172
- const context = options.context ?? BACKGROUND_CONTEXT2;
3092
+ const context = options.context ?? BACKGROUND_CONTEXT3;
2173
3093
  const defaultCwd = normalizeWorkspace(options.cwd);
2174
3094
  const model = options.model ?? DEFAULT_MODEL;
2175
3095
  const providers = options.providers ?? [];
@@ -2205,6 +3125,7 @@ async function createNekoRuntime(options = {}) {
2205
3125
  const registry = createRegistry();
2206
3126
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
2207
3127
  registry.install(ImageDetails);
3128
+ registry.install(AgentFileSearch);
2208
3129
  registry.install(createWebSearchTools(async () => {
2209
3130
  const saved = await effectiveCredentials.read("tavily");
2210
3131
  if (saved?.type === "api_key" && saved.key)
@@ -2214,8 +3135,14 @@ async function createNekoRuntime(options = {}) {
2214
3135
  const loadResources = createResourceLoader(options.promptHome);
2215
3136
  const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
2216
3137
  registry.install(createNekoPrompt(resourceSnapshots.current));
3138
+ registry.install(createSubagentExtension(async (id, requestId, cwd) => {
3139
+ resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
3140
+ }, resourceSnapshots.discard));
2217
3141
  for (const extension of options.extensions ?? [])
2218
3142
  registry.install(extension);
3143
+ let approvals;
3144
+ const approvalExtension = createApprovalExtension(() => approvals);
3145
+ registry.install(approvalExtension);
2219
3146
  const harness = await Harness.open(storage, {
2220
3147
  models,
2221
3148
  registry,
@@ -2235,8 +3162,11 @@ async function createNekoRuntime(options = {}) {
2235
3162
  } while (cursor);
2236
3163
  return records;
2237
3164
  }, context);
3165
+ approvals = new ToolApprovals(harness, context);
2238
3166
  const existing = await scanConversations();
2239
3167
  try {
3168
+ await installSubagentCoordinator(harness, existing.map((r) => r.id), context);
3169
+ await prepareApprovalConversations(harness, existing.map((r) => r.id), approvalExtension, context);
2240
3170
  for (const record of existing) {
2241
3171
  const target = await harness.conversation(record.id, context);
2242
3172
  if (target) {
@@ -2287,7 +3217,7 @@ async function createNekoRuntime(options = {}) {
2287
3217
  if (existing.length === 0)
2288
3218
  await metadata.initialize(conversation.id);
2289
3219
  for (const record of existing)
2290
- if (!(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
3220
+ if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
2291
3221
  await metadata.restore(record.id);
2292
3222
  let closing;
2293
3223
  const cleanup = () => {
@@ -2389,7 +3319,7 @@ async function createNekoRuntime(options = {}) {
2389
3319
  await metadata.flush();
2390
3320
  await metadata.reconcile(target.id);
2391
3321
  if (settled.status === "done" && settled.type === "input") {
2392
- const entry = await target.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
3322
+ const entry = await target.commit((tx) => tx.entry(AssistantEntry2, settled.answer), context);
2393
3323
  const message = entry?.model?.[0];
2394
3324
  const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
2395
3325
  return { status: "done", text };
@@ -2420,7 +3350,7 @@ async function createNekoRuntime(options = {}) {
2420
3350
  return watchEvents(harness, target.id, context);
2421
3351
  },
2422
3352
  async usage() {
2423
- return mergeUsage(await harness.snapshot(UsageDoc, target.id, context) ?? { models: {}, tools: {} }, (await harness.snapshot(SessionMetadataDoc, target.id, context))?.namingUsage ?? { models: {}, tools: {} });
3353
+ return mergeUsage(await harness.snapshot(UsageDoc2, target.id, context) ?? { models: {}, tools: {} }, (await harness.snapshot(SessionMetadataDoc, target.id, context))?.namingUsage ?? { models: {}, tools: {} });
2424
3354
  },
2425
3355
  compactionRecord() {
2426
3356
  return readCompaction(harness, storage, target.id, context);
@@ -2454,7 +3384,8 @@ async function createNekoRuntime(options = {}) {
2454
3384
  if (target === undefined)
2455
3385
  return;
2456
3386
  const archived = (await harness.snapshot(ArchivedDoc, id, context))?.archived ?? false;
2457
- return archived ? undefined : makeSession(target);
3387
+ const record = await harness.commit((tx) => tx.conversation(id), context);
3388
+ return archived || record?.owner ? undefined : makeSession(target);
2458
3389
  };
2459
3390
  return {
2460
3391
  context,
@@ -2468,7 +3399,7 @@ async function createNekoRuntime(options = {}) {
2468
3399
  async globalUsage() {
2469
3400
  let total = { models: {}, tools: {} };
2470
3401
  for (const record of await scanConversations()) {
2471
- total = mergeUsage(total, await harness.snapshot(UsageDoc, record.id, context) ?? { models: {}, tools: {} });
3402
+ total = mergeUsage(total, await harness.snapshot(UsageDoc2, record.id, context) ?? { models: {}, tools: {} });
2472
3403
  total = mergeUsage(total, (await harness.snapshot(SessionMetadataDoc, record.id, context))?.namingUsage ?? { models: {}, tools: {} });
2473
3404
  }
2474
3405
  return total;
@@ -2485,24 +3416,70 @@ async function createNekoRuntime(options = {}) {
2485
3416
  const newest = await source.conversation.entries({}, 1, undefined, context);
2486
3417
  const at = newest.items[0]?.id;
2487
3418
  const sourceMetadata = at === undefined ? undefined : await harness.snapshot(SessionMetadataDoc, id, context);
2488
- const child = at === undefined ? await harness.createConversation({
2489
- ownership: { kind: "ownerless" },
2490
- agent: { model: agent.model, thinkingLevel: agent.thinkingLevel, cwd: savedWorkspace(agent.cwd) }
2491
- }, context) : await source.conversation.fork(at, {
2492
- ownership: { kind: "ownerless" },
2493
- init: async (tx, childId) => {
2494
- const doc = await tx.doc(SessionMetadataDoc, childId);
3419
+ const childId = await harness.commit(async (tx) => {
3420
+ const inherited = [];
3421
+ if (at !== undefined) {
3422
+ const references = await refreshDelegations(tx, id);
3423
+ const calls = new Set;
3424
+ let cursor;
3425
+ do {
3426
+ const page = await tx.scanEntries({ conversationId: id, maxEntryId: at }, 100, cursor);
3427
+ for (const entry of page.items)
3428
+ for (const message of entry.model ?? []) {
3429
+ if (message.role === "assistant")
3430
+ for (const block of message.content) {
3431
+ if (block.type === "toolCall")
3432
+ calls.add(block.id);
3433
+ }
3434
+ }
3435
+ cursor = page.next;
3436
+ } while (cursor);
3437
+ const frozen = new Map;
3438
+ for (const reference of references) {
3439
+ if (!calls.has(reference.callId))
3440
+ continue;
3441
+ let snapshot = frozen.get(reference.childId);
3442
+ if (!snapshot) {
3443
+ const latest = await tx.scanEntries({ conversationId: reference.childId }, 1);
3444
+ snapshot = {
3445
+ through: latest.items[0]?.id ?? 0,
3446
+ state: JSON.parse(JSON.stringify(await tx.doc(UsageDoc2, reference.childId)))
3447
+ };
3448
+ frozen.set(reference.childId, snapshot);
3449
+ }
3450
+ const usage = totalUsage(snapshot.state);
3451
+ inherited.push({
3452
+ ...reference,
3453
+ historyThrough: reference.historyThrough ?? snapshot.through,
3454
+ roundUsage: reference.roundUsage ?? (reference.usageStart ? subagentUsageTotals(subtractSubagentUsage(snapshot.state, reference.usageStart)) : null),
3455
+ historyUsage: reference.historyUsage ?? {
3456
+ input: usage.input,
3457
+ output: usage.output,
3458
+ cacheRead: usage.cacheRead,
3459
+ cacheWrite: usage.cacheWrite,
3460
+ cost: usage.cost.total
3461
+ }
3462
+ });
3463
+ }
3464
+ }
3465
+ const child = at === undefined ? await tx.createConversation({ ownership: { kind: "ownerless" } }) : await tx.forkConversation(id, at, { ownership: { kind: "ownerless" } });
3466
+ (await tx.doc(DelegationsDoc, child.id)).items = inherited;
3467
+ if (sourceMetadata) {
3468
+ const doc = await tx.doc(SessionMetadataDoc, child.id);
2495
3469
  doc.title = sourceMetadata.title || sourceMetadata.firstPromptPreview;
2496
3470
  doc.firstPromptPreview = sourceMetadata.firstPromptPreview;
2497
3471
  doc.naming = "ready";
2498
3472
  }
3473
+ return child.id;
2499
3474
  }, context);
3475
+ const child = await harness.conversation(childId, context);
2500
3476
  await child.configure({ model: agent.model, thinkingLevel: agent.thinkingLevel, cwd: savedWorkspace(agent.cwd) }, context);
2501
3477
  await metadata.initialize(child.id);
2502
3478
  await touchConversation(child.id);
2503
3479
  return makeSession(child);
2504
3480
  },
2505
3481
  harness,
3482
+ approvals,
2506
3483
  closed: harness.closed,
2507
3484
  workspaceFiles,
2508
3485
  conversation,
@@ -2528,6 +3505,8 @@ async function createNekoRuntime(options = {}) {
2528
3505
  const records = await scanConversations();
2529
3506
  const infos = [];
2530
3507
  for (const record of records) {
3508
+ if (record.owner)
3509
+ continue;
2531
3510
  const archived = (await harness.snapshot(ArchivedDoc, record.id, context))?.archived ?? false;
2532
3511
  if (!archived) {
2533
3512
  const target = await harness.conversation(record.id, context);
@@ -2566,6 +3545,7 @@ async function createNekoRuntime(options = {}) {
2566
3545
  },
2567
3546
  getConversation,
2568
3547
  async archiveConversation(id) {
3548
+ await (await sessionFor(id)).abort();
2569
3549
  await metadata.archive(id);
2570
3550
  await harness.commit(async (tx) => {
2571
3551
  (await tx.doc(ArchivedDoc, id)).archived = true;
@@ -2690,13 +3670,13 @@ async function removeGatewayPortFile(nekoDir, serverId) {
2690
3670
  }
2691
3671
 
2692
3672
  // packages/gateway/src/server-services.ts
2693
- import { randomUUID as randomUUID2 } from "crypto";
3673
+ import { randomUUID as randomUUID3 } from "crypto";
2694
3674
  import {
2695
- ChordError as ChordError2,
2696
- cell as cell2,
2697
- hostPrincipal as hostPrincipal2,
2698
- listener as listener2,
2699
- local as local2,
3675
+ ChordError as ChordError3,
3676
+ cell as cell3,
3677
+ hostPrincipal as hostPrincipal3,
3678
+ listener as listener3,
3679
+ local as local3,
2700
3680
  method as method2
2701
3681
  } from "@earendil-works/chord";
2702
3682
 
@@ -2707,8 +3687,8 @@ import { dirname as dirname5, join as join7 } from "path";
2707
3687
  import { fileURLToPath as fileURLToPath2 } from "url";
2708
3688
 
2709
3689
  // packages/core/src/application-version.ts
2710
- var RUNNING_VERSION = "0.2.9";
2711
- var APPLICATION_PROTOCOL = 1;
3690
+ var RUNNING_VERSION = "0.3.1";
3691
+ var APPLICATION_PROTOCOL = 4;
2712
3692
  function stableVersion(value) {
2713
3693
  return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value) && value.split(".").every((n) => Number.isSafeInteger(Number(n)));
2714
3694
  }
@@ -2857,11 +3837,11 @@ class UpdateChecker {
2857
3837
  }
2858
3838
 
2859
3839
  // packages/gateway/src/server-services.ts
2860
- import { Type as Type4 } from "typebox";
3840
+ import { Type as Type7 } from "typebox";
2861
3841
 
2862
3842
  // packages/gateway/src/contract.ts
2863
3843
  import { method, remote } from "@earendil-works/chord";
2864
- import { Type as Type2 } from "typebox";
3844
+ import { Type as Type4 } from "typebox";
2865
3845
  var rpc = (args, result) => [args, result];
2866
3846
  function contract(name, def) {
2867
3847
  const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
@@ -2873,19 +3853,19 @@ function contract(name, def) {
2873
3853
  ]));
2874
3854
  return remote(name, { ...def, variants });
2875
3855
  }
2876
- var str = Type2.String();
2877
- var num = Type2.Number();
2878
- var bool = Type2.Boolean();
2879
- var nil = Type2.Null();
2880
- var integer = Type2.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
2881
- var nullableString = Type2.Union([str, nil]);
2882
- var phase = Type2.Enum(["pending", "running", "done"]);
2883
- var level = Type2.Enum(["project", "user", "builtin"]);
2884
- var thinking = Type2.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
2885
- var model = Type2.Object({ provider: str, modelId: str });
2886
- var change = Type2.Object({ model: Type2.Optional(model), thinkingLevel: Type2.Optional(thinking) }, { additionalProperties: false });
2887
- var usage = Type2.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
2888
- var image = Type2.Object({
3856
+ var str = Type4.String();
3857
+ var num = Type4.Number();
3858
+ var bool = Type4.Boolean();
3859
+ var nil = Type4.Null();
3860
+ var integer = Type4.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
3861
+ var nullableString = Type4.Union([str, nil]);
3862
+ var phase = Type4.Enum(["pending", "running", "done"]);
3863
+ var level = Type4.Enum(["project", "user", "builtin"]);
3864
+ var thinking = Type4.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
3865
+ var model = Type4.Object({ provider: str, modelId: str });
3866
+ var change = Type4.Object({ model: Type4.Optional(model), thinkingLevel: Type4.Optional(thinking) }, { additionalProperties: false });
3867
+ var usage = Type4.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
3868
+ var image = Type4.Object({
2889
3869
  name: str,
2890
3870
  mimeType: str,
2891
3871
  width: integer,
@@ -2894,8 +3874,8 @@ var image = Type2.Object({
2894
3874
  entryId: integer,
2895
3875
  contentIndex: integer
2896
3876
  });
2897
- var skill = Type2.Object({ name: str, location: str, body: str, args: str });
2898
- var call = Type2.Object({
3877
+ var skill = Type4.Object({ name: str, location: str, body: str, args: str });
3878
+ var call = Type4.Object({
2899
3879
  callId: str,
2900
3880
  name: str,
2901
3881
  summary: str,
@@ -2903,44 +3883,90 @@ var call = Type2.Object({
2903
3883
  phase,
2904
3884
  output: nullableString
2905
3885
  });
2906
- var ChatViewSchema = Type2.Object({
3886
+ var DelegationSchema = Type4.Object({
3887
+ parentId: integer,
3888
+ agentId: Type4.Optional(str),
3889
+ round: Type4.Optional(integer),
3890
+ runnerId: Type4.Optional(integer),
3891
+ inputEntryId: Type4.Optional(integer),
3892
+ answerEntryId: Type4.Optional(integer),
3893
+ roundUsage: Type4.Optional(Type4.Union([usage, nil])),
3894
+ taskId: integer,
3895
+ callId: str,
3896
+ childId: integer,
3897
+ role: Type4.Enum(["explore", "plan", "coder"]),
3898
+ title: str,
3899
+ createdAt: num,
3900
+ startedAt: Type4.Union([num, nil]),
3901
+ endedAt: Type4.Union([num, nil]),
3902
+ status: Type4.Enum(["queued", "running", "completed", "failed", "cancelled"]),
3903
+ report: nullableString,
3904
+ approvalCount: Type4.Optional(integer),
3905
+ historyThrough: Type4.Optional(integer),
3906
+ historyUsage: Type4.Optional(usage)
3907
+ });
3908
+ var SubagentSummarySchema = Type4.Object({
3909
+ agentId: str,
3910
+ parentId: integer,
3911
+ childId: integer,
3912
+ role: Type4.Enum(["explore", "plan", "coder"]),
3913
+ title: str,
3914
+ status: Type4.Enum(["idle", "queued", "running"]),
3915
+ activeRoundId: Type4.Union([integer, nil]),
3916
+ lastRoundId: integer,
3917
+ lastReport: Type4.Optional(nullableString),
3918
+ roundCount: integer,
3919
+ taskTitle: str,
3920
+ approvalCount: integer,
3921
+ createdAt: num,
3922
+ historyThrough: Type4.Optional(integer),
3923
+ historyUsage: Type4.Optional(usage)
3924
+ });
3925
+ var ChatViewSchema = Type4.Object({
3926
+ historyBefore: Type4.Optional(Type4.Union([integer, nil])),
3927
+ agents: Type4.Optional(Type4.Array(SubagentSummarySchema)),
3928
+ rounds: Type4.Optional(Type4.Array(DelegationSchema)),
3929
+ subagents: Type4.Optional(Type4.Array(DelegationSchema)),
3930
+ taskUsage: Type4.Optional(usage),
2907
3931
  model: str,
2908
- agent: Type2.Object({ model: Type2.Union([model, nil]), thinkingLevel: Type2.Union([thinking, nil]) }),
3932
+ agent: Type4.Object({ model: Type4.Union([model, nil]), thinkingLevel: Type4.Union([thinking, nil]) }),
2909
3933
  cwd: str,
2910
3934
  busy: bool,
2911
3935
  status: nullableString,
2912
3936
  usage,
2913
- contextUsage: Type2.Optional(Type2.Object({ tokens: Type2.Union([num, nil]), window: num, percent: Type2.Union([num, nil]) })),
2914
- compaction: Type2.Optional(Type2.Object({
2915
- failure: Type2.Optional(Type2.Object({ taskId: integer, reason: Type2.Enum(["manual", "threshold", "overflow"]), error: str })),
2916
- active: Type2.Array(Type2.Object({
3937
+ contextUsage: Type4.Optional(Type4.Object({ tokens: Type4.Union([num, nil]), window: num, percent: Type4.Union([num, nil]) })),
3938
+ compaction: Type4.Optional(Type4.Object({
3939
+ failure: Type4.Optional(Type4.Object({ taskId: integer, reason: Type4.Enum(["manual", "threshold", "overflow"]), error: str })),
3940
+ active: Type4.Array(Type4.Object({
2917
3941
  taskId: integer,
2918
- reason: Type2.Enum(["manual", "threshold", "overflow"]),
3942
+ reason: Type4.Enum(["manual", "threshold", "overflow"]),
2919
3943
  blocking: bool,
2920
3944
  attempt: integer,
2921
- retry: Type2.Optional(Type2.Object({ at: num, error: str }))
3945
+ retry: Type4.Optional(Type4.Object({ at: num, error: str }))
2922
3946
  })),
2923
- latest: Type2.Union([
3947
+ latest: Type4.Union([
2924
3948
  nil,
2925
- Type2.Object({
3949
+ Type4.Object({
2926
3950
  taskId: integer,
2927
- phase: Type2.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
3951
+ phase: Type4.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
2928
3952
  at: num
2929
3953
  })
2930
3954
  ])
2931
3955
  })),
2932
- items: Type2.Array(Type2.Union([
2933
- Type2.Object({
2934
- type: Type2.Literal("user"),
3956
+ items: Type4.Array(Type4.Union([
3957
+ Type4.Object({
3958
+ type: Type4.Literal("user"),
3959
+ order: Type4.Optional(num),
2935
3960
  key: str,
2936
3961
  id: integer,
2937
3962
  text: str,
2938
- images: Type2.Optional(Type2.Array(image)),
2939
- skill: Type2.Optional(skill),
2940
- defaultImagePrompt: Type2.Optional(Type2.Object({ version: Type2.Integer({ minimum: 1 }), text: str }))
3963
+ images: Type4.Optional(Type4.Array(image)),
3964
+ skill: Type4.Optional(skill),
3965
+ defaultImagePrompt: Type4.Optional(Type4.Object({ version: Type4.Integer({ minimum: 1 }), text: str }))
2941
3966
  }),
2942
- Type2.Object({
2943
- type: Type2.Literal("assistant"),
3967
+ Type4.Object({
3968
+ type: Type4.Literal("assistant"),
3969
+ order: Type4.Optional(num),
2944
3970
  key: str,
2945
3971
  id: integer,
2946
3972
  thinking: str,
@@ -2949,10 +3975,11 @@ var ChatViewSchema = Type2.Object({
2949
3975
  errorMessage: nullableString,
2950
3976
  note: nullableString
2951
3977
  }),
2952
- Type2.Object({
2953
- type: Type2.Literal("tool"),
3978
+ Type4.Object({
3979
+ type: Type4.Literal("tool"),
3980
+ order: Type4.Optional(num),
2954
3981
  key: str,
2955
- images: Type2.Optional(Type2.Array(image)),
3982
+ images: Type4.Optional(Type4.Array(image)),
2956
3983
  callId: str,
2957
3984
  name: str,
2958
3985
  summary: str,
@@ -2961,50 +3988,50 @@ var ChatViewSchema = Type2.Object({
2961
3988
  isError: bool,
2962
3989
  output: nullableString,
2963
3990
  result: nullableString,
2964
- durationMs: Type2.Union([num, nil]),
3991
+ durationMs: Type4.Union([num, nil]),
2965
3992
  note: nullableString
2966
3993
  }),
2967
- Type2.Object({ type: Type2.Literal("system"), key: str, id: integer, text: str })
3994
+ Type4.Object({ type: Type4.Literal("system"), order: Type4.Optional(num), key: str, id: integer, text: str })
2968
3995
  ])),
2969
- streaming: Type2.Union([nil, Type2.Object({ thinking: str, text: str, toolCalls: Type2.Array(call) })])
3996
+ streaming: Type4.Union([nil, Type4.Object({ thinking: str, text: str, toolCalls: Type4.Array(call) })])
2970
3997
  });
2971
- var SessionSummarySchema = Type2.Object({
3998
+ var SessionSummarySchema = Type4.Object({
2972
3999
  workspaceRoot: str,
2973
4000
  lastUsedAt: num,
2974
4001
  title: str,
2975
4002
  firstPromptPreview: str,
2976
4003
  createdAt: num,
2977
- lastMessageAt: Type2.Union([num, nil]),
2978
- model: Type2.Union([model, nil]),
2979
- status: Type2.Enum(["idle", "running", "error"]),
4004
+ lastMessageAt: Type4.Union([num, nil]),
4005
+ model: Type4.Union([model, nil]),
4006
+ status: Type4.Enum(["idle", "running", "error"]),
2980
4007
  id: str
2981
4008
  });
2982
- var DirectoryStateSchema = Type2.Object({ revision: integer, sessions: Type2.Array(SessionSummarySchema) });
2983
- var WorkspaceSummarySchema = Type2.Object({ path: str, createdAt: num });
2984
- var WorkspaceStateSchema = Type2.Object({ revision: integer, workspaces: Type2.Array(WorkspaceSummarySchema) });
2985
- var credentialStatus = Type2.Object({
4009
+ var DirectoryStateSchema = Type4.Object({ revision: integer, sessions: Type4.Array(SessionSummarySchema) });
4010
+ var WorkspaceSummarySchema = Type4.Object({ path: str, createdAt: num });
4011
+ var WorkspaceStateSchema = Type4.Object({ revision: integer, workspaces: Type4.Array(WorkspaceSummarySchema) });
4012
+ var credentialStatus = Type4.Object({
2986
4013
  savedConfigured: bool,
2987
4014
  environmentConfigured: bool,
2988
- source: Type2.Enum(["saved", "environment", "runtime", "keyless", "none"])
4015
+ source: Type4.Enum(["saved", "environment", "runtime", "keyless", "none"])
2989
4016
  });
2990
- var CredentialStateSchema = Type2.Object({
4017
+ var CredentialStateSchema = Type4.Object({
2991
4018
  revision: integer,
2992
- statuses: Type2.Object({ deepseek: credentialStatus, tavily: credentialStatus })
4019
+ statuses: Type4.Object({ deepseek: credentialStatus, tavily: credentialStatus })
2993
4020
  });
2994
- var AttachmentSchema = Type2.Object({ sessionId: str, attachmentId: str });
2995
- var chatInput = Type2.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
4021
+ var AttachmentSchema = Type4.Object({ sessionId: str, attachmentId: str });
4022
+ var chatInput = Type4.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
2996
4023
  var NekoSessionDirectory = contract("neko.session-directory", {
2997
4024
  version: 1,
2998
- input: Type2.Object({}, { additionalProperties: false }),
4025
+ input: Type4.Object({}, { additionalProperties: false }),
2999
4026
  variants: { ready: { state: { state: DirectoryStateSchema } } }
3000
4027
  });
3001
4028
  var NekoSessionManagement = contract("neko.session-management", {
3002
4029
  version: 1,
3003
- input: Type2.Object({}, { additionalProperties: false }),
4030
+ input: Type4.Object({}, { additionalProperties: false }),
3004
4031
  variants: {
3005
4032
  ready: {
3006
4033
  methods: {
3007
- create: rpc(Type2.Object({ ...change.properties, cwd: Type2.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
4034
+ create: rpc(Type4.Object({ ...change.properties, cwd: Type4.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
3008
4035
  archive: rpc(str, nil),
3009
4036
  attach: rpc(str, AttachmentSchema),
3010
4037
  detach: rpc(nil, nil),
@@ -3013,30 +4040,30 @@ var NekoSessionManagement = contract("neko.session-management", {
3013
4040
  }
3014
4041
  }
3015
4042
  });
3016
- var resize = Type2.Object({
3017
- maxWidth: Type2.Optional(integer),
3018
- maxHeight: Type2.Optional(integer),
3019
- maxBytes: Type2.Optional(integer),
3020
- jpegQuality: Type2.Optional(integer)
4043
+ var resize = Type4.Object({
4044
+ maxWidth: Type4.Optional(integer),
4045
+ maxHeight: Type4.Optional(integer),
4046
+ maxBytes: Type4.Optional(integer),
4047
+ jpegQuality: Type4.Optional(integer)
3021
4048
  });
3022
- var inputLimits = Type2.Object({
3023
- maxRequestBytes: Type2.Optional(integer),
3024
- images: Type2.Optional(Type2.Object({
3025
- resize: Type2.Optional(resize),
3026
- maxPerMessage: Type2.Optional(integer),
3027
- maxPerRequest: Type2.Optional(integer)
4049
+ var inputLimits = Type4.Object({
4050
+ maxRequestBytes: Type4.Optional(integer),
4051
+ images: Type4.Optional(Type4.Object({
4052
+ resize: Type4.Optional(resize),
4053
+ maxPerMessage: Type4.Optional(integer),
4054
+ maxPerRequest: Type4.Optional(integer)
3028
4055
  }))
3029
4056
  });
3030
- var modelInfo = Type2.Object({
4057
+ var modelInfo = Type4.Object({
3031
4058
  provider: str,
3032
4059
  modelId: str,
3033
4060
  name: str,
3034
4061
  contextWindow: num,
3035
- input: Type2.Array(str),
3036
- inputLimits: Type2.Optional(inputLimits),
4062
+ input: Type4.Array(str),
4063
+ inputLimits: Type4.Optional(inputLimits),
3037
4064
  reasoning: bool,
3038
- supportedThinkingLevels: Type2.Array(thinking),
3039
- thinkingLevelAdjustments: Type2.Object({
4065
+ supportedThinkingLevels: Type4.Array(thinking),
4066
+ thinkingLevelAdjustments: Type4.Object({
3040
4067
  off: thinking,
3041
4068
  minimal: thinking,
3042
4069
  low: thinking,
@@ -3048,10 +4075,35 @@ var modelInfo = Type2.Object({
3048
4075
  });
3049
4076
  var NekoModels = contract("neko.models", {
3050
4077
  version: 1,
3051
- input: Type2.Object({}, { additionalProperties: false }),
3052
- variants: { ready: { methods: { list: rpc(nil, Type2.Array(modelInfo)) } } }
4078
+ input: Type4.Object({}, { additionalProperties: false }),
4079
+ variants: { ready: { methods: { list: rpc(nil, Type4.Array(modelInfo)) } } }
4080
+ });
4081
+ var imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
4082
+ var NekoSubagent = contract("neko.subagent", {
4083
+ version: 1,
4084
+ input: Type4.Object({
4085
+ sessionId: str,
4086
+ attachmentId: str,
4087
+ taskId: Type4.Optional(integer),
4088
+ agentId: Type4.Optional(str)
4089
+ }),
4090
+ variants: {
4091
+ ready: {
4092
+ state: { revision: integer },
4093
+ methods: {
4094
+ chunk: rpc(Type4.Object({
4095
+ kind: Type4.Enum(["view", "history"]),
4096
+ before: Type4.Optional(integer),
4097
+ token: Type4.Optional(integer),
4098
+ offset: integer
4099
+ }), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
4100
+ getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
4101
+ decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
4102
+ stop: rpc(Type4.Union([nil, Type4.Object({ taskId: integer })]), nil)
4103
+ }
4104
+ }
4105
+ }
3053
4106
  });
3054
- var imageChunk = Type2.Object({ data: str, total: integer, mimeType: str });
3055
4107
  var NekoChat = contract("neko.chat", {
3056
4108
  version: 1,
3057
4109
  input: chatInput,
@@ -3059,79 +4111,79 @@ var NekoChat = contract("neko.chat", {
3059
4111
  ready: {
3060
4112
  state: { view: ChatViewSchema },
3061
4113
  methods: {
3062
- submit: rpc(Type2.Object({
4114
+ submit: rpc(Type4.Object({
3063
4115
  content: str,
3064
- images: Type2.Optional(Type2.Array(str, { maxItems: 4 })),
3065
- requestId: Type2.Optional(str),
3066
- whenBusy: Type2.Optional(Type2.Enum(["steer", "followUp"]))
4116
+ images: Type4.Optional(Type4.Array(str, { maxItems: 4 })),
4117
+ requestId: Type4.Optional(str),
4118
+ whenBusy: Type4.Optional(Type4.Enum(["steer", "followUp"]))
3067
4119
  }, { additionalProperties: false }), nil),
3068
4120
  abort: rpc(nil, nil),
3069
4121
  usage: rpc(nil, usage),
3070
- skills: rpc(nil, Type2.Array(Type2.Object({ name: str, description: str, location: str, level }))),
4122
+ skills: rpc(nil, Type4.Array(Type4.Object({ name: str, description: str, location: str, level }))),
3071
4123
  configure: rpc(change, nil),
3072
- compact: rpc(Type2.Object({ instructions: Type2.Optional(str) }, { additionalProperties: false }), nil),
3073
- importImage: rpc(str, Type2.Object({ id: str, name: str })),
3074
- draftImage: rpc(Type2.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
3075
- beginImage: rpc(Type2.Object({ name: str, mimeType: str, size: Type2.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
3076
- appendImage: rpc(Type2.Object({ id: str, offset: integer, data: Type2.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
4124
+ compact: rpc(Type4.Object({ instructions: Type4.Optional(str) }, { additionalProperties: false }), nil),
4125
+ importImage: rpc(str, Type4.Object({ id: str, name: str })),
4126
+ draftImage: rpc(Type4.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
4127
+ beginImage: rpc(Type4.Object({ name: str, mimeType: str, size: Type4.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
4128
+ appendImage: rpc(Type4.Object({ id: str, offset: integer, data: Type4.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
3077
4129
  finishImage: rpc(str, nil),
3078
4130
  releaseImage: rpc(str, nil),
3079
- getImage: rpc(Type2.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
4131
+ getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
3080
4132
  }
3081
4133
  }
3082
4134
  }
3083
4135
  });
3084
- var browse = Type2.Object({ path: str, query: Type2.Optional(str), showHidden: Type2.Optional(bool), cursor: Type2.Optional(str) }, { additionalProperties: false });
3085
- var page = Type2.Object({
4136
+ var browse = Type4.Object({ path: str, query: Type4.Optional(str), showHidden: Type4.Optional(bool), cursor: Type4.Optional(str) }, { additionalProperties: false });
4137
+ var page = Type4.Object({
3086
4138
  path: str,
3087
4139
  parent: nullableString,
3088
- entries: Type2.Array(Type2.Object({ name: str, path: str, error: Type2.Optional(str) })),
4140
+ entries: Type4.Array(Type4.Object({ name: str, path: str, error: Type4.Optional(str) })),
3089
4141
  next: nullableString
3090
4142
  });
3091
4143
  var NekoWorkspaces = contract("neko.workspaces", {
3092
4144
  version: 1,
3093
- input: Type2.Object({}, { additionalProperties: false }),
4145
+ input: Type4.Object({}, { additionalProperties: false }),
3094
4146
  variants: {
3095
4147
  ready: {
3096
4148
  state: { state: WorkspaceStateSchema },
3097
4149
  methods: {
3098
- add: rpc(Type2.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
4150
+ add: rpc(Type4.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
3099
4151
  browse: rpc(browse, page)
3100
4152
  }
3101
4153
  }
3102
4154
  }
3103
4155
  });
3104
- var provider = Type2.Enum(["deepseek", "tavily"]);
4156
+ var provider = Type4.Enum(["deepseek", "tavily"]);
3105
4157
  var NekoCredentials = contract("neko.credentials", {
3106
4158
  version: 1,
3107
- input: Type2.Object({}, { additionalProperties: false }),
4159
+ input: Type4.Object({}, { additionalProperties: false }),
3108
4160
  variants: {
3109
4161
  ready: {
3110
4162
  state: { state: CredentialStateSchema },
3111
4163
  methods: {
3112
- save: rpc(Type2.Object({ provider, key: Type2.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
3113
- remove: rpc(Type2.Object({ provider }, { additionalProperties: false }), nil)
4164
+ save: rpc(Type4.Object({ provider, key: Type4.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
4165
+ remove: rpc(Type4.Object({ provider }, { additionalProperties: false }), nil)
3114
4166
  }
3115
4167
  }
3116
4168
  }
3117
4169
  });
3118
- var FileCandidatesSchema = Type2.Object({
3119
- entries: Type2.Array(Type2.Object({ name: str, path: str, kind: Type2.Enum(["file", "directory"]) }), { maxItems: 100 }),
4170
+ var FileCandidatesSchema = Type4.Object({
4171
+ entries: Type4.Array(Type4.Object({ name: str, path: str, kind: Type4.Enum(["file", "directory"]) }), { maxItems: 100 }),
3120
4172
  limited: bool,
3121
4173
  incomplete: bool
3122
4174
  });
3123
4175
  var NekoWorkspaceFiles = contract("neko.workspace-files", {
3124
4176
  version: 1,
3125
- input: Type2.Object({}, { additionalProperties: false }),
4177
+ input: Type4.Object({}, { additionalProperties: false }),
3126
4178
  variants: {
3127
4179
  ready: {
3128
4180
  methods: {
3129
- search: rpc(Type2.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
4181
+ search: rpc(Type4.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
3130
4182
  }
3131
4183
  }
3132
4184
  }
3133
4185
  });
3134
- var marketSkill = Type2.Object({
4186
+ var marketSkill = Type4.Object({
3135
4187
  name: str,
3136
4188
  description: str,
3137
4189
  category: str,
@@ -3141,38 +4193,38 @@ var marketSkill = Type2.Object({
3141
4193
  sha256: str,
3142
4194
  sizeBytes: integer,
3143
4195
  uploadedAt: str,
3144
- installed: Type2.Object({ project: bool, user: bool, builtin: bool }),
3145
- activeLevel: Type2.Union([level, nil])
4196
+ installed: Type4.Object({ project: bool, user: bool, builtin: bool }),
4197
+ activeLevel: Type4.Union([level, nil])
3146
4198
  });
3147
- var preview = Type2.Object({ body: str, files: Type2.Array(str) });
3148
- var scope = Type2.Enum(["user", "project"]);
3149
- var installResult = Type2.Object({ name: str, scope, path: str, shadowedBy: Type2.Optional(scope) });
4199
+ var preview = Type4.Object({ body: str, files: Type4.Array(str) });
4200
+ var scope = Type4.Enum(["user", "project"]);
4201
+ var installResult = Type4.Object({ name: str, scope, path: str, shadowedBy: Type4.Optional(scope) });
3150
4202
  var NekoSkillMarket = contract("neko.skill-market", {
3151
4203
  version: 1,
3152
- input: Type2.Object({}, { additionalProperties: false }),
4204
+ input: Type4.Object({}, { additionalProperties: false }),
3153
4205
  variants: {
3154
4206
  ready: {
3155
4207
  methods: {
3156
- list: rpc(Type2.Object({ query: Type2.Optional(str), workspaceRoot: Type2.Optional(str) }, { additionalProperties: false }), Type2.Array(marketSkill)),
3157
- detail: rpc(Type2.Object({ name: str, workspaceRoot: Type2.Optional(str) }, { additionalProperties: false }), marketSkill),
3158
- preview: rpc(Type2.Object({ name: str }, { additionalProperties: false }), preview),
3159
- install: rpc(Type2.Object({ name: str, scope, workspaceRoot: Type2.Optional(str), overwrite: Type2.Optional(bool) }, { additionalProperties: false }), installResult),
3160
- uninstall: rpc(Type2.Object({ name: str, scope, workspaceRoot: Type2.Optional(str) }, { additionalProperties: false }), nil)
4208
+ list: rpc(Type4.Object({ query: Type4.Optional(str), workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), Type4.Array(marketSkill)),
4209
+ detail: rpc(Type4.Object({ name: str, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), marketSkill),
4210
+ preview: rpc(Type4.Object({ name: str }, { additionalProperties: false }), preview),
4211
+ install: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str), overwrite: Type4.Optional(bool) }, { additionalProperties: false }), installResult),
4212
+ uninstall: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), nil)
3161
4213
  }
3162
4214
  }
3163
4215
  }
3164
4216
  });
3165
- var ApplicationInfoSchema = Type2.Object({
4217
+ var ApplicationInfoSchema = Type4.Object({
3166
4218
  runningVersion: str,
3167
4219
  installedVersion: nullableString,
3168
- protocolVersion: Type2.Integer({ minimum: 1 }),
4220
+ protocolVersion: Type4.Integer({ minimum: 1 }),
3169
4221
  latestVersion: nullableString,
3170
- checkedAt: Type2.Union([integer, nil]),
3171
- checkState: Type2.Enum(["unknown", "checking", "ok", "failed", "development"])
4222
+ checkedAt: Type4.Union([integer, nil]),
4223
+ checkState: Type4.Enum(["unknown", "checking", "ok", "failed", "development"])
3172
4224
  });
3173
4225
  var NekoApplicationInfo = contract("neko.application-info", {
3174
4226
  version: 1,
3175
- input: Type2.Object({}),
4227
+ input: Type4.Object({}),
3176
4228
  variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
3177
4229
  });
3178
4230
 
@@ -3180,16 +4232,17 @@ var NekoApplicationInfo = contract("neko.application-info", {
3180
4232
  import { open as open2 } from "fs/promises";
3181
4233
  import { basename as basename3, extname } from "path";
3182
4234
  import {
3183
- ChordError,
3184
- cell,
3185
- hostPrincipal,
3186
- listener,
3187
- local,
3188
- opaque,
3189
- resource
4235
+ ChordError as ChordError2,
4236
+ cell as cell2,
4237
+ hostPrincipal as hostPrincipal2,
4238
+ listener as listener2,
4239
+ local as local2,
4240
+ opaque as opaque2,
4241
+ resource as resource2
3190
4242
  } from "@earendil-works/chord";
3191
- import { withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
3192
- import { Type as Type3 } from "typebox";
4243
+ import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
4244
+ import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
4245
+ import { Type as Type6 } from "typebox";
3193
4246
 
3194
4247
  // packages/gateway/src/image-uploads.ts
3195
4248
  var IMAGE_CHUNK_BYTES = 256 * 1024;
@@ -3285,19 +4338,259 @@ class ImageUploads {
3285
4338
  }
3286
4339
  }
3287
4340
 
4341
+ // packages/gateway/src/subagent-host.ts
4342
+ import {
4343
+ ChordError,
4344
+ cell,
4345
+ hostPrincipal,
4346
+ listener,
4347
+ local,
4348
+ opaque,
4349
+ resource
4350
+ } from "@earendil-works/chord";
4351
+ import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
4352
+ import { Type as Type5 } from "typebox";
4353
+ async function openReader(runtime, childId, parentId, context, through, historyUsage) {
4354
+ const child = await runtime.harness.conversation(childId, context);
4355
+ if (!child)
4356
+ throw new ChordError("gone", "Subagent unavailable");
4357
+ const native = await child.viewState(context);
4358
+ if (through !== undefined)
4359
+ native.dispose();
4360
+ const project = (entries, streaming = true) => {
4361
+ const result = projectChatView({ ...native.value, entries: [...entries].sort((a, b) => a.id - b.id) }, "", {
4362
+ models: runtime.listModels(),
4363
+ contextEntries: native.value.entries
4364
+ });
4365
+ if (!streaming || through !== undefined) {
4366
+ result.streaming = null;
4367
+ result.busy = false;
4368
+ result.status = null;
4369
+ if (through !== undefined) {
4370
+ result.usage = historyUsage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
4371
+ delete result.contextUsage;
4372
+ }
4373
+ }
4374
+ let order = 0;
4375
+ for (const item of result.items) {
4376
+ if (item.type !== "tool")
4377
+ order = item.id;
4378
+ item.order = order;
4379
+ }
4380
+ return result;
4381
+ };
4382
+ const pageAt = async (before) => {
4383
+ const max = Math.min(before === undefined ? Infinity : before - 1, through ?? Infinity);
4384
+ const page = await child.entries(Number.isFinite(max) ? { maxEntryId: max } : {}, 100, undefined, context);
4385
+ const entries = [...page.items];
4386
+ while (page.next && !["assistant", "user"].includes(entries.at(-1)?.model?.[0]?.role ?? "")) {
4387
+ const extra = await child.entries({ maxEntryId: entries.at(-1).id - 1 }, 1, undefined, context);
4388
+ entries.push(...extra.items);
4389
+ if (!extra.items.length || !extra.next)
4390
+ break;
4391
+ }
4392
+ const first = entries.at(-1)?.id;
4393
+ const older = first === undefined ? [] : (await child.entries({ maxEntryId: first - 1 }, 1, undefined, context)).items;
4394
+ return { entries, next: older.length ? first : null };
4395
+ };
4396
+ let revision = 0;
4397
+ let sequence = 0;
4398
+ const payloads = new Map;
4399
+ const remember = (value) => {
4400
+ const token = ++sequence;
4401
+ payloads.set(token, JSON.stringify(value));
4402
+ while (payloads.size > 4) {
4403
+ const oldest = [...payloads.keys()].find((key) => key !== revision);
4404
+ payloads.delete(oldest);
4405
+ }
4406
+ return token;
4407
+ };
4408
+ return {
4409
+ subscribe: (fn) => {
4410
+ if (through !== undefined)
4411
+ return () => {};
4412
+ const stop = native.subscribe(() => fn(null));
4413
+ const stopApprovals = runtime.harness.subscribeCommits(({ changes }) => {
4414
+ if (changes.some((c) => c.type === "document" && ["app.neko.approvals", "app.neko.delegations", "app.neko.subagents"].includes(c.record.kind) && c.conversationId === parentId))
4415
+ fn(null);
4416
+ });
4417
+ return () => {
4418
+ stop();
4419
+ stopApprovals();
4420
+ };
4421
+ },
4422
+ async refresh() {
4423
+ const page = await pageAt();
4424
+ const approvals = through === undefined ? (await runtime.approvals.list(parentId, context)).filter((a) => a.childId === childId) : [];
4425
+ const currentUsage = await runtime.harness.snapshot(UsageDoc3, childId, context);
4426
+ const rounds = (await listDelegations(runtime.harness, parentId, context)).filter((r) => r.childId === childId).map(({ usageStart, usageEnd: _end, ...round }) => ({
4427
+ ...round,
4428
+ roundUsage: round.roundUsage ?? (through === undefined && usageStart && currentUsage ? subagentUsageTotals(subtractSubagentUsage(currentUsage, usageStart)) : null)
4429
+ }));
4430
+ revision = remember({ ...project(page.entries), rounds, approvals, historyBefore: page.next });
4431
+ return revision;
4432
+ },
4433
+ async chunk(input) {
4434
+ let token = input.token;
4435
+ if (token === undefined) {
4436
+ if (input.kind === "view")
4437
+ token = revision;
4438
+ else {
4439
+ const page = await pageAt(input.before);
4440
+ token = remember({ items: project(page.entries, false).items, next: page.next });
4441
+ }
4442
+ }
4443
+ const payload = payloads.get(token);
4444
+ if (payload === undefined)
4445
+ throw new ChordError("gone", "Transcript changed; reload");
4446
+ if (input.offset > payload.length)
4447
+ throw new ChordError("invalid", "Invalid transcript range");
4448
+ const end = Math.min(payload.length, input.offset + 32768);
4449
+ return { token, data: payload.slice(input.offset, end), next: end < payload.length ? end : null };
4450
+ },
4451
+ async history(before) {
4452
+ const page = await pageAt(before);
4453
+ return { items: project(page.entries, false).items, next: page.next };
4454
+ },
4455
+ async image(entryId, contentIndex, offset) {
4456
+ if (through !== undefined && entryId > through)
4457
+ throw new ChordError("forbidden", "Image outside fork history");
4458
+ const page = await child.entries({ minEntryId: entryId, maxEntryId: entryId }, 1, undefined, context);
4459
+ const content = page.items[0]?.model?.[0]?.content;
4460
+ const image = Array.isArray(content) ? content[contentIndex] : undefined;
4461
+ if (image?.type !== "image")
4462
+ throw new ChordError("invalid", "Image not in subagent conversation");
4463
+ const bytes = Buffer.from(image.data, "base64");
4464
+ if (offset > bytes.length)
4465
+ throw new ChordError("invalid", "Invalid image range");
4466
+ return {
4467
+ data: bytes.subarray(offset, offset + IMAGE_CHUNK_BYTES).toString("base64"),
4468
+ total: bytes.length,
4469
+ mimeType: image.mimeType
4470
+ };
4471
+ },
4472
+ close: () => native.dispose()
4473
+ };
4474
+ }
4475
+ var ChildProjection = local("neko.subagent-projection", {
4476
+ version: 1,
4477
+ input: Type5.Object({ parentId: Type5.Integer(), agentId: Type5.String() }),
4478
+ members: {
4479
+ revision: Type5.Integer(),
4480
+ reader: opaque("neko.subagent-reader@1")
4481
+ }
4482
+ });
4483
+ function installSubagentHost(host, runtime, context) {
4484
+ host.serve(ChildProjection, (input, _principal, self) => {
4485
+ const { revision } = self.state({ revision: cell(Type5.Integer(), 0) });
4486
+ const { reader } = self.resources({
4487
+ reader: resource({
4488
+ rev: 1,
4489
+ async open() {
4490
+ const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => input.agentId.startsWith("round-") ? d.taskId === Number(input.agentId.slice(6)) : (d.agentId ?? agentIdFor(d.childId)) === input.agentId);
4491
+ if (!entry)
4492
+ throw new ChordError("gone", "Subagent unavailable");
4493
+ const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
4494
+ try {
4495
+ const initial = await r.refresh();
4496
+ await host.root((ctx) => revision.replace(ctx, initial));
4497
+ return r;
4498
+ } catch (error) {
4499
+ r.close();
4500
+ throw error;
4501
+ }
4502
+ },
4503
+ close: (r) => r.close()
4504
+ })
4505
+ });
4506
+ self.listen({
4507
+ view: listener(reader, (r) => r, async (_event, ctx) => {
4508
+ revision.replace(ctx, await reader.value.refresh());
4509
+ })
4510
+ });
4511
+ return { revision, reader: self.expose(reader) };
4512
+ });
4513
+ host.serve(NekoSubagent, (input, principal, self) => {
4514
+ const owner = self.get(Owner, {}, { principal });
4515
+ const check = async () => {
4516
+ if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
4517
+ throw new ChordError("forbidden", "Attachment expired");
4518
+ if (!await runtime.getConversation(Number(input.sessionId)))
4519
+ throw new ChordError("gone", "Session unavailable");
4520
+ const entry = (await listDelegations(runtime.harness, Number(input.sessionId), context)).find((d) => input.agentId !== undefined ? (d.agentId ?? agentIdFor(d.childId)) === input.agentId : d.taskId === input.taskId);
4521
+ if (!entry)
4522
+ throw new ChordError("forbidden", "Subagent does not belong to this session");
4523
+ return entry;
4524
+ };
4525
+ const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
4526
+ const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
4527
+ const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
4528
+ self.listen({
4529
+ authorizedView: listener(shared.revision, async (value, ctx) => {
4530
+ try {
4531
+ target.value;
4532
+ await check();
4533
+ if (value)
4534
+ authorizedRevision.replace(ctx, value);
4535
+ } catch {
4536
+ authorizedRevision.replace(ctx, 0);
4537
+ }
4538
+ })
4539
+ });
4540
+ return {
4541
+ kind: "ready",
4542
+ revision: authorizedRevision,
4543
+ chunk: async (request) => {
4544
+ await check();
4545
+ return shared.reader.chunk(request);
4546
+ },
4547
+ getImage: async ({ entryId, contentIndex, offset }) => {
4548
+ await check();
4549
+ return shared.reader.image(entryId, contentIndex, offset);
4550
+ },
4551
+ decideApproval: async ({ id, version, decision }) => {
4552
+ const entry = await check();
4553
+ if (entry.historyThrough !== undefined)
4554
+ throw new ChordError("forbidden", "Fork history is read-only");
4555
+ const approval = (await runtime.approvals.list(Number(input.sessionId), context)).find((a) => a.id === id && a.childId === entry.childId);
4556
+ if (!approval)
4557
+ throw new ChordError("forbidden", "Approval does not belong to this agent");
4558
+ return runtime.approvals.decide(Number(input.sessionId), approval.delegationTaskId, id, version, decision, context);
4559
+ },
4560
+ stop: async (request) => {
4561
+ const entry = await check();
4562
+ if (entry.historyThrough !== undefined)
4563
+ throw new ChordError("forbidden", "Fork history is read-only");
4564
+ const taskId = request?.taskId ?? input.taskId;
4565
+ if (taskId === undefined)
4566
+ throw new ChordError("invalid", "Select the exact active round to stop");
4567
+ const valid = await runtime.harness.commit(async (tx) => {
4568
+ const identity = (await tx.doc(SubagentsDoc, Number(input.sessionId))).items.find((a) => a.childId === entry.childId);
4569
+ return identity?.activeRoundId === taskId;
4570
+ }, context);
4571
+ if (!valid)
4572
+ return null;
4573
+ await runtime.harness.abortTask(taskId, context);
4574
+ await runtime.harness.waitForTask(taskId, context);
4575
+ return null;
4576
+ }
4577
+ };
4578
+ });
4579
+ }
4580
+
3288
4581
  // packages/gateway/src/session-host.ts
3289
- var OwnerInput = Type3.Object({});
3290
- var Owner = local("neko.attachment-owner", {
4582
+ var OwnerInput = Type6.Object({});
4583
+ var Owner = local2("neko.attachment-owner", {
3291
4584
  version: 1,
3292
4585
  input: OwnerInput,
3293
- members: { owner: opaque("neko.owner@1") }
4586
+ members: { owner: opaque2("neko.owner@1") }
3294
4587
  });
3295
- var Projection = local("neko.projection", {
4588
+ var Projection = local2("neko.projection", {
3296
4589
  version: 1,
3297
- input: Type3.Object({ id: Type3.String() }),
4590
+ input: Type6.Object({ id: Type6.String() }),
3298
4591
  members: {
3299
4592
  view: ChatViewSchema,
3300
- reader: opaque("neko.projection-reader@1")
4593
+ reader: opaque2("neko.projection-reader@1")
3301
4594
  }
3302
4595
  });
3303
4596
  var emptyView = {
@@ -3323,6 +4616,11 @@ async function openProjection(runtime, session, context) {
3323
4616
  let changed;
3324
4617
  const stop = view.subscribe(() => changed?.(null));
3325
4618
  const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
4619
+ let childIds = new Set;
4620
+ const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
4621
+ if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals", "app.neko.subagents"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
4622
+ changed?.(null);
4623
+ });
3326
4624
  return {
3327
4625
  subscribe(fn) {
3328
4626
  changed = fn;
@@ -3335,7 +4633,7 @@ async function openProjection(runtime, session, context) {
3335
4633
  for (const entry of view.value.entries)
3336
4634
  history.set(entry.id, entry);
3337
4635
  const [naming, compaction] = await Promise.all([runtime.namingUsage(session.id), session.compactionRecord()]);
3338
- return projectChatView({
4636
+ const projected = projectChatView({
3339
4637
  ...view.value,
3340
4638
  entries: [...history.values()].sort((a, b) => a.id - b.id),
3341
4639
  docs: {
@@ -3346,6 +4644,33 @@ async function openProjection(runtime, session, context) {
3346
4644
  }, naming)
3347
4645
  }
3348
4646
  }, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
4647
+ const subagents = await listDelegations(runtime.harness, session.id, context);
4648
+ const approvals = await runtime.approvals.list(session.id, context);
4649
+ for (const entry of subagents)
4650
+ entry.approvalCount = entry.historyThrough === undefined ? approvals.filter((a) => a.delegationTaskId === entry.taskId && a.status === "pending").length : 0;
4651
+ childIds = new Set(subagents.filter((entry) => entry.historyThrough === undefined).map((entry) => entry.childId));
4652
+ let aggregate = await session.usage();
4653
+ for (const child of new Map(subagents.filter((entry) => entry.historyThrough === undefined).map((r) => [r.childId, r])).values())
4654
+ aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(UsageDoc4, child.childId, context) ?? {
4655
+ models: {},
4656
+ tools: {}
4657
+ });
4658
+ const u = totalUsage(aggregate);
4659
+ return {
4660
+ ...projected,
4661
+ agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
4662
+ subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
4663
+ ...entry,
4664
+ report: entry.report?.slice(0, 512) ?? null
4665
+ })),
4666
+ taskUsage: {
4667
+ input: u.input,
4668
+ output: u.output,
4669
+ cacheRead: u.cacheRead,
4670
+ cacheWrite: u.cacheWrite,
4671
+ cost: u.cost.total
4672
+ }
4673
+ };
3349
4674
  },
3350
4675
  async entry(id, ctx) {
3351
4676
  ctx.abortSignal?.throwIfAborted();
@@ -3356,15 +4681,17 @@ async function openProjection(runtime, session, context) {
3356
4681
  close() {
3357
4682
  stop();
3358
4683
  stopMetadata();
4684
+ stopChildren();
3359
4685
  view.dispose();
3360
4686
  history.clear();
3361
4687
  }
3362
4688
  };
3363
4689
  }
3364
4690
  function serveSessions(host, runtime, context) {
4691
+ installSubagentHost(host, runtime, context);
3365
4692
  host.serve(Owner, (_input, _principal, self) => {
3366
4693
  const { owner } = self.resources({
3367
- owner: resource({
4694
+ owner: resource2({
3368
4695
  rev: 1,
3369
4696
  open: () => ({ active: null }),
3370
4697
  close: (value) => {
@@ -3375,14 +4702,14 @@ function serveSessions(host, runtime, context) {
3375
4702
  return { owner: self.expose(owner) };
3376
4703
  });
3377
4704
  host.serve(Projection, (input, _principal, self) => {
3378
- const { view } = self.state({ view: cell(ChatViewSchema, emptyView) });
4705
+ const { view } = self.state({ view: cell2(ChatViewSchema, emptyView) });
3379
4706
  const { projection } = self.resources({
3380
- projection: resource({
4707
+ projection: resource2({
3381
4708
  rev: 1,
3382
4709
  async open({ signal }) {
3383
4710
  const session = await runtime.getConversation(Number(input.id));
3384
4711
  if (!session)
3385
- throw new ChordError("gone", "Session unavailable");
4712
+ throw new ChordError2("gone", "Session unavailable");
3386
4713
  const opened = await openProjection(runtime, session, context);
3387
4714
  try {
3388
4715
  const initial = await opened.project();
@@ -3397,7 +4724,7 @@ function serveSessions(host, runtime, context) {
3397
4724
  })
3398
4725
  });
3399
4726
  self.listen({
3400
- view: listener(projection, (value) => value, async (_event, ctx) => {
4727
+ view: listener2(projection, (value) => value, async (_event, ctx) => {
3401
4728
  view.replace(ctx, await projection.value.project());
3402
4729
  })
3403
4730
  });
@@ -3405,18 +4732,18 @@ function serveSessions(host, runtime, context) {
3405
4732
  });
3406
4733
  host.serve(NekoChat, (input, principal, self) => {
3407
4734
  const owner = self.get(Owner, {}, { principal });
3408
- const shared = self.get(Projection, { id: input.sessionId }, { principal: hostPrincipal });
4735
+ const shared = self.get(Projection, { id: input.sessionId }, { principal: hostPrincipal2 });
3409
4736
  const { uploads, projection } = self.resources({
3410
- uploads: resource({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
3411
- projection: resource({
4737
+ uploads: resource2({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
4738
+ projection: resource2({
3412
4739
  rev: 1,
3413
4740
  deps: [owner],
3414
4741
  async open() {
3415
4742
  if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
3416
- throw new ChordError("forbidden", "Attachment does not belong to this connection");
4743
+ throw new ChordError2("forbidden", "Attachment does not belong to this connection");
3417
4744
  const session = await runtime.getConversation(Number(input.sessionId));
3418
4745
  if (!session)
3419
- throw new ChordError("gone", "Session unavailable");
4746
+ throw new ChordError2("gone", "Session unavailable");
3420
4747
  return {
3421
4748
  session
3422
4749
  };
@@ -3427,7 +4754,7 @@ function serveSessions(host, runtime, context) {
3427
4754
  try {
3428
4755
  return action();
3429
4756
  } catch (error) {
3430
- throw new ChordError("invalid", error instanceof Error ? error.message : "Image upload failed");
4757
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Image upload failed");
3431
4758
  }
3432
4759
  };
3433
4760
  const behavior = {
@@ -3483,7 +4810,7 @@ function serveSessions(host, runtime, context) {
3483
4810
  await projection.value.session.compact(instructions);
3484
4811
  return null;
3485
4812
  } catch (error) {
3486
- throw new ChordError("invalid", error instanceof Error ? error.message : "Cannot compact");
4813
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot compact");
3487
4814
  }
3488
4815
  },
3489
4816
  beginImage: async (input) => safeUpload(() => uploads.value.begin(input)),
@@ -3501,15 +4828,15 @@ function serveSessions(host, runtime, context) {
3501
4828
  },
3502
4829
  getImage: async (input, ctx) => {
3503
4830
  if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
3504
- throw new ChordError("invalid", "Invalid image range");
3505
- const entry = await shared.reader.entry(input.entryId, withAbortSignal2(ctx.signal, context));
4831
+ throw new ChordError2("invalid", "Invalid image range");
4832
+ const entry = await shared.reader.entry(input.entryId, withAbortSignal3(ctx.signal, context));
3506
4833
  const content = entry?.model?.[0]?.content;
3507
4834
  const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
3508
4835
  if (image?.type !== "image")
3509
- throw new ChordError("invalid", "Image is not part of this conversation");
4836
+ throw new ChordError2("invalid", "Image is not part of this conversation");
3510
4837
  const bytes = Buffer.from(image.data, "base64");
3511
4838
  if (input.offset > bytes.length)
3512
- throw new ChordError("invalid", "Invalid image range");
4839
+ throw new ChordError2("invalid", "Invalid image range");
3513
4840
  return {
3514
4841
  data: bytes.subarray(input.offset, input.offset + IMAGE_CHUNK_BYTES).toString("base64"),
3515
4842
  total: bytes.length,
@@ -3529,8 +4856,8 @@ function serveSessions(host, runtime, context) {
3529
4856
  uploads.value.release(id);
3530
4857
  } catch (error) {
3531
4858
  if (error instanceof Error && error.message.startsWith("DEEPSEEK_KEY_REQUIRED:"))
3532
- throw new ChordError("invalid", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
3533
- throw new ChordError("invalid", error instanceof Error ? error.message : "Cannot submit message");
4859
+ throw new ChordError2("invalid", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
4860
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot submit message");
3534
4861
  }
3535
4862
  return null;
3536
4863
  },
@@ -3552,16 +4879,16 @@ function serveSessions(host, runtime, context) {
3552
4879
  configure: async (change) => {
3553
4880
  const applied = await runtime.configureConversation(projection.value.session.id, change);
3554
4881
  if (!applied) {
3555
- throw new ChordError("invalid", `Session ${String(projection.value.session.id)} is unavailable or archived`);
4882
+ throw new ChordError2("invalid", `Session ${String(projection.value.session.id)} is unavailable or archived`);
3556
4883
  }
3557
4884
  return null;
3558
4885
  }
3559
4886
  };
3560
4887
  const guard = async () => {
3561
4888
  if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
3562
- throw new ChordError("gone", "Attachment is no longer active");
4889
+ throw new ChordError2("gone", "Attachment is no longer active");
3563
4890
  if (!await runtime.getConversation(Number(input.sessionId)))
3564
- throw new ChordError("gone", "Session unavailable or archived");
4891
+ throw new ChordError2("gone", "Session unavailable or archived");
3565
4892
  };
3566
4893
  for (const name of Object.keys(NekoChat.def.variants.ready.methods)) {
3567
4894
  const method = behavior[name];
@@ -3576,7 +4903,7 @@ function serveSessions(host, runtime, context) {
3576
4903
  }
3577
4904
 
3578
4905
  // packages/gateway/src/skill-market.ts
3579
- import { createHash, randomUUID } from "crypto";
4906
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
3580
4907
  import { mkdir as mkdir3, readdir as readdir2, rename as rename3, rm as rm3, stat as stat2, writeFile as writeFile3 } from "fs/promises";
3581
4908
  import { homedir as homedir6 } from "os";
3582
4909
  import { dirname as dirname6, join as join8 } from "path";
@@ -4150,7 +5477,7 @@ function createSkillMarketClient(options) {
4150
5477
  const skill = await detail(name);
4151
5478
  const files = unpack(skill, await download(skill));
4152
5479
  await sweepTempDirs(skillsDir);
4153
- const temp = join8(dirname6(skillsDir), `.tmp-${randomUUID()}`);
5480
+ const temp = join8(dirname6(skillsDir), `.tmp-${randomUUID2()}`);
4154
5481
  try {
4155
5482
  for (const [relative, bytes] of files) {
4156
5483
  const dest = join8(temp, ...relative.split("/"));
@@ -4271,14 +5598,14 @@ async function listDrives() {
4271
5598
  }
4272
5599
 
4273
5600
  // packages/gateway/src/server-services.ts
4274
- var ServerState = local2("neko.server-state", {
5601
+ var ServerState = local3("neko.server-state", {
4275
5602
  version: 1,
4276
- input: Type4.Object({}),
5603
+ input: Type7.Object({}),
4277
5604
  members: {
4278
5605
  directory: DirectoryStateSchema,
4279
5606
  workspaces: WorkspaceStateSchema,
4280
5607
  credentials: CredentialStateSchema,
4281
- refreshCredentials: method2(Type4.Null(), Type4.Null())
5608
+ refreshCredentials: method2(Type7.Null(), Type7.Null())
4282
5609
  }
4283
5610
  });
4284
5611
  async function createNekoServerServices(options) {
@@ -4298,9 +5625,9 @@ async function createNekoServerServices(options) {
4298
5625
  listener(updates.state);
4299
5626
  };
4300
5627
  host.serve(NekoApplicationInfo, (_input, _principal, self) => {
4301
- const state = self.state({ info: cell2(ApplicationInfoSchema, updates.state) });
5628
+ const state = self.state({ info: cell3(ApplicationInfoSchema, updates.state) });
4302
5629
  self.listen({
4303
- updates: listener2({
5630
+ updates: listener3({
4304
5631
  subscribe: (fn) => {
4305
5632
  updateListeners.add(fn);
4306
5633
  return () => updateListeners.delete(fn);
@@ -4332,7 +5659,7 @@ async function createNekoServerServices(options) {
4332
5659
  const toRemote = (error) => {
4333
5660
  if (error instanceof SkillMarketError) {
4334
5661
  const code = error.code === "skill_not_found" || error.code === "skill_not_installed" ? "gone" : "invalid";
4335
- throw new ChordError2(code, error.message, { reason: error.code });
5662
+ throw new ChordError3(code, error.message, { reason: error.code });
4336
5663
  }
4337
5664
  throw error;
4338
5665
  };
@@ -4366,12 +5693,12 @@ async function createNekoServerServices(options) {
4366
5693
  };
4367
5694
  host.serve(ServerState, (_input, _principal, self) => {
4368
5695
  const state = self.state({
4369
- directory: cell2(DirectoryStateSchema, { revision: 1, sessions }),
4370
- workspaces: cell2(WorkspaceStateSchema, { revision: 1, workspaces }),
4371
- credentials: cell2(CredentialStateSchema, { revision: 1, statuses })
5696
+ directory: cell3(DirectoryStateSchema, { revision: 1, sessions }),
5697
+ workspaces: cell3(WorkspaceStateSchema, { revision: 1, workspaces }),
5698
+ credentials: cell3(CredentialStateSchema, { revision: 1, statuses })
4372
5699
  });
4373
5700
  self.listen({
4374
- directory: listener2({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
5701
+ directory: listener3({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
4375
5702
  if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
4376
5703
  "app.neko.session-metadata",
4377
5704
  "app.neko.activity",
@@ -4398,7 +5725,7 @@ async function createNekoServerServices(options) {
4398
5725
  }
4399
5726
  };
4400
5727
  }, { eager: { input: {} } });
4401
- const shared = (self) => self.get(ServerState, {}, { principal: hostPrincipal2 });
5728
+ const shared = (self) => self.get(ServerState, {}, { principal: hostPrincipal3 });
4402
5729
  host.serve(NekoSessionDirectory, (_input, _principal, self) => ({ kind: "ready", state: shared(self).directory }));
4403
5730
  host.serve(NekoWorkspaces, (_input, _principal, self) => ({
4404
5731
  kind: "ready",
@@ -4434,7 +5761,7 @@ async function createNekoServerServices(options) {
4434
5761
  search: async (input, ctx) => {
4435
5762
  const roots = await runtime.listWorkspaces();
4436
5763
  if (!roots.some((root) => root.path === input.workspaceRoot))
4437
- throw new ChordError2("invalid", "Unknown workspace.");
5764
+ throw new ChordError3("invalid", "Unknown workspace.");
4438
5765
  return runtime.workspaceFiles.search(input.workspaceRoot, input.query, ctx.signal);
4439
5766
  }
4440
5767
  }));
@@ -4444,7 +5771,7 @@ async function createNekoServerServices(options) {
4444
5771
  const summary = async (id) => {
4445
5772
  const found = (await list()).find((item) => item.id === String(id));
4446
5773
  if (!found)
4447
- throw new ChordError2("gone", "Session unavailable");
5774
+ throw new ChordError3("gone", "Session unavailable");
4448
5775
  return found;
4449
5776
  };
4450
5777
  return {
@@ -4457,9 +5784,9 @@ async function createNekoServerServices(options) {
4457
5784
  },
4458
5785
  attach: (id) => serialize(async () => {
4459
5786
  if (!await runtime.getConversation(toConversationId(id)))
4460
- throw new ChordError2("gone", `Unknown session: ${id}`);
5787
+ throw new ChordError3("gone", `Unknown session: ${id}`);
4461
5788
  if (owner.owner.active?.sessionId !== id) {
4462
- owner.owner.active = { sessionId: id, attachmentId: randomUUID2() };
5789
+ owner.owner.active = { sessionId: id, attachmentId: randomUUID3() };
4463
5790
  await runtime.touchConversation(toConversationId(id));
4464
5791
  }
4465
5792
  return owner.owner.active;
@@ -4495,7 +5822,7 @@ async function createNekoServerServices(options) {
4495
5822
  function toConversationId(sessionId) {
4496
5823
  const numeric = Number(sessionId);
4497
5824
  if (!Number.isSafeInteger(numeric) || numeric < 1 || String(numeric) !== sessionId)
4498
- throw new ChordError2("invalid", `Invalid neko session id: ${sessionId}`);
5825
+ throw new ChordError3("invalid", `Invalid neko session id: ${sessionId}`);
4499
5826
  return numeric;
4500
5827
  }
4501
5828
 
@@ -4792,7 +6119,7 @@ var DEFAULT_HOST = "127.0.0.1";
4792
6119
  var DEFAULT_PORT = 8321;
4793
6120
  var SERVER_ID_FILENAME = "gateway-server-id";
4794
6121
  async function startGateway(options = {}) {
4795
- const context = options.context ?? BACKGROUND_CONTEXT3;
6122
+ const context = options.context ?? BACKGROUND_CONTEXT4;
4796
6123
  const fileConfig = loadNekoConfig(options.configDir);
4797
6124
  const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
4798
6125
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
@@ -4862,7 +6189,7 @@ async function bindGateway(runtime, options) {
4862
6189
  }
4863
6190
  try {
4864
6191
  for (const listener of listeners)
4865
- await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID3() } }));
6192
+ await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID4() } }));
4866
6193
  } catch (error) {
4867
6194
  await Promise.allSettled(listeners.map((listener) => listener.close()));
4868
6195
  await chord.close();
@@ -4932,7 +6259,7 @@ async function loadOrCreateServerId(nekoDir) {
4932
6259
  if (existing !== "")
4933
6260
  return existing;
4934
6261
  } catch {}
4935
- const created = randomUUID3();
6262
+ const created = randomUUID4();
4936
6263
  await mkdir5(nekoDir, { recursive: true, mode: 448 });
4937
6264
  await writeFile4(file, `${created}
4938
6265
  `, { mode: 384 });