neko-harness 0.2.8 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/gateway.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // @bun
3
3
 
4
4
  // packages/gateway/src/cli-args.ts
5
- var GATEWAY_USAGE = `\u7528\u6CD5: main.ts [\u9009\u9879]
5
+ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
6
6
 
7
7
  \u9009\u9879:
8
8
  --host <ip> WebSocket \u76D1\u542C\u5730\u5740\uFF08\u9ED8\u8BA4 127.0.0.1\uFF0C\u4EC5\u672C\u673A\uFF09
@@ -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,14 @@ function truncateMiddle(text, maxWidth) {
516
516
  function summarizeToolCall(name, args) {
517
517
  const record = args ?? {};
518
518
  switch (name) {
519
+ case "subagent":
520
+ return { args: `${record.type}: ${record.title}`, detail: String(record.task ?? "") };
521
+ case "find_files":
522
+ case "search_text":
523
+ return {
524
+ args: truncateMiddle(`${record.path ?? "."} ${record.pattern ?? record.query ?? ""}`, 100),
525
+ detail: JSON.stringify(args, null, 2)
526
+ };
519
527
  case "bash": {
520
528
  const command = typeof record.command === "string" ? record.command : "";
521
529
  const firstLine = command.split(`
@@ -653,6 +661,7 @@ function projectChatView(view, modelLabel, options = {}) {
653
661
  items.push(withArgs(result, block.name, block.arguments));
654
662
  } else {
655
663
  const summary = summarizeToolCall(block.name, block.arguments);
664
+ const slot = live.tools?.find((s) => s.callId === block.id);
656
665
  items.push({
657
666
  type: "tool",
658
667
  key: `t${block.id}`,
@@ -660,9 +669,9 @@ function projectChatView(view, modelLabel, options = {}) {
660
669
  name: block.name,
661
670
  summary: summary.args,
662
671
  detail: summary.detail ?? null,
663
- phase: "done",
672
+ phase: slot?.status ?? "done",
664
673
  isError: false,
665
- output: null,
674
+ output: slot?.output ?? null,
666
675
  result: null,
667
676
  durationMs: null,
668
677
  note: ran ? null : "Not run: response interrupted."
@@ -821,13 +830,13 @@ var stripFrontmatter = (content) => extractFrontmatter(content).body;
821
830
  // packages/core/src/runtime.ts
822
831
  import { homedir as homedir4 } from "os";
823
832
  import { join as join5 } from "path";
824
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2 } from "@earendil-works/chord/context";
833
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT3 } from "@earendil-works/chord/context";
825
834
  import { clampThinkingLevel, createModels, getSupportedThinkingLevels as getSupportedThinkingLevels2 } from "@earendil-works/pi-ai/models";
826
835
  import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
827
836
  import {
828
- AssistantEntry,
837
+ AssistantEntry as AssistantEntry2,
829
838
  createRegistry,
830
- defineDoc as defineDoc2,
839
+ defineDoc as defineDoc4,
831
840
  Harness,
832
841
  LiveDoc as importLiveDoc,
833
842
  MemoryStorage,
@@ -838,6 +847,164 @@ import { createNodePhotonImages } from "@earendil-works/pi-durable/images/node";
838
847
  import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite/node";
839
848
  import { createCodingTools } from "@earendil-works/pi-durable/tools";
840
849
 
850
+ // packages/core/src/agent-file-search.ts
851
+ import { BACKGROUND_CONTEXT, withAbortSignal } from "@earendil-works/chord/context";
852
+ import { defineExtension, defineTool } from "@earendil-works/pi-durable";
853
+ import { getOrThrow } from "@earendil-works/pi-durable/env";
854
+ import { Type } from "typebox";
855
+ var SEARCH_LIMITS = {
856
+ entries: 1e4,
857
+ fileBytes: 2 * 1024 * 1024,
858
+ scanBytes: 32 * 1024 * 1024,
859
+ outputBytes: 32 * 1024,
860
+ timeoutMs: 1e4
861
+ };
862
+ var path = Type.Optional(Type.String({ description: "Directory relative to this conversation's cwd, or absolute path." }));
863
+ var limit = Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum results (default 100)." }));
864
+ var text = (value) => ({ type: "text", text: value });
865
+ async function search(api, context, run) {
866
+ if (!api.env)
867
+ throw new Error("File environment unavailable");
868
+ const timer = new AbortController;
869
+ const timeout = setTimeout(() => timer.abort(new Error("File search timed out; narrow the directory.")), SEARCH_LIMITS.timeoutMs);
870
+ const signal = context.abortSignal ? AbortSignal.any([context.abortSignal, timer.signal]) : timer.signal;
871
+ try {
872
+ signal.throwIfAborted();
873
+ return await run(api.env, withAbortSignal(signal, context));
874
+ } finally {
875
+ clearTimeout(timeout);
876
+ }
877
+ }
878
+ async function listing(env, path, ctx) {
879
+ if (!env.listFiles)
880
+ throw new Error("File discovery unavailable. Retry.");
881
+ const base = getOrThrow(await env.absolutePath(path, ctx));
882
+ const files = getOrThrow(await env.listFiles(base, {
883
+ hidden: true,
884
+ gitignore: true,
885
+ maxEntries: SEARCH_LIMITS.entries,
886
+ maxBytes: 1024 * 1024,
887
+ timeoutMs: SEARCH_LIMITS.timeoutMs
888
+ }, ctx));
889
+ return { base, ...files };
890
+ }
891
+ function output(lines, complete, skipped = 0) {
892
+ const status = complete ? "Search complete within Pi listing; unreadable entries may be omitted." : "Partial results; narrow the directory or filter and retry.";
893
+ return {
894
+ output: [text(`${lines.join(`
895
+ `)}
896
+ ${status}${skipped ? ` Skipped/unreadable files: ${skipped}.` : ""}`)],
897
+ details: { complete, skipped }
898
+ };
899
+ }
900
+ var FindFiles = defineTool({
901
+ name: "find_files",
902
+ 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.",
903
+ parameters: Type.Object({ path, query: Type.Optional(Type.String()), limit }),
904
+ replay: "safe",
905
+ execute: (args, api, context) => search(api, context, async (env, ctx) => {
906
+ const files = await listing(env, args.path ?? ".", ctx);
907
+ const lines = [];
908
+ let bytes = 0;
909
+ let complete = !files.truncated;
910
+ for (const file of files.entries) {
911
+ ctx.abortSignal?.throwIfAborted();
912
+ if (args.query && !file.path.includes(args.query))
913
+ continue;
914
+ const absolute = getOrThrow(await env.joinPath([files.base, file.path], ctx)).replace(/\\/g, "/");
915
+ const line = `${file.kind} ${absolute}`;
916
+ bytes += Buffer.byteLength(line) + 1;
917
+ if (lines.length >= (args.limit ?? 100) || bytes > SEARCH_LIMITS.outputBytes - 256) {
918
+ complete = false;
919
+ break;
920
+ }
921
+ lines.push(line);
922
+ }
923
+ return output(lines, complete);
924
+ })
925
+ });
926
+ var SearchText = defineTool({
927
+ name: "search_text",
928
+ 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.",
929
+ parameters: Type.Object({
930
+ pattern: Type.String({ minLength: 1, maxLength: 4096 }),
931
+ path,
932
+ filePattern: Type.Optional(Type.String()),
933
+ caseSensitive: Type.Optional(Type.Boolean()),
934
+ contextLines: Type.Optional(Type.Integer({ minimum: 0, maximum: 5 })),
935
+ limit
936
+ }),
937
+ replay: "safe",
938
+ execute: (args, api, context) => search(api, context, async (env, ctx) => {
939
+ const files = await listing(env, args.path ?? ".", ctx);
940
+ const results = [];
941
+ let complete = !files.truncated;
942
+ let skipped = 0;
943
+ let scanned = 0;
944
+ let bytes = 0;
945
+ const needle = args.caseSensitive === false ? args.pattern.toLowerCase() : args.pattern;
946
+ outer:
947
+ for (const file of files.entries) {
948
+ ctx.abortSignal?.throwIfAborted();
949
+ if (file.kind !== "file" || args.filePattern && !file.path.includes(args.filePattern))
950
+ continue;
951
+ const absolute = getOrThrow(await env.joinPath([files.base, file.path], ctx));
952
+ const opened = await env.openBinaryReader(absolute, { noFollow: true }, ctx);
953
+ if (!opened.ok) {
954
+ ctx.abortSignal?.throwIfAborted();
955
+ skipped++;
956
+ complete = false;
957
+ continue;
958
+ }
959
+ try {
960
+ const info = getOrThrow(await opened.value.info(ctx));
961
+ if (info.size > SEARCH_LIMITS.fileBytes) {
962
+ skipped++;
963
+ complete = false;
964
+ continue;
965
+ }
966
+ if (scanned + info.size > SEARCH_LIMITS.scanBytes) {
967
+ complete = false;
968
+ break;
969
+ }
970
+ const data = getOrThrow(await opened.value.read(0, info.size + 1, ctx));
971
+ scanned += data.length;
972
+ if (data.length !== info.size || data.includes(0)) {
973
+ skipped++;
974
+ complete = false;
975
+ continue;
976
+ }
977
+ const lines = new TextDecoder("utf-8", { fatal: true }).decode(data).split(/\r?\n/);
978
+ for (let i = 0;i < lines.length; i++) {
979
+ ctx.abortSignal?.throwIfAborted();
980
+ const value = lines[i];
981
+ if (!(args.caseSensitive === false ? value.toLowerCase() : value).includes(needle))
982
+ continue;
983
+ const n = args.contextLines ?? 0;
984
+ 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(`
985
+ `);
986
+ bytes += Buffer.byteLength(block) + 1;
987
+ if (results.length >= (args.limit ?? 100) || bytes > SEARCH_LIMITS.outputBytes - 256) {
988
+ complete = false;
989
+ break outer;
990
+ }
991
+ results.push(block);
992
+ }
993
+ } catch (error) {
994
+ ctx.abortSignal?.throwIfAborted();
995
+ if (!(error instanceof Error))
996
+ throw error;
997
+ skipped++;
998
+ complete = false;
999
+ } finally {
1000
+ await opened.value.close(BACKGROUND_CONTEXT);
1001
+ }
1002
+ }
1003
+ return output(results, complete, skipped);
1004
+ })
1005
+ });
1006
+ var AgentFileSearch = defineExtension({ name: "neko-file-search", tools: [FindFiles, SearchText] });
1007
+
841
1008
  // packages/core/src/compaction.ts
842
1009
  import {
843
1010
  CompactionTask
@@ -870,9 +1037,9 @@ async function readCompaction(harness, storage, id, context) {
870
1037
  }
871
1038
 
872
1039
  // packages/core/src/image-metadata.ts
873
- import { defineExtension, hook, ToolTask } from "@earendil-works/pi-durable";
1040
+ import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
874
1041
  import sharp from "sharp";
875
- var ImageDetails = defineExtension({
1042
+ var ImageDetails = defineExtension2({
876
1043
  name: "neko-image-details",
877
1044
  hooks: [
878
1045
  hook(ToolTask, {
@@ -1045,10 +1212,219 @@ ${args}` : block;
1045
1212
  };
1046
1213
 
1047
1214
  // packages/core/src/prompt/prompt.ts
1048
- import { defineExtension as defineExtension2, section } from "@earendil-works/pi-durable";
1215
+ import { defineExtension as defineExtension4, section } from "@earendil-works/pi-durable";
1216
+
1217
+ // packages/core/src/subagents.ts
1218
+ import {
1219
+ AssistantEntry,
1220
+ configure,
1221
+ defineDoc,
1222
+ defineExtension as defineExtension3,
1223
+ defineTool as defineTool2
1224
+ } from "@earendil-works/pi-durable";
1225
+ import { Type as Type2 } from "typebox";
1226
+ var DelegationsDoc = defineDoc({
1227
+ kind: "app.neko.delegations",
1228
+ version: 1,
1229
+ scope: "conversation",
1230
+ history: "latest",
1231
+ fork: "initial",
1232
+ initial: () => ({ items: [] })
1233
+ });
1234
+ var SubagentDoc = defineDoc({
1235
+ kind: "app.neko.subagent",
1236
+ version: 1,
1237
+ scope: "conversation",
1238
+ history: "latest",
1239
+ fork: "initial",
1240
+ initial: () => ({ role: null, parentId: null })
1241
+ });
1242
+ var SlotsDoc = defineDoc({
1243
+ kind: "app.neko.subagent-slots",
1244
+ version: 1,
1245
+ scope: "session",
1246
+ initial: () => ({ items: [] })
1247
+ });
1248
+ var ROLE_PROMPTS = {
1249
+ 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.",
1250
+ 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.",
1251
+ 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."
1252
+ };
1253
+ var SUBAGENT_DELIVERY = " Work only from the assigned task and your 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.";
1254
+ var READ_TOOLS = new Set(["read", "find_files", "search_text", "web_search", "web_extract"]);
1255
+ async function refreshDelegations(tx, id) {
1256
+ const doc = await tx.doc(DelegationsDoc, id);
1257
+ for (const item of doc.items) {
1258
+ if (item.historyThrough !== undefined)
1259
+ continue;
1260
+ if (item.status === "completed" || item.status === "failed" || item.status === "cancelled")
1261
+ continue;
1262
+ const task = await tx.task(item.taskId);
1263
+ if (task?.state.status !== "terminal")
1264
+ continue;
1265
+ item.status = task.state.outcome.status === "aborted" ? "cancelled" : "failed";
1266
+ item.endedAt = Date.now();
1267
+ }
1268
+ return JSON.parse(JSON.stringify(doc.items));
1269
+ }
1270
+ async function admit(api, cwd, role, context) {
1271
+ for (;; ) {
1272
+ context.abortSignal?.throwIfAborted();
1273
+ const acquired = await api.commit(async (tx) => {
1274
+ const slots = await tx.doc(SlotsDoc);
1275
+ const live = [];
1276
+ for (const slot of slots.items) {
1277
+ const task = await tx.task(slot.taskId);
1278
+ if (task && task.state.status !== "terminal")
1279
+ live.push(slot);
1280
+ }
1281
+ slots.items = live;
1282
+ if (live.some((slot) => slot.taskId === api.taskId))
1283
+ return true;
1284
+ if (live.filter((slot) => slot.parentId === api.conversationId).length >= 3 || role === "coder" && live.some((slot) => slot.coder && slot.cwd === cwd))
1285
+ return false;
1286
+ slots.items.push({ taskId: api.taskId, parentId: api.conversationId, cwd, coder: role === "coder" });
1287
+ return true;
1288
+ }, context);
1289
+ if (acquired)
1290
+ return;
1291
+ await new Promise((resolve, reject) => {
1292
+ const cleanup = () => {
1293
+ clearTimeout(timer);
1294
+ context.abortSignal?.removeEventListener("abort", abort);
1295
+ };
1296
+ const abort = () => {
1297
+ cleanup();
1298
+ reject(context.abortSignal?.reason ?? new Error("Cancelled"));
1299
+ };
1300
+ const timer = setTimeout(() => {
1301
+ cleanup();
1302
+ resolve();
1303
+ }, 100);
1304
+ context.abortSignal?.addEventListener("abort", abort, { once: true });
1305
+ if (context.abortSignal?.aborted)
1306
+ abort();
1307
+ });
1308
+ }
1309
+ }
1310
+ function createSubagentExtension(prepare, discard) {
1311
+ const extension = defineExtension3({
1312
+ name: "neko-subagents",
1313
+ tools: [
1314
+ defineTool2({
1315
+ name: "subagent",
1316
+ description: "Delegate a self-contained task to an independent foreground subagent. explore: investigation, Bash requires user approval; plan: read-only implementation planning; coder: scoped changes and verification. Include all needed context, constraints and expected delivery. Use only when isolation or parallel independent work helps; handle simple tasks directly. Returns the final report; the main agent waits and must verify/integrate it.",
1317
+ parameters: Type2.Object({
1318
+ type: Type2.Union([Type2.Literal("explore"), Type2.Literal("plan"), Type2.Literal("coder")]),
1319
+ title: Type2.String({ minLength: 1, maxLength: 80 }),
1320
+ task: Type2.String({ minLength: 1, maxLength: 32768 })
1321
+ }),
1322
+ replay: "safe",
1323
+ execute: async (args, api, context) => {
1324
+ if (!args.task.trim() || !args.title.trim() || Buffer.byteLength(args.task) > 32768)
1325
+ throw new Error("Task/title must be nonempty; task limit is 32 KiB.");
1326
+ if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
1327
+ throw new Error("Subagents cannot delegate.");
1328
+ const agent = await api.agent(context);
1329
+ const childId = await api.commit(async (tx) => {
1330
+ const existing = (await tx.scanConversations({ ownerTaskId: api.taskId }, 1)).items[0];
1331
+ if (existing)
1332
+ return existing.id;
1333
+ const child = await tx.createConversation({ ownership: { kind: "task", taskId: api.taskId } });
1334
+ await configure(tx, child.id, {
1335
+ model: agent.model,
1336
+ thinkingLevel: agent.thinkingLevel,
1337
+ cwd: agent.cwd,
1338
+ extensions: agent.extensions.filter((e) => e.name !== extension.name),
1339
+ tools: agent.tools.filter((tool) => tool.name !== "subagent" && (args.type === "coder" || READ_TOOLS.has(tool.name) || args.type === "explore" && tool.name === "bash"))
1340
+ });
1341
+ Object.assign(await tx.doc(SubagentDoc, child.id), { role: args.type, parentId: api.conversationId });
1342
+ (await tx.doc(DelegationsDoc, api.conversationId)).items.push({
1343
+ parentId: api.conversationId,
1344
+ taskId: api.taskId,
1345
+ callId: api.callId,
1346
+ childId: child.id,
1347
+ role: args.type,
1348
+ title: args.title,
1349
+ createdAt: Date.now(),
1350
+ startedAt: null,
1351
+ endedAt: null,
1352
+ status: "queued",
1353
+ report: null
1354
+ });
1355
+ return child.id;
1356
+ }, context);
1357
+ await api.details({ conversationId: childId, role: args.type, title: args.title }, context);
1358
+ const cwd = api.env ? await api.env.canonicalPath(agent.cwd ?? api.env.cwd, context) : undefined;
1359
+ await admit(api, cwd?.ok ? cwd.value : agent.cwd ?? "", args.type, context);
1360
+ await api.commit(async (tx) => {
1361
+ const item = (await tx.doc(DelegationsDoc, api.conversationId)).items.find((d) => d.taskId === api.taskId);
1362
+ item.status = "running";
1363
+ item.startedAt ??= Date.now();
1364
+ }, context);
1365
+ const child = await api.conversation(childId, context);
1366
+ const requestId = `subagent:${api.taskId}`;
1367
+ const duplicate = await api.commit((tx) => tx.submissionByRequest(childId, requestId), context);
1368
+ if (!duplicate)
1369
+ await prepare(childId, requestId, agent.cwd ?? "");
1370
+ let submission;
1371
+ try {
1372
+ submission = await child.submit({
1373
+ type: "input",
1374
+ content: args.task,
1375
+ requestId,
1376
+ entry: { kind: NekoInput.kind, data: { at: Date.now(), prompt: args.task, images: [] } }
1377
+ }, context);
1378
+ } catch (error) {
1379
+ discard(childId, requestId);
1380
+ throw error;
1381
+ }
1382
+ const settled = await submission.wait(context);
1383
+ let report = "";
1384
+ let status = "failed";
1385
+ if (settled.status === "done" && settled.type === "input") {
1386
+ const entry = await api.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
1387
+ const message = entry?.model?.[0];
1388
+ report = message?.role === "assistant" ? message.content.flatMap((b) => b.type === "text" ? [b.text] : []).join("") : "";
1389
+ status = "completed";
1390
+ } else if (settled.status === "unanswered") {
1391
+ status = settled.reason === "aborted" ? "cancelled" : "failed";
1392
+ report = `Subagent ${status}: ${settled.reason}`;
1393
+ }
1394
+ await api.commit(async (tx) => {
1395
+ const item = (await tx.doc(DelegationsDoc, api.conversationId)).items.find((d) => d.taskId === api.taskId);
1396
+ item.status = status;
1397
+ item.report = report;
1398
+ item.endedAt = Date.now();
1399
+ }, context);
1400
+ return {
1401
+ output: [{ type: "text", text: report || "Subagent completed without a text report." }],
1402
+ isError: status !== "completed",
1403
+ details: { conversationId: childId, role: args.type, title: args.title, status }
1404
+ };
1405
+ }
1406
+ })
1407
+ ]
1408
+ });
1409
+ return extension;
1410
+ }
1411
+ async function listDelegations(harness, parent, context) {
1412
+ return harness.commit((tx) => refreshDelegations(tx, parent), context);
1413
+ }
1414
+
1415
+ // packages/core/src/prompt/prompt.ts
1049
1416
  var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
1050
1417
  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
1418
  var TOOL_GUIDELINES = {
1419
+ subagent: [
1420
+ "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. Verify and integrate reports. Do not modify files concurrently with a coder assigned those files."
1421
+ ],
1422
+ find_files: [
1423
+ "Use find_files to discover files; query is a literal substring. Narrow the directory when results are partial."
1424
+ ],
1425
+ search_text: [
1426
+ "Use search_text for literal text search and read for surrounding evidence. Partial results do not prove absence."
1427
+ ],
1052
1428
  read: ["Use read to examine files instead of cat or sed."],
1053
1429
  write: ["Use write only for new files or complete rewrites."],
1054
1430
  edit: [
@@ -1084,11 +1460,12 @@ ${content}
1084
1460
  `);
1085
1461
  var createNekoPrompt = (loadResources) => {
1086
1462
  const built = new WeakMap;
1087
- const build = async (input) => {
1463
+ const build = async (input, context) => {
1088
1464
  const cwd = input.agent.cwd;
1465
+ const role = (await input.read.snapshot(SubagentDoc, input.conversationId, context))?.role;
1089
1466
  const resources = cwd ? await loadResources(input.conversationId, cwd) : { contextFiles: [], skills: [] };
1090
1467
  const sections = {
1091
- preamble: PREAMBLE,
1468
+ preamble: role ? ROLE_PROMPTS[role] + SUBAGENT_DELIVERY : PREAMBLE,
1092
1469
  rules: buildRules(input.agent.tools.map((tool) => tool.name)),
1093
1470
  project_context: renderProjectContext(resources.contextFiles),
1094
1471
  skills: formatSkillsForPrompt(resources.skills) || undefined,
@@ -1096,12 +1473,12 @@ var createNekoPrompt = (loadResources) => {
1096
1473
  };
1097
1474
  return sections;
1098
1475
  };
1099
- return defineExtension2({
1476
+ return defineExtension4({
1100
1477
  name: "neko-prompt",
1101
- sections: KEYS.map((key) => section(key, async (input) => {
1478
+ sections: KEYS.map((key) => section(key, async (input, context) => {
1102
1479
  let pending = built.get(input);
1103
1480
  if (!pending) {
1104
- pending = build(input);
1481
+ pending = build(input, context);
1105
1482
  built.set(input, pending);
1106
1483
  }
1107
1484
  const text = (await pending)[key];
@@ -1248,13 +1625,13 @@ var createResourceSnapshots = (load, submission) => {
1248
1625
  import { stripVTControlCharacters } from "util";
1249
1626
  import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
1250
1627
  import {
1251
- defineDoc,
1252
- defineExtension as defineExtension3,
1628
+ defineDoc as defineDoc2,
1629
+ defineExtension as defineExtension5,
1253
1630
  defineTask,
1254
1631
  InboxDoc,
1255
1632
  LiveDoc
1256
1633
  } from "@earendil-works/pi-durable";
1257
- var SessionMetadataDoc = defineDoc({
1634
+ var SessionMetadataDoc = defineDoc2({
1258
1635
  kind: "app.neko.session-metadata",
1259
1636
  version: 1,
1260
1637
  scope: "conversation",
@@ -1441,7 +1818,7 @@ class SessionMetadataManager {
1441
1818
  this.storage = storage;
1442
1819
  this.context = context;
1443
1820
  this.report = report;
1444
- registry.install(defineExtension3({ name: "neko-naming", tasks: [this.task] }));
1821
+ registry.install(defineExtension5({ name: "neko-naming", tasks: [this.task] }));
1445
1822
  this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
1446
1823
  const ids = new Set;
1447
1824
  const submissions = new Map;
@@ -1580,9 +1957,191 @@ class SessionMetadataManager {
1580
1957
  }
1581
1958
  }
1582
1959
 
1960
+ // packages/core/src/tool-approvals.ts
1961
+ import { randomUUID } from "crypto";
1962
+ import { realpath } from "fs/promises";
1963
+ import {
1964
+ configure as configure2,
1965
+ defineDoc as defineDoc3,
1966
+ defineExtension as defineExtension6,
1967
+ hook as hook2,
1968
+ ToolTask as ToolTask2,
1969
+ wrapTool
1970
+ } from "@earendil-works/pi-durable";
1971
+ import { createBashTool } from "@earendil-works/pi-durable/tools";
1972
+ var ApprovalsDoc = defineDoc3({
1973
+ kind: "app.neko.approvals",
1974
+ version: 1,
1975
+ scope: "conversation",
1976
+ history: "latest",
1977
+ fork: "initial",
1978
+ initial: () => ({ items: [] })
1979
+ });
1980
+ async function live(tx, id) {
1981
+ const task = await tx.task(id);
1982
+ return !!task && !task.abortRequested && task.state.status !== "terminal" && task.state.status !== "completing";
1983
+ }
1984
+
1985
+ class ToolApprovals {
1986
+ harness;
1987
+ #parents = new Set;
1988
+ constructor(harness, context) {
1989
+ this.harness = harness;
1990
+ const stop = harness.subscribeCommits(({ changes }) => {
1991
+ if (!changes.some((c) => c.type === "task" && (c.value.abortRequested || c.value.state.status === "terminal")))
1992
+ return;
1993
+ for (const parent of this.#parents)
1994
+ this.list(parent, context).catch(() => {});
1995
+ });
1996
+ harness.closed.finally(stop).catch(() => {});
1997
+ }
1998
+ async list(parent, context) {
1999
+ this.#parents.add(parent);
2000
+ return this.harness.commit(async (tx) => {
2001
+ const doc = await tx.doc(ApprovalsDoc, parent);
2002
+ for (const item of doc.items) {
2003
+ if (item.status === "pending" && (!await live(tx, item.id) || !await live(tx, item.delegationTaskId))) {
2004
+ item.status = "cancelled";
2005
+ item.decidedAt = Date.now();
2006
+ }
2007
+ }
2008
+ return JSON.parse(JSON.stringify(doc.items));
2009
+ }, context);
2010
+ }
2011
+ async decide(parent, delegationTaskId, id, version, decision, context) {
2012
+ return this.harness.commit(async (tx) => {
2013
+ const delegation = (await tx.doc(DelegationsDoc, parent)).items.find((d) => d.taskId === delegationTaskId);
2014
+ const item = (await tx.doc(ApprovalsDoc, parent)).items.find((a) => a.id === id);
2015
+ if (!delegation || delegation.historyThrough !== undefined || !item || item.delegationTaskId !== delegationTaskId || item.childId !== delegation.childId || item.version !== version)
2016
+ throw new Error("Approval does not belong to this task or has changed");
2017
+ if (item.status !== "pending")
2018
+ return item.status;
2019
+ item.status = await live(tx, id) && await live(tx, delegationTaskId) ? decision : "cancelled";
2020
+ item.decidedAt = Date.now();
2021
+ return item.status;
2022
+ }, context);
2023
+ }
2024
+ async wait(parent, id, context) {
2025
+ const state = await this.harness.documentState(ApprovalsDoc, parent, context);
2026
+ if (!state)
2027
+ throw new Error("Approval unavailable");
2028
+ try {
2029
+ return await new Promise((resolve, reject) => {
2030
+ let stop = () => {};
2031
+ const cleanup = () => {
2032
+ stop();
2033
+ context.abortSignal?.removeEventListener("abort", abort);
2034
+ };
2035
+ const abort = () => {
2036
+ cleanup();
2037
+ reject(context.abortSignal?.reason ?? new Error("Cancelled"));
2038
+ };
2039
+ const check = () => {
2040
+ const item = state.value?.items.find((a) => a.id === id);
2041
+ if (item && item.status !== "pending") {
2042
+ cleanup();
2043
+ resolve(structuredClone(item));
2044
+ }
2045
+ };
2046
+ stop = state.subscribe(check);
2047
+ context.abortSignal?.addEventListener("abort", abort, { once: true });
2048
+ if (context.abortSignal?.aborted)
2049
+ abort();
2050
+ else
2051
+ check();
2052
+ });
2053
+ } finally {
2054
+ state.dispose();
2055
+ }
2056
+ }
2057
+ async ensure(parent, candidate, context) {
2058
+ this.#parents.add(parent);
2059
+ return this.harness.commit(async (tx) => {
2060
+ if (!await live(tx, candidate.id) || !await live(tx, candidate.delegationTaskId))
2061
+ throw new Error("Task stopped");
2062
+ const doc = await tx.doc(ApprovalsDoc, parent);
2063
+ const existing = doc.items.find((a) => a.id === candidate.id);
2064
+ if (existing) {
2065
+ if (existing.command !== candidate.command || existing.cwd !== candidate.cwd || existing.timeout !== candidate.timeout || existing.childId !== candidate.childId)
2066
+ throw new Error("Approval operation changed; a new call is required");
2067
+ return;
2068
+ }
2069
+ doc.items.push({
2070
+ ...candidate,
2071
+ version: randomUUID(),
2072
+ status: "pending",
2073
+ createdAt: Date.now(),
2074
+ decidedAt: null
2075
+ });
2076
+ }, context);
2077
+ }
2078
+ }
2079
+ function createApprovalExtension(service) {
2080
+ return defineExtension6({
2081
+ name: "neko-tool-approvals",
2082
+ hooks: [
2083
+ hook2(ToolTask2, {
2084
+ beforeTool: async (call, api, context) => {
2085
+ const role = await api.snapshot(SubagentDoc, api.conversationId, context);
2086
+ if (role?.role !== "explore" || call.name !== "bash")
2087
+ return;
2088
+ if (role.parentId === null)
2089
+ return { block: "Missing parent session" };
2090
+ const approvals = service(), parent = role.parentId;
2091
+ const child = await approvals.harness.conversation(api.conversationId, context);
2092
+ const agent = await child.agent(context);
2093
+ const cwd = await realpath(agent.cwd);
2094
+ const delegation = (await api.snapshot(DelegationsDoc, parent, context))?.items.find((d) => d.childId === api.conversationId && d.historyThrough === undefined);
2095
+ if (!delegation)
2096
+ return { block: "Missing delegation" };
2097
+ await approvals.ensure(parent, {
2098
+ id: api.taskId,
2099
+ childId: api.conversationId,
2100
+ delegationTaskId: delegation.taskId,
2101
+ callId: call.id,
2102
+ command: String(call.arguments.command),
2103
+ cwd,
2104
+ timeout: typeof call.arguments.timeout === "number" ? call.arguments.timeout : null
2105
+ }, context);
2106
+ const result = await approvals.wait(parent, api.taskId, context);
2107
+ return result.status === "allowed" ? undefined : { block: "Command denied or cancelled by user. Use another approach or report the limitation." };
2108
+ }
2109
+ })
2110
+ ],
2111
+ wraps: [
2112
+ wrapTool(createBashTool(), (bash) => ({
2113
+ ...bash,
2114
+ execute: async (args, api, context) => {
2115
+ const role = await api.snapshot(SubagentDoc, api.conversationId, context);
2116
+ if (role?.role === "explore") {
2117
+ const item = role.parentId === null ? undefined : (await api.snapshot(ApprovalsDoc, role.parentId, context))?.items.find((a) => a.id === api.taskId);
2118
+ const cwd = api.env && await api.env.canonicalPath(api.env.cwd, context);
2119
+ if (!item || item.status !== "allowed" || item.command !== args.command || item.timeout !== (args.timeout ?? null) || !cwd?.ok || cwd.value !== item.cwd)
2120
+ throw new Error("Bash approval missing or operation changed");
2121
+ }
2122
+ context.abortSignal?.throwIfAborted();
2123
+ return bash.execute(args, api, context);
2124
+ }
2125
+ }))
2126
+ ]
2127
+ });
2128
+ }
2129
+ async function prepareApprovalConversations(harness, ids, extension, context) {
2130
+ for (const id of ids) {
2131
+ if ((await harness.snapshot(SubagentDoc, id, context))?.role !== "explore")
2132
+ continue;
2133
+ const child = await harness.conversation(id, context);
2134
+ const agent = await child.agent(context);
2135
+ await harness.commit((tx) => configure2(tx, id, {
2136
+ tools: [...agent.tools.filter((t) => t.name !== "bash"), createBashTool()],
2137
+ extensions: { add: [extension] }
2138
+ }), context);
2139
+ }
2140
+ }
2141
+
1583
2142
  // 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";
2143
+ import { Type as Type3 } from "@earendil-works/pi-ai";
2144
+ import { defineExtension as defineExtension7, defineTool as defineTool3 } from "@earendil-works/pi-durable";
1586
2145
  var SEARCH_URL = "https://api.tavily.com/search";
1587
2146
  var EXTRACT_URL = "https://api.tavily.com/extract";
1588
2147
  var REQUEST_TIMEOUT_MS = 30000;
@@ -1700,15 +2259,15 @@ function urlKey(value) {
1700
2259
  }
1701
2260
  }
1702
2261
  function createWebSearchTools(resolveKey, fetchImpl = fetch) {
1703
- return defineExtension4({
2262
+ return defineExtension7({
1704
2263
  name: "neko-web-search",
1705
2264
  tools: [
1706
- defineTool({
2265
+ defineTool3({
1707
2266
  name: "web_search",
1708
2267
  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})` }))
2268
+ parameters: Type3.Object({
2269
+ query: Type3.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
2270
+ maxResults: Type3.Optional(Type3.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
1712
2271
  }),
1713
2272
  async execute({ query, maxResults }, _api, context) {
1714
2273
  try {
@@ -1733,11 +2292,11 @@ ${truncate(String(item.url ?? ""), 300)}
1733
2292
  }
1734
2293
  }
1735
2294
  }),
1736
- defineTool({
2295
+ defineTool3({
1737
2296
  name: "web_extract",
1738
2297
  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" }), {
2298
+ parameters: Type3.Object({
2299
+ urls: Type3.Array(Type3.String({ maxLength: 2048, description: "http(s) URL" }), {
1741
2300
  minItems: 1,
1742
2301
  maxItems: MAX_URLS,
1743
2302
  description: `URLs to extract (1-${MAX_URLS})`
@@ -1782,10 +2341,10 @@ ${truncate(String(item.url ?? ""), 300)}
1782
2341
  }
1783
2342
 
1784
2343
  // packages/core/src/workspace-files.ts
1785
- import { realpath, stat } from "fs/promises";
2344
+ import { realpath as realpath2, stat } from "fs/promises";
1786
2345
  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";
2346
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
2347
+ import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
1789
2348
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
1790
2349
  function relativeQuery(query) {
1791
2350
  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 +2352,8 @@ function relativeQuery(query) {
1793
2352
  return query.replace(/^\.\//, "");
1794
2353
  }
1795
2354
  async function workspaceTarget(root, path) {
1796
- const base = await realpath(root);
1797
- const target = await realpath(resolve3(base, relativeQuery(path)));
2355
+ const base = await realpath2(root);
2356
+ const target = await realpath2(resolve3(base, relativeQuery(path)));
1798
2357
  const local = relative(base, target);
1799
2358
  if (local === ".." || local.startsWith(`..${sep}`) || isAbsolute2(local))
1800
2359
  throw new Error("Path is outside the workspace.");
@@ -1903,7 +2462,7 @@ class WorkspaceFiles {
1903
2462
  }
1904
2463
  #disposeWatch(watch) {
1905
2464
  watch.controller.abort();
1906
- const closing = watch.work.catch(() => {}).then(() => watch.watcher?.close(BACKGROUND_CONTEXT)).then(() => {}).catch(() => {}).finally(() => this.#disposing.delete(closing));
2465
+ const closing = watch.work.catch(() => {}).then(() => watch.watcher?.close(BACKGROUND_CONTEXT2)).then(() => {}).catch(() => {}).finally(() => this.#disposing.delete(closing));
1907
2466
  this.#disposing.add(closing);
1908
2467
  }
1909
2468
  #closeWatcher(item) {
@@ -1950,11 +2509,11 @@ class WorkspaceFiles {
1950
2509
  for (const user of [...current.users])
1951
2510
  this.#closeWatcher(user);
1952
2511
  }
1953
- }, withAbortSignal(current.controller.signal, BACKGROUND_CONTEXT));
2512
+ }, withAbortSignal2(current.controller.signal, BACKGROUND_CONTEXT2));
1954
2513
  if (result.ok) {
1955
2514
  current.watcher = result.value;
1956
2515
  if (current.controller.signal.aborted)
1957
- await result.value.close(BACKGROUND_CONTEXT);
2516
+ await result.value.close(BACKGROUND_CONTEXT2);
1958
2517
  } else {
1959
2518
  forget();
1960
2519
  }
@@ -1978,7 +2537,7 @@ class WorkspaceFiles {
1978
2537
  async#scan(item) {
1979
2538
  const controller = new AbortController;
1980
2539
  item.controller = controller;
1981
- const context = withAbortSignal(controller.signal, BACKGROUND_CONTEXT);
2540
+ const context = withAbortSignal2(controller.signal, BACKGROUND_CONTEXT2);
1982
2541
  while (this.#active >= this.limits.concurrent) {
1983
2542
  await new Promise((resolve, reject) => {
1984
2543
  const wake = () => {
@@ -2023,7 +2582,7 @@ class WorkspaceFiles {
2023
2582
  i.generation++;
2024
2583
  }
2025
2584
  }
2026
- const listing = getOrThrow(result);
2585
+ const listing = getOrThrow2(result);
2027
2586
  controller.signal.throwIfAborted();
2028
2587
  if (this.#closed || item.generation !== generation)
2029
2588
  throw new Error("Workspace changed during file search. Retry.");
@@ -2043,7 +2602,7 @@ class WorkspaceFiles {
2043
2602
  throw new Error("File environment closed");
2044
2603
  query = relativeQuery(query);
2045
2604
  const slash = query.lastIndexOf("/"), prefix = slash < 0 ? "" : query.slice(0, slash + 1), needle = query.slice(slash + 1);
2046
- root = await realpath(root);
2605
+ root = await realpath2(root);
2047
2606
  const base = await workspaceTarget(root, prefix);
2048
2607
  if (!(await stat(base)).isDirectory())
2049
2608
  throw new Error("Workspace directory is unavailable.");
@@ -2144,7 +2703,7 @@ function cancellable(work, signal) {
2144
2703
  // packages/core/src/runtime.ts
2145
2704
  var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
2146
2705
  var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
2147
- var ArchivedDoc = defineDoc2({
2706
+ var ArchivedDoc = defineDoc4({
2148
2707
  kind: "app.neko.conversation",
2149
2708
  version: 1,
2150
2709
  scope: "conversation",
@@ -2152,7 +2711,7 @@ var ArchivedDoc = defineDoc2({
2152
2711
  fork: "initial",
2153
2712
  initial: () => ({ archived: false })
2154
2713
  });
2155
- var WorkspacesDoc = defineDoc2({
2714
+ var WorkspacesDoc = defineDoc4({
2156
2715
  kind: "app.neko.workspaces",
2157
2716
  version: 1,
2158
2717
  scope: "conversation",
@@ -2160,7 +2719,7 @@ var WorkspacesDoc = defineDoc2({
2160
2719
  fork: "initial",
2161
2720
  initial: () => ({ items: [] })
2162
2721
  });
2163
- var ActivityDoc = defineDoc2({
2722
+ var ActivityDoc = defineDoc4({
2164
2723
  kind: "app.neko.activity",
2165
2724
  version: 1,
2166
2725
  scope: "conversation",
@@ -2169,7 +2728,7 @@ var ActivityDoc = defineDoc2({
2169
2728
  initial: () => ({ lastUsedAt: 0 })
2170
2729
  });
2171
2730
  async function createNekoRuntime(options = {}) {
2172
- const context = options.context ?? BACKGROUND_CONTEXT2;
2731
+ const context = options.context ?? BACKGROUND_CONTEXT3;
2173
2732
  const defaultCwd = normalizeWorkspace(options.cwd);
2174
2733
  const model = options.model ?? DEFAULT_MODEL;
2175
2734
  const providers = options.providers ?? [];
@@ -2205,6 +2764,7 @@ async function createNekoRuntime(options = {}) {
2205
2764
  const registry = createRegistry();
2206
2765
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
2207
2766
  registry.install(ImageDetails);
2767
+ registry.install(AgentFileSearch);
2208
2768
  registry.install(createWebSearchTools(async () => {
2209
2769
  const saved = await effectiveCredentials.read("tavily");
2210
2770
  if (saved?.type === "api_key" && saved.key)
@@ -2214,8 +2774,14 @@ async function createNekoRuntime(options = {}) {
2214
2774
  const loadResources = createResourceLoader(options.promptHome);
2215
2775
  const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
2216
2776
  registry.install(createNekoPrompt(resourceSnapshots.current));
2777
+ registry.install(createSubagentExtension(async (id, requestId, cwd) => {
2778
+ resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
2779
+ }, resourceSnapshots.discard));
2217
2780
  for (const extension of options.extensions ?? [])
2218
2781
  registry.install(extension);
2782
+ let approvals;
2783
+ const approvalExtension = createApprovalExtension(() => approvals);
2784
+ registry.install(approvalExtension);
2219
2785
  const harness = await Harness.open(storage, {
2220
2786
  models,
2221
2787
  registry,
@@ -2235,8 +2801,10 @@ async function createNekoRuntime(options = {}) {
2235
2801
  } while (cursor);
2236
2802
  return records;
2237
2803
  }, context);
2804
+ approvals = new ToolApprovals(harness, context);
2238
2805
  const existing = await scanConversations();
2239
2806
  try {
2807
+ await prepareApprovalConversations(harness, existing.map((r) => r.id), approvalExtension, context);
2240
2808
  for (const record of existing) {
2241
2809
  const target = await harness.conversation(record.id, context);
2242
2810
  if (target) {
@@ -2287,7 +2855,7 @@ async function createNekoRuntime(options = {}) {
2287
2855
  if (existing.length === 0)
2288
2856
  await metadata.initialize(conversation.id);
2289
2857
  for (const record of existing)
2290
- if (!(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
2858
+ if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
2291
2859
  await metadata.restore(record.id);
2292
2860
  let closing;
2293
2861
  const cleanup = () => {
@@ -2389,7 +2957,7 @@ async function createNekoRuntime(options = {}) {
2389
2957
  await metadata.flush();
2390
2958
  await metadata.reconcile(target.id);
2391
2959
  if (settled.status === "done" && settled.type === "input") {
2392
- const entry = await target.commit((tx) => tx.entry(AssistantEntry, settled.answer), context);
2960
+ const entry = await target.commit((tx) => tx.entry(AssistantEntry2, settled.answer), context);
2393
2961
  const message = entry?.model?.[0];
2394
2962
  const text = message?.role === "assistant" ? message.content.flatMap((block) => block.type === "text" ? [block.text] : []).join("") : "";
2395
2963
  return { status: "done", text };
@@ -2454,7 +3022,8 @@ async function createNekoRuntime(options = {}) {
2454
3022
  if (target === undefined)
2455
3023
  return;
2456
3024
  const archived = (await harness.snapshot(ArchivedDoc, id, context))?.archived ?? false;
2457
- return archived ? undefined : makeSession(target);
3025
+ const record = await harness.commit((tx) => tx.conversation(id), context);
3026
+ return archived || record?.owner ? undefined : makeSession(target);
2458
3027
  };
2459
3028
  return {
2460
3029
  context,
@@ -2485,24 +3054,60 @@ async function createNekoRuntime(options = {}) {
2485
3054
  const newest = await source.conversation.entries({}, 1, undefined, context);
2486
3055
  const at = newest.items[0]?.id;
2487
3056
  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);
3057
+ const childId = await harness.commit(async (tx) => {
3058
+ const inherited = [];
3059
+ if (at !== undefined) {
3060
+ const references = await refreshDelegations(tx, id);
3061
+ const calls = new Set;
3062
+ let cursor;
3063
+ do {
3064
+ const page = await tx.scanEntries({ conversationId: id, maxEntryId: at }, 100, cursor);
3065
+ for (const entry of page.items)
3066
+ for (const message of entry.model ?? []) {
3067
+ if (message.role === "assistant")
3068
+ for (const block of message.content) {
3069
+ if (block.type === "toolCall")
3070
+ calls.add(block.id);
3071
+ }
3072
+ }
3073
+ cursor = page.next;
3074
+ } while (cursor);
3075
+ for (const reference of references) {
3076
+ if (!calls.has(reference.callId))
3077
+ continue;
3078
+ const latest = await tx.scanEntries({ conversationId: reference.childId }, 1);
3079
+ const usage = totalUsage(await tx.doc(UsageDoc, reference.childId));
3080
+ inherited.push({
3081
+ ...reference,
3082
+ historyThrough: reference.historyThrough ?? latest.items[0]?.id ?? 0,
3083
+ historyUsage: reference.historyUsage ?? {
3084
+ input: usage.input,
3085
+ output: usage.output,
3086
+ cacheRead: usage.cacheRead,
3087
+ cacheWrite: usage.cacheWrite,
3088
+ cost: usage.cost.total
3089
+ }
3090
+ });
3091
+ }
3092
+ }
3093
+ const child = at === undefined ? await tx.createConversation({ ownership: { kind: "ownerless" } }) : await tx.forkConversation(id, at, { ownership: { kind: "ownerless" } });
3094
+ (await tx.doc(DelegationsDoc, child.id)).items = inherited;
3095
+ if (sourceMetadata) {
3096
+ const doc = await tx.doc(SessionMetadataDoc, child.id);
2495
3097
  doc.title = sourceMetadata.title || sourceMetadata.firstPromptPreview;
2496
3098
  doc.firstPromptPreview = sourceMetadata.firstPromptPreview;
2497
3099
  doc.naming = "ready";
2498
3100
  }
3101
+ return child.id;
2499
3102
  }, context);
3103
+ const child = await harness.conversation(childId, context);
2500
3104
  await child.configure({ model: agent.model, thinkingLevel: agent.thinkingLevel, cwd: savedWorkspace(agent.cwd) }, context);
2501
3105
  await metadata.initialize(child.id);
2502
3106
  await touchConversation(child.id);
2503
3107
  return makeSession(child);
2504
3108
  },
2505
3109
  harness,
3110
+ approvals,
2506
3111
  closed: harness.closed,
2507
3112
  workspaceFiles,
2508
3113
  conversation,
@@ -2528,6 +3133,8 @@ async function createNekoRuntime(options = {}) {
2528
3133
  const records = await scanConversations();
2529
3134
  const infos = [];
2530
3135
  for (const record of records) {
3136
+ if (record.owner)
3137
+ continue;
2531
3138
  const archived = (await harness.snapshot(ArchivedDoc, record.id, context))?.archived ?? false;
2532
3139
  if (!archived) {
2533
3140
  const target = await harness.conversation(record.id, context);
@@ -2566,6 +3173,7 @@ async function createNekoRuntime(options = {}) {
2566
3173
  },
2567
3174
  getConversation,
2568
3175
  async archiveConversation(id) {
3176
+ await (await sessionFor(id)).abort();
2569
3177
  await metadata.archive(id);
2570
3178
  await harness.commit(async (tx) => {
2571
3179
  (await tx.doc(ArchivedDoc, id)).archived = true;
@@ -2690,13 +3298,13 @@ async function removeGatewayPortFile(nekoDir, serverId) {
2690
3298
  }
2691
3299
 
2692
3300
  // packages/gateway/src/server-services.ts
2693
- import { randomUUID as randomUUID2 } from "crypto";
3301
+ import { randomUUID as randomUUID3 } from "crypto";
2694
3302
  import {
2695
- ChordError as ChordError2,
2696
- cell as cell2,
2697
- hostPrincipal as hostPrincipal2,
2698
- listener as listener2,
2699
- local as local2,
3303
+ ChordError as ChordError3,
3304
+ cell as cell3,
3305
+ hostPrincipal as hostPrincipal3,
3306
+ listener as listener3,
3307
+ local as local3,
2700
3308
  method as method2
2701
3309
  } from "@earendil-works/chord";
2702
3310
 
@@ -2707,8 +3315,8 @@ import { dirname as dirname5, join as join7 } from "path";
2707
3315
  import { fileURLToPath as fileURLToPath2 } from "url";
2708
3316
 
2709
3317
  // packages/core/src/application-version.ts
2710
- var RUNNING_VERSION = "0.2.8";
2711
- var APPLICATION_PROTOCOL = 1;
3318
+ var RUNNING_VERSION = "0.3.0";
3319
+ var APPLICATION_PROTOCOL = 3;
2712
3320
  function stableVersion(value) {
2713
3321
  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
3322
  }
@@ -2857,11 +3465,11 @@ class UpdateChecker {
2857
3465
  }
2858
3466
 
2859
3467
  // packages/gateway/src/server-services.ts
2860
- import { Type as Type4 } from "typebox";
3468
+ import { Type as Type7 } from "typebox";
2861
3469
 
2862
3470
  // packages/gateway/src/contract.ts
2863
3471
  import { method, remote } from "@earendil-works/chord";
2864
- import { Type as Type2 } from "typebox";
3472
+ import { Type as Type4 } from "typebox";
2865
3473
  var rpc = (args, result) => [args, result];
2866
3474
  function contract(name, def) {
2867
3475
  const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
@@ -2873,19 +3481,19 @@ function contract(name, def) {
2873
3481
  ]));
2874
3482
  return remote(name, { ...def, variants });
2875
3483
  }
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({
3484
+ var str = Type4.String();
3485
+ var num = Type4.Number();
3486
+ var bool = Type4.Boolean();
3487
+ var nil = Type4.Null();
3488
+ var integer = Type4.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
3489
+ var nullableString = Type4.Union([str, nil]);
3490
+ var phase = Type4.Enum(["pending", "running", "done"]);
3491
+ var level = Type4.Enum(["project", "user", "builtin"]);
3492
+ var thinking = Type4.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
3493
+ var model = Type4.Object({ provider: str, modelId: str });
3494
+ var change = Type4.Object({ model: Type4.Optional(model), thinkingLevel: Type4.Optional(thinking) }, { additionalProperties: false });
3495
+ var usage = Type4.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
3496
+ var image = Type4.Object({
2889
3497
  name: str,
2890
3498
  mimeType: str,
2891
3499
  width: integer,
@@ -2894,8 +3502,8 @@ var image = Type2.Object({
2894
3502
  entryId: integer,
2895
3503
  contentIndex: integer
2896
3504
  });
2897
- var skill = Type2.Object({ name: str, location: str, body: str, args: str });
2898
- var call = Type2.Object({
3505
+ var skill = Type4.Object({ name: str, location: str, body: str, args: str });
3506
+ var call = Type4.Object({
2899
3507
  callId: str,
2900
3508
  name: str,
2901
3509
  summary: str,
@@ -2903,44 +3511,65 @@ var call = Type2.Object({
2903
3511
  phase,
2904
3512
  output: nullableString
2905
3513
  });
2906
- var ChatViewSchema = Type2.Object({
3514
+ var DelegationSchema = Type4.Object({
3515
+ parentId: integer,
3516
+ taskId: integer,
3517
+ callId: str,
3518
+ childId: integer,
3519
+ role: Type4.Enum(["explore", "plan", "coder"]),
3520
+ title: str,
3521
+ createdAt: num,
3522
+ startedAt: Type4.Union([num, nil]),
3523
+ endedAt: Type4.Union([num, nil]),
3524
+ status: Type4.Enum(["queued", "running", "completed", "failed", "cancelled"]),
3525
+ report: nullableString,
3526
+ approvalCount: Type4.Optional(integer),
3527
+ historyThrough: Type4.Optional(integer),
3528
+ historyUsage: Type4.Optional(usage)
3529
+ });
3530
+ var ChatViewSchema = Type4.Object({
3531
+ historyBefore: Type4.Optional(Type4.Union([integer, nil])),
3532
+ subagents: Type4.Optional(Type4.Array(DelegationSchema)),
3533
+ taskUsage: Type4.Optional(usage),
2907
3534
  model: str,
2908
- agent: Type2.Object({ model: Type2.Union([model, nil]), thinkingLevel: Type2.Union([thinking, nil]) }),
3535
+ agent: Type4.Object({ model: Type4.Union([model, nil]), thinkingLevel: Type4.Union([thinking, nil]) }),
2909
3536
  cwd: str,
2910
3537
  busy: bool,
2911
3538
  status: nullableString,
2912
3539
  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({
3540
+ contextUsage: Type4.Optional(Type4.Object({ tokens: Type4.Union([num, nil]), window: num, percent: Type4.Union([num, nil]) })),
3541
+ compaction: Type4.Optional(Type4.Object({
3542
+ failure: Type4.Optional(Type4.Object({ taskId: integer, reason: Type4.Enum(["manual", "threshold", "overflow"]), error: str })),
3543
+ active: Type4.Array(Type4.Object({
2917
3544
  taskId: integer,
2918
- reason: Type2.Enum(["manual", "threshold", "overflow"]),
3545
+ reason: Type4.Enum(["manual", "threshold", "overflow"]),
2919
3546
  blocking: bool,
2920
3547
  attempt: integer,
2921
- retry: Type2.Optional(Type2.Object({ at: num, error: str }))
3548
+ retry: Type4.Optional(Type4.Object({ at: num, error: str }))
2922
3549
  })),
2923
- latest: Type2.Union([
3550
+ latest: Type4.Union([
2924
3551
  nil,
2925
- Type2.Object({
3552
+ Type4.Object({
2926
3553
  taskId: integer,
2927
- phase: Type2.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
3554
+ phase: Type4.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
2928
3555
  at: num
2929
3556
  })
2930
3557
  ])
2931
3558
  })),
2932
- items: Type2.Array(Type2.Union([
2933
- Type2.Object({
2934
- type: Type2.Literal("user"),
3559
+ items: Type4.Array(Type4.Union([
3560
+ Type4.Object({
3561
+ type: Type4.Literal("user"),
3562
+ order: Type4.Optional(num),
2935
3563
  key: str,
2936
3564
  id: integer,
2937
3565
  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 }))
3566
+ images: Type4.Optional(Type4.Array(image)),
3567
+ skill: Type4.Optional(skill),
3568
+ defaultImagePrompt: Type4.Optional(Type4.Object({ version: Type4.Integer({ minimum: 1 }), text: str }))
2941
3569
  }),
2942
- Type2.Object({
2943
- type: Type2.Literal("assistant"),
3570
+ Type4.Object({
3571
+ type: Type4.Literal("assistant"),
3572
+ order: Type4.Optional(num),
2944
3573
  key: str,
2945
3574
  id: integer,
2946
3575
  thinking: str,
@@ -2949,10 +3578,11 @@ var ChatViewSchema = Type2.Object({
2949
3578
  errorMessage: nullableString,
2950
3579
  note: nullableString
2951
3580
  }),
2952
- Type2.Object({
2953
- type: Type2.Literal("tool"),
3581
+ Type4.Object({
3582
+ type: Type4.Literal("tool"),
3583
+ order: Type4.Optional(num),
2954
3584
  key: str,
2955
- images: Type2.Optional(Type2.Array(image)),
3585
+ images: Type4.Optional(Type4.Array(image)),
2956
3586
  callId: str,
2957
3587
  name: str,
2958
3588
  summary: str,
@@ -2961,50 +3591,50 @@ var ChatViewSchema = Type2.Object({
2961
3591
  isError: bool,
2962
3592
  output: nullableString,
2963
3593
  result: nullableString,
2964
- durationMs: Type2.Union([num, nil]),
3594
+ durationMs: Type4.Union([num, nil]),
2965
3595
  note: nullableString
2966
3596
  }),
2967
- Type2.Object({ type: Type2.Literal("system"), key: str, id: integer, text: str })
3597
+ Type4.Object({ type: Type4.Literal("system"), order: Type4.Optional(num), key: str, id: integer, text: str })
2968
3598
  ])),
2969
- streaming: Type2.Union([nil, Type2.Object({ thinking: str, text: str, toolCalls: Type2.Array(call) })])
3599
+ streaming: Type4.Union([nil, Type4.Object({ thinking: str, text: str, toolCalls: Type4.Array(call) })])
2970
3600
  });
2971
- var SessionSummarySchema = Type2.Object({
3601
+ var SessionSummarySchema = Type4.Object({
2972
3602
  workspaceRoot: str,
2973
3603
  lastUsedAt: num,
2974
3604
  title: str,
2975
3605
  firstPromptPreview: str,
2976
3606
  createdAt: num,
2977
- lastMessageAt: Type2.Union([num, nil]),
2978
- model: Type2.Union([model, nil]),
2979
- status: Type2.Enum(["idle", "running", "error"]),
3607
+ lastMessageAt: Type4.Union([num, nil]),
3608
+ model: Type4.Union([model, nil]),
3609
+ status: Type4.Enum(["idle", "running", "error"]),
2980
3610
  id: str
2981
3611
  });
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({
3612
+ var DirectoryStateSchema = Type4.Object({ revision: integer, sessions: Type4.Array(SessionSummarySchema) });
3613
+ var WorkspaceSummarySchema = Type4.Object({ path: str, createdAt: num });
3614
+ var WorkspaceStateSchema = Type4.Object({ revision: integer, workspaces: Type4.Array(WorkspaceSummarySchema) });
3615
+ var credentialStatus = Type4.Object({
2986
3616
  savedConfigured: bool,
2987
3617
  environmentConfigured: bool,
2988
- source: Type2.Enum(["saved", "environment", "runtime", "keyless", "none"])
3618
+ source: Type4.Enum(["saved", "environment", "runtime", "keyless", "none"])
2989
3619
  });
2990
- var CredentialStateSchema = Type2.Object({
3620
+ var CredentialStateSchema = Type4.Object({
2991
3621
  revision: integer,
2992
- statuses: Type2.Object({ deepseek: credentialStatus, tavily: credentialStatus })
3622
+ statuses: Type4.Object({ deepseek: credentialStatus, tavily: credentialStatus })
2993
3623
  });
2994
- var AttachmentSchema = Type2.Object({ sessionId: str, attachmentId: str });
2995
- var chatInput = Type2.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
3624
+ var AttachmentSchema = Type4.Object({ sessionId: str, attachmentId: str });
3625
+ var chatInput = Type4.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
2996
3626
  var NekoSessionDirectory = contract("neko.session-directory", {
2997
3627
  version: 1,
2998
- input: Type2.Object({}, { additionalProperties: false }),
3628
+ input: Type4.Object({}, { additionalProperties: false }),
2999
3629
  variants: { ready: { state: { state: DirectoryStateSchema } } }
3000
3630
  });
3001
3631
  var NekoSessionManagement = contract("neko.session-management", {
3002
3632
  version: 1,
3003
- input: Type2.Object({}, { additionalProperties: false }),
3633
+ input: Type4.Object({}, { additionalProperties: false }),
3004
3634
  variants: {
3005
3635
  ready: {
3006
3636
  methods: {
3007
- create: rpc(Type2.Object({ ...change.properties, cwd: Type2.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
3637
+ create: rpc(Type4.Object({ ...change.properties, cwd: Type4.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
3008
3638
  archive: rpc(str, nil),
3009
3639
  attach: rpc(str, AttachmentSchema),
3010
3640
  detach: rpc(nil, nil),
@@ -3013,30 +3643,30 @@ var NekoSessionManagement = contract("neko.session-management", {
3013
3643
  }
3014
3644
  }
3015
3645
  });
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)
3646
+ var resize = Type4.Object({
3647
+ maxWidth: Type4.Optional(integer),
3648
+ maxHeight: Type4.Optional(integer),
3649
+ maxBytes: Type4.Optional(integer),
3650
+ jpegQuality: Type4.Optional(integer)
3021
3651
  });
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)
3652
+ var inputLimits = Type4.Object({
3653
+ maxRequestBytes: Type4.Optional(integer),
3654
+ images: Type4.Optional(Type4.Object({
3655
+ resize: Type4.Optional(resize),
3656
+ maxPerMessage: Type4.Optional(integer),
3657
+ maxPerRequest: Type4.Optional(integer)
3028
3658
  }))
3029
3659
  });
3030
- var modelInfo = Type2.Object({
3660
+ var modelInfo = Type4.Object({
3031
3661
  provider: str,
3032
3662
  modelId: str,
3033
3663
  name: str,
3034
3664
  contextWindow: num,
3035
- input: Type2.Array(str),
3036
- inputLimits: Type2.Optional(inputLimits),
3665
+ input: Type4.Array(str),
3666
+ inputLimits: Type4.Optional(inputLimits),
3037
3667
  reasoning: bool,
3038
- supportedThinkingLevels: Type2.Array(thinking),
3039
- thinkingLevelAdjustments: Type2.Object({
3668
+ supportedThinkingLevels: Type4.Array(thinking),
3669
+ thinkingLevelAdjustments: Type4.Object({
3040
3670
  off: thinking,
3041
3671
  minimal: thinking,
3042
3672
  low: thinking,
@@ -3048,10 +3678,30 @@ var modelInfo = Type2.Object({
3048
3678
  });
3049
3679
  var NekoModels = contract("neko.models", {
3050
3680
  version: 1,
3051
- input: Type2.Object({}, { additionalProperties: false }),
3052
- variants: { ready: { methods: { list: rpc(nil, Type2.Array(modelInfo)) } } }
3681
+ input: Type4.Object({}, { additionalProperties: false }),
3682
+ variants: { ready: { methods: { list: rpc(nil, Type4.Array(modelInfo)) } } }
3683
+ });
3684
+ var imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
3685
+ var NekoSubagent = contract("neko.subagent", {
3686
+ version: 1,
3687
+ input: Type4.Object({ sessionId: str, attachmentId: str, taskId: integer }),
3688
+ variants: {
3689
+ ready: {
3690
+ state: { revision: integer },
3691
+ methods: {
3692
+ chunk: rpc(Type4.Object({
3693
+ kind: Type4.Enum(["view", "history"]),
3694
+ before: Type4.Optional(integer),
3695
+ token: Type4.Optional(integer),
3696
+ offset: integer
3697
+ }), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
3698
+ getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
3699
+ decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
3700
+ stop: rpc(nil, nil)
3701
+ }
3702
+ }
3703
+ }
3053
3704
  });
3054
- var imageChunk = Type2.Object({ data: str, total: integer, mimeType: str });
3055
3705
  var NekoChat = contract("neko.chat", {
3056
3706
  version: 1,
3057
3707
  input: chatInput,
@@ -3059,79 +3709,79 @@ var NekoChat = contract("neko.chat", {
3059
3709
  ready: {
3060
3710
  state: { view: ChatViewSchema },
3061
3711
  methods: {
3062
- submit: rpc(Type2.Object({
3712
+ submit: rpc(Type4.Object({
3063
3713
  content: str,
3064
- images: Type2.Optional(Type2.Array(str, { maxItems: 4 })),
3065
- requestId: Type2.Optional(str),
3066
- whenBusy: Type2.Optional(Type2.Enum(["steer", "followUp"]))
3714
+ images: Type4.Optional(Type4.Array(str, { maxItems: 4 })),
3715
+ requestId: Type4.Optional(str),
3716
+ whenBusy: Type4.Optional(Type4.Enum(["steer", "followUp"]))
3067
3717
  }, { additionalProperties: false }), nil),
3068
3718
  abort: rpc(nil, nil),
3069
3719
  usage: rpc(nil, usage),
3070
- skills: rpc(nil, Type2.Array(Type2.Object({ name: str, description: str, location: str, level }))),
3720
+ skills: rpc(nil, Type4.Array(Type4.Object({ name: str, description: str, location: str, level }))),
3071
3721
  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),
3722
+ compact: rpc(Type4.Object({ instructions: Type4.Optional(str) }, { additionalProperties: false }), nil),
3723
+ importImage: rpc(str, Type4.Object({ id: str, name: str })),
3724
+ draftImage: rpc(Type4.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
3725
+ beginImage: rpc(Type4.Object({ name: str, mimeType: str, size: Type4.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
3726
+ appendImage: rpc(Type4.Object({ id: str, offset: integer, data: Type4.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
3077
3727
  finishImage: rpc(str, nil),
3078
3728
  releaseImage: rpc(str, nil),
3079
- getImage: rpc(Type2.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
3729
+ getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
3080
3730
  }
3081
3731
  }
3082
3732
  }
3083
3733
  });
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({
3734
+ var browse = Type4.Object({ path: str, query: Type4.Optional(str), showHidden: Type4.Optional(bool), cursor: Type4.Optional(str) }, { additionalProperties: false });
3735
+ var page = Type4.Object({
3086
3736
  path: str,
3087
3737
  parent: nullableString,
3088
- entries: Type2.Array(Type2.Object({ name: str, path: str, error: Type2.Optional(str) })),
3738
+ entries: Type4.Array(Type4.Object({ name: str, path: str, error: Type4.Optional(str) })),
3089
3739
  next: nullableString
3090
3740
  });
3091
3741
  var NekoWorkspaces = contract("neko.workspaces", {
3092
3742
  version: 1,
3093
- input: Type2.Object({}, { additionalProperties: false }),
3743
+ input: Type4.Object({}, { additionalProperties: false }),
3094
3744
  variants: {
3095
3745
  ready: {
3096
3746
  state: { state: WorkspaceStateSchema },
3097
3747
  methods: {
3098
- add: rpc(Type2.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
3748
+ add: rpc(Type4.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
3099
3749
  browse: rpc(browse, page)
3100
3750
  }
3101
3751
  }
3102
3752
  }
3103
3753
  });
3104
- var provider = Type2.Enum(["deepseek", "tavily"]);
3754
+ var provider = Type4.Enum(["deepseek", "tavily"]);
3105
3755
  var NekoCredentials = contract("neko.credentials", {
3106
3756
  version: 1,
3107
- input: Type2.Object({}, { additionalProperties: false }),
3757
+ input: Type4.Object({}, { additionalProperties: false }),
3108
3758
  variants: {
3109
3759
  ready: {
3110
3760
  state: { state: CredentialStateSchema },
3111
3761
  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)
3762
+ save: rpc(Type4.Object({ provider, key: Type4.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
3763
+ remove: rpc(Type4.Object({ provider }, { additionalProperties: false }), nil)
3114
3764
  }
3115
3765
  }
3116
3766
  }
3117
3767
  });
3118
- var FileCandidatesSchema = Type2.Object({
3119
- entries: Type2.Array(Type2.Object({ name: str, path: str, kind: Type2.Enum(["file", "directory"]) }), { maxItems: 100 }),
3768
+ var FileCandidatesSchema = Type4.Object({
3769
+ entries: Type4.Array(Type4.Object({ name: str, path: str, kind: Type4.Enum(["file", "directory"]) }), { maxItems: 100 }),
3120
3770
  limited: bool,
3121
3771
  incomplete: bool
3122
3772
  });
3123
3773
  var NekoWorkspaceFiles = contract("neko.workspace-files", {
3124
3774
  version: 1,
3125
- input: Type2.Object({}, { additionalProperties: false }),
3775
+ input: Type4.Object({}, { additionalProperties: false }),
3126
3776
  variants: {
3127
3777
  ready: {
3128
3778
  methods: {
3129
- search: rpc(Type2.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
3779
+ search: rpc(Type4.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
3130
3780
  }
3131
3781
  }
3132
3782
  }
3133
3783
  });
3134
- var marketSkill = Type2.Object({
3784
+ var marketSkill = Type4.Object({
3135
3785
  name: str,
3136
3786
  description: str,
3137
3787
  category: str,
@@ -3141,38 +3791,38 @@ var marketSkill = Type2.Object({
3141
3791
  sha256: str,
3142
3792
  sizeBytes: integer,
3143
3793
  uploadedAt: str,
3144
- installed: Type2.Object({ project: bool, user: bool, builtin: bool }),
3145
- activeLevel: Type2.Union([level, nil])
3794
+ installed: Type4.Object({ project: bool, user: bool, builtin: bool }),
3795
+ activeLevel: Type4.Union([level, nil])
3146
3796
  });
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) });
3797
+ var preview = Type4.Object({ body: str, files: Type4.Array(str) });
3798
+ var scope = Type4.Enum(["user", "project"]);
3799
+ var installResult = Type4.Object({ name: str, scope, path: str, shadowedBy: Type4.Optional(scope) });
3150
3800
  var NekoSkillMarket = contract("neko.skill-market", {
3151
3801
  version: 1,
3152
- input: Type2.Object({}, { additionalProperties: false }),
3802
+ input: Type4.Object({}, { additionalProperties: false }),
3153
3803
  variants: {
3154
3804
  ready: {
3155
3805
  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)
3806
+ list: rpc(Type4.Object({ query: Type4.Optional(str), workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), Type4.Array(marketSkill)),
3807
+ detail: rpc(Type4.Object({ name: str, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), marketSkill),
3808
+ preview: rpc(Type4.Object({ name: str }, { additionalProperties: false }), preview),
3809
+ install: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str), overwrite: Type4.Optional(bool) }, { additionalProperties: false }), installResult),
3810
+ uninstall: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), nil)
3161
3811
  }
3162
3812
  }
3163
3813
  }
3164
3814
  });
3165
- var ApplicationInfoSchema = Type2.Object({
3815
+ var ApplicationInfoSchema = Type4.Object({
3166
3816
  runningVersion: str,
3167
3817
  installedVersion: nullableString,
3168
- protocolVersion: Type2.Integer({ minimum: 1 }),
3818
+ protocolVersion: Type4.Integer({ minimum: 1 }),
3169
3819
  latestVersion: nullableString,
3170
- checkedAt: Type2.Union([integer, nil]),
3171
- checkState: Type2.Enum(["unknown", "checking", "ok", "failed", "development"])
3820
+ checkedAt: Type4.Union([integer, nil]),
3821
+ checkState: Type4.Enum(["unknown", "checking", "ok", "failed", "development"])
3172
3822
  });
3173
3823
  var NekoApplicationInfo = contract("neko.application-info", {
3174
3824
  version: 1,
3175
- input: Type2.Object({}),
3825
+ input: Type4.Object({}),
3176
3826
  variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
3177
3827
  });
3178
3828
 
@@ -3180,16 +3830,17 @@ var NekoApplicationInfo = contract("neko.application-info", {
3180
3830
  import { open as open2 } from "fs/promises";
3181
3831
  import { basename as basename3, extname } from "path";
3182
3832
  import {
3183
- ChordError,
3184
- cell,
3185
- hostPrincipal,
3186
- listener,
3187
- local,
3188
- opaque,
3189
- resource
3833
+ ChordError as ChordError2,
3834
+ cell as cell2,
3835
+ hostPrincipal as hostPrincipal2,
3836
+ listener as listener2,
3837
+ local as local2,
3838
+ opaque as opaque2,
3839
+ resource as resource2
3190
3840
  } from "@earendil-works/chord";
3191
- import { withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
3192
- import { Type as Type3 } from "typebox";
3841
+ import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
3842
+ import { UsageDoc as UsageDoc2 } from "@earendil-works/pi-durable";
3843
+ import { Type as Type6 } from "typebox";
3193
3844
 
3194
3845
  // packages/gateway/src/image-uploads.ts
3195
3846
  var IMAGE_CHUNK_BYTES = 256 * 1024;
@@ -3285,19 +3936,241 @@ class ImageUploads {
3285
3936
  }
3286
3937
  }
3287
3938
 
3939
+ // packages/gateway/src/subagent-host.ts
3940
+ import {
3941
+ ChordError,
3942
+ cell,
3943
+ hostPrincipal,
3944
+ listener,
3945
+ local,
3946
+ opaque,
3947
+ resource
3948
+ } from "@earendil-works/chord";
3949
+ import { Type as Type5 } from "typebox";
3950
+ async function openReader(runtime, childId, parentId, context, through, historyUsage) {
3951
+ const child = await runtime.harness.conversation(childId, context);
3952
+ if (!child)
3953
+ throw new ChordError("gone", "Subagent unavailable");
3954
+ const native = await child.viewState(context);
3955
+ if (through !== undefined)
3956
+ native.dispose();
3957
+ const project = (entries, streaming = true) => {
3958
+ const result = projectChatView({ ...native.value, entries: [...entries].sort((a, b) => a.id - b.id) }, "", {
3959
+ models: runtime.listModels(),
3960
+ contextEntries: native.value.entries
3961
+ });
3962
+ if (!streaming || through !== undefined) {
3963
+ result.streaming = null;
3964
+ result.busy = false;
3965
+ result.status = null;
3966
+ if (through !== undefined) {
3967
+ result.usage = historyUsage ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
3968
+ delete result.contextUsage;
3969
+ }
3970
+ }
3971
+ let order = 0;
3972
+ for (const item of result.items) {
3973
+ if (item.type !== "tool")
3974
+ order = item.id;
3975
+ item.order = order;
3976
+ }
3977
+ return result;
3978
+ };
3979
+ const pageAt = async (before) => {
3980
+ const max = Math.min(before === undefined ? Infinity : before - 1, through ?? Infinity);
3981
+ const page = await child.entries(Number.isFinite(max) ? { maxEntryId: max } : {}, 100, undefined, context);
3982
+ const entries = [...page.items];
3983
+ while (page.next && !["assistant", "user"].includes(entries.at(-1)?.model?.[0]?.role ?? "")) {
3984
+ const extra = await child.entries({ maxEntryId: entries.at(-1).id - 1 }, 1, undefined, context);
3985
+ entries.push(...extra.items);
3986
+ if (!extra.items.length || !extra.next)
3987
+ break;
3988
+ }
3989
+ const first = entries.at(-1)?.id;
3990
+ const older = first === undefined ? [] : (await child.entries({ maxEntryId: first - 1 }, 1, undefined, context)).items;
3991
+ return { entries, next: older.length ? first : null };
3992
+ };
3993
+ let revision = 0;
3994
+ let sequence = 0;
3995
+ const payloads = new Map;
3996
+ const remember = (value) => {
3997
+ const token = ++sequence;
3998
+ payloads.set(token, JSON.stringify(value));
3999
+ while (payloads.size > 4) {
4000
+ const oldest = [...payloads.keys()].find((key) => key !== revision);
4001
+ payloads.delete(oldest);
4002
+ }
4003
+ return token;
4004
+ };
4005
+ return {
4006
+ subscribe: (fn) => {
4007
+ if (through !== undefined)
4008
+ return () => {};
4009
+ const stop = native.subscribe(() => fn(null));
4010
+ const stopApprovals = runtime.harness.subscribeCommits(({ changes }) => {
4011
+ if (changes.some((c) => c.type === "document" && c.record.kind === "app.neko.approvals" && c.conversationId === parentId))
4012
+ fn(null);
4013
+ });
4014
+ return () => {
4015
+ stop();
4016
+ stopApprovals();
4017
+ };
4018
+ },
4019
+ async refresh() {
4020
+ const page = await pageAt();
4021
+ const approvals = through === undefined ? (await runtime.approvals.list(parentId, context)).filter((a) => a.childId === childId) : [];
4022
+ revision = remember({ ...project(page.entries), approvals, historyBefore: page.next });
4023
+ return revision;
4024
+ },
4025
+ async chunk(input) {
4026
+ let token = input.token;
4027
+ if (token === undefined) {
4028
+ if (input.kind === "view")
4029
+ token = revision;
4030
+ else {
4031
+ const page = await pageAt(input.before);
4032
+ token = remember({ items: project(page.entries, false).items, next: page.next });
4033
+ }
4034
+ }
4035
+ const payload = payloads.get(token);
4036
+ if (payload === undefined)
4037
+ throw new ChordError("gone", "Transcript changed; reload");
4038
+ if (input.offset > payload.length)
4039
+ throw new ChordError("invalid", "Invalid transcript range");
4040
+ const end = Math.min(payload.length, input.offset + 32768);
4041
+ return { token, data: payload.slice(input.offset, end), next: end < payload.length ? end : null };
4042
+ },
4043
+ async history(before) {
4044
+ const page = await pageAt(before);
4045
+ return { items: project(page.entries, false).items, next: page.next };
4046
+ },
4047
+ async image(entryId, contentIndex, offset) {
4048
+ if (through !== undefined && entryId > through)
4049
+ throw new ChordError("forbidden", "Image outside fork history");
4050
+ const page = await child.entries({ minEntryId: entryId, maxEntryId: entryId }, 1, undefined, context);
4051
+ const content = page.items[0]?.model?.[0]?.content;
4052
+ const image = Array.isArray(content) ? content[contentIndex] : undefined;
4053
+ if (image?.type !== "image")
4054
+ throw new ChordError("invalid", "Image not in subagent conversation");
4055
+ const bytes = Buffer.from(image.data, "base64");
4056
+ if (offset > bytes.length)
4057
+ throw new ChordError("invalid", "Invalid image range");
4058
+ return {
4059
+ data: bytes.subarray(offset, offset + IMAGE_CHUNK_BYTES).toString("base64"),
4060
+ total: bytes.length,
4061
+ mimeType: image.mimeType
4062
+ };
4063
+ },
4064
+ close: () => native.dispose()
4065
+ };
4066
+ }
4067
+ var ChildProjection = local("neko.subagent-projection", {
4068
+ version: 1,
4069
+ input: Type5.Object({ parentId: Type5.Integer(), taskId: Type5.Integer() }),
4070
+ members: {
4071
+ revision: Type5.Integer(),
4072
+ reader: opaque("neko.subagent-reader@1")
4073
+ }
4074
+ });
4075
+ function installSubagentHost(host, runtime, context) {
4076
+ host.serve(ChildProjection, (input, _principal, self) => {
4077
+ const { revision } = self.state({ revision: cell(Type5.Integer(), 0) });
4078
+ const { reader } = self.resources({
4079
+ reader: resource({
4080
+ rev: 1,
4081
+ async open() {
4082
+ const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => d.taskId === input.taskId);
4083
+ if (!entry)
4084
+ throw new ChordError("gone", "Subagent unavailable");
4085
+ const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
4086
+ try {
4087
+ const initial = await r.refresh();
4088
+ await host.root((ctx) => revision.replace(ctx, initial));
4089
+ return r;
4090
+ } catch (error) {
4091
+ r.close();
4092
+ throw error;
4093
+ }
4094
+ },
4095
+ close: (r) => r.close()
4096
+ })
4097
+ });
4098
+ self.listen({
4099
+ view: listener(reader, (r) => r, async (_event, ctx) => {
4100
+ revision.replace(ctx, await reader.value.refresh());
4101
+ })
4102
+ });
4103
+ return { revision, reader: self.expose(reader) };
4104
+ });
4105
+ host.serve(NekoSubagent, (input, principal, self) => {
4106
+ const owner = self.get(Owner, {}, { principal });
4107
+ const check = async () => {
4108
+ if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
4109
+ throw new ChordError("forbidden", "Attachment expired");
4110
+ if (!await runtime.getConversation(Number(input.sessionId)))
4111
+ throw new ChordError("gone", "Session unavailable");
4112
+ const entry = (await listDelegations(runtime.harness, Number(input.sessionId), context)).find((d) => d.taskId === input.taskId);
4113
+ if (!entry)
4114
+ throw new ChordError("forbidden", "Subagent does not belong to this session");
4115
+ return entry;
4116
+ };
4117
+ const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
4118
+ const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), taskId: input.taskId }, { principal: hostPrincipal });
4119
+ const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
4120
+ self.listen({
4121
+ authorizedView: listener(shared.revision, async (value, ctx) => {
4122
+ try {
4123
+ target.value;
4124
+ await check();
4125
+ if (value)
4126
+ authorizedRevision.replace(ctx, value);
4127
+ } catch {
4128
+ authorizedRevision.replace(ctx, 0);
4129
+ }
4130
+ })
4131
+ });
4132
+ return {
4133
+ kind: "ready",
4134
+ revision: authorizedRevision,
4135
+ chunk: async (request) => {
4136
+ await check();
4137
+ return shared.reader.chunk(request);
4138
+ },
4139
+ getImage: async ({ entryId, contentIndex, offset }) => {
4140
+ await check();
4141
+ return shared.reader.image(entryId, contentIndex, offset);
4142
+ },
4143
+ decideApproval: async ({ id, version, decision }) => {
4144
+ const entry = await check();
4145
+ if (entry.historyThrough !== undefined)
4146
+ throw new ChordError("forbidden", "Fork history is read-only");
4147
+ return runtime.approvals.decide(Number(input.sessionId), entry.taskId, id, version, decision, context);
4148
+ },
4149
+ stop: async () => {
4150
+ const entry = await check();
4151
+ if (entry.historyThrough !== undefined)
4152
+ throw new ChordError("forbidden", "Fork history is read-only");
4153
+ await runtime.harness.abortTask(entry.taskId, context);
4154
+ await runtime.harness.waitForTask(entry.taskId, context);
4155
+ return null;
4156
+ }
4157
+ };
4158
+ });
4159
+ }
4160
+
3288
4161
  // packages/gateway/src/session-host.ts
3289
- var OwnerInput = Type3.Object({});
3290
- var Owner = local("neko.attachment-owner", {
4162
+ var OwnerInput = Type6.Object({});
4163
+ var Owner = local2("neko.attachment-owner", {
3291
4164
  version: 1,
3292
4165
  input: OwnerInput,
3293
- members: { owner: opaque("neko.owner@1") }
4166
+ members: { owner: opaque2("neko.owner@1") }
3294
4167
  });
3295
- var Projection = local("neko.projection", {
4168
+ var Projection = local2("neko.projection", {
3296
4169
  version: 1,
3297
- input: Type3.Object({ id: Type3.String() }),
4170
+ input: Type6.Object({ id: Type6.String() }),
3298
4171
  members: {
3299
4172
  view: ChatViewSchema,
3300
- reader: opaque("neko.projection-reader@1")
4173
+ reader: opaque2("neko.projection-reader@1")
3301
4174
  }
3302
4175
  });
3303
4176
  var emptyView = {
@@ -3323,6 +4196,11 @@ async function openProjection(runtime, session, context) {
3323
4196
  let changed;
3324
4197
  const stop = view.subscribe(() => changed?.(null));
3325
4198
  const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
4199
+ let childIds = new Set;
4200
+ const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
4201
+ if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
4202
+ changed?.(null);
4203
+ });
3326
4204
  return {
3327
4205
  subscribe(fn) {
3328
4206
  changed = fn;
@@ -3335,7 +4213,7 @@ async function openProjection(runtime, session, context) {
3335
4213
  for (const entry of view.value.entries)
3336
4214
  history.set(entry.id, entry);
3337
4215
  const [naming, compaction] = await Promise.all([runtime.namingUsage(session.id), session.compactionRecord()]);
3338
- return projectChatView({
4216
+ const projected = projectChatView({
3339
4217
  ...view.value,
3340
4218
  entries: [...history.values()].sort((a, b) => a.id - b.id),
3341
4219
  docs: {
@@ -3346,6 +4224,29 @@ async function openProjection(runtime, session, context) {
3346
4224
  }, naming)
3347
4225
  }
3348
4226
  }, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
4227
+ const subagents = await listDelegations(runtime.harness, session.id, context);
4228
+ const approvals = await runtime.approvals.list(session.id, context);
4229
+ for (const entry of subagents)
4230
+ entry.approvalCount = entry.historyThrough === undefined ? approvals.filter((a) => a.delegationTaskId === entry.taskId && a.status === "pending").length : 0;
4231
+ childIds = new Set(subagents.filter((entry) => entry.historyThrough === undefined).map((entry) => entry.childId));
4232
+ let aggregate = await session.usage();
4233
+ for (const child of subagents.filter((entry) => entry.historyThrough === undefined))
4234
+ aggregate = mergeUsage(aggregate, await runtime.harness.snapshot(UsageDoc2, child.childId, context) ?? {
4235
+ models: {},
4236
+ tools: {}
4237
+ });
4238
+ const u = totalUsage(aggregate);
4239
+ return {
4240
+ ...projected,
4241
+ subagents: subagents.map((entry) => ({ ...entry, report: entry.report?.slice(0, 512) ?? null })),
4242
+ taskUsage: {
4243
+ input: u.input,
4244
+ output: u.output,
4245
+ cacheRead: u.cacheRead,
4246
+ cacheWrite: u.cacheWrite,
4247
+ cost: u.cost.total
4248
+ }
4249
+ };
3349
4250
  },
3350
4251
  async entry(id, ctx) {
3351
4252
  ctx.abortSignal?.throwIfAborted();
@@ -3356,15 +4257,17 @@ async function openProjection(runtime, session, context) {
3356
4257
  close() {
3357
4258
  stop();
3358
4259
  stopMetadata();
4260
+ stopChildren();
3359
4261
  view.dispose();
3360
4262
  history.clear();
3361
4263
  }
3362
4264
  };
3363
4265
  }
3364
4266
  function serveSessions(host, runtime, context) {
4267
+ installSubagentHost(host, runtime, context);
3365
4268
  host.serve(Owner, (_input, _principal, self) => {
3366
4269
  const { owner } = self.resources({
3367
- owner: resource({
4270
+ owner: resource2({
3368
4271
  rev: 1,
3369
4272
  open: () => ({ active: null }),
3370
4273
  close: (value) => {
@@ -3375,14 +4278,14 @@ function serveSessions(host, runtime, context) {
3375
4278
  return { owner: self.expose(owner) };
3376
4279
  });
3377
4280
  host.serve(Projection, (input, _principal, self) => {
3378
- const { view } = self.state({ view: cell(ChatViewSchema, emptyView) });
4281
+ const { view } = self.state({ view: cell2(ChatViewSchema, emptyView) });
3379
4282
  const { projection } = self.resources({
3380
- projection: resource({
4283
+ projection: resource2({
3381
4284
  rev: 1,
3382
4285
  async open({ signal }) {
3383
4286
  const session = await runtime.getConversation(Number(input.id));
3384
4287
  if (!session)
3385
- throw new ChordError("gone", "Session unavailable");
4288
+ throw new ChordError2("gone", "Session unavailable");
3386
4289
  const opened = await openProjection(runtime, session, context);
3387
4290
  try {
3388
4291
  const initial = await opened.project();
@@ -3397,7 +4300,7 @@ function serveSessions(host, runtime, context) {
3397
4300
  })
3398
4301
  });
3399
4302
  self.listen({
3400
- view: listener(projection, (value) => value, async (_event, ctx) => {
4303
+ view: listener2(projection, (value) => value, async (_event, ctx) => {
3401
4304
  view.replace(ctx, await projection.value.project());
3402
4305
  })
3403
4306
  });
@@ -3405,18 +4308,18 @@ function serveSessions(host, runtime, context) {
3405
4308
  });
3406
4309
  host.serve(NekoChat, (input, principal, self) => {
3407
4310
  const owner = self.get(Owner, {}, { principal });
3408
- const shared = self.get(Projection, { id: input.sessionId }, { principal: hostPrincipal });
4311
+ const shared = self.get(Projection, { id: input.sessionId }, { principal: hostPrincipal2 });
3409
4312
  const { uploads, projection } = self.resources({
3410
- uploads: resource({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
3411
- projection: resource({
4313
+ uploads: resource2({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
4314
+ projection: resource2({
3412
4315
  rev: 1,
3413
4316
  deps: [owner],
3414
4317
  async open() {
3415
4318
  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");
4319
+ throw new ChordError2("forbidden", "Attachment does not belong to this connection");
3417
4320
  const session = await runtime.getConversation(Number(input.sessionId));
3418
4321
  if (!session)
3419
- throw new ChordError("gone", "Session unavailable");
4322
+ throw new ChordError2("gone", "Session unavailable");
3420
4323
  return {
3421
4324
  session
3422
4325
  };
@@ -3427,7 +4330,7 @@ function serveSessions(host, runtime, context) {
3427
4330
  try {
3428
4331
  return action();
3429
4332
  } catch (error) {
3430
- throw new ChordError("invalid", error instanceof Error ? error.message : "Image upload failed");
4333
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Image upload failed");
3431
4334
  }
3432
4335
  };
3433
4336
  const behavior = {
@@ -3483,7 +4386,7 @@ function serveSessions(host, runtime, context) {
3483
4386
  await projection.value.session.compact(instructions);
3484
4387
  return null;
3485
4388
  } catch (error) {
3486
- throw new ChordError("invalid", error instanceof Error ? error.message : "Cannot compact");
4389
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot compact");
3487
4390
  }
3488
4391
  },
3489
4392
  beginImage: async (input) => safeUpload(() => uploads.value.begin(input)),
@@ -3501,15 +4404,15 @@ function serveSessions(host, runtime, context) {
3501
4404
  },
3502
4405
  getImage: async (input, ctx) => {
3503
4406
  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));
4407
+ throw new ChordError2("invalid", "Invalid image range");
4408
+ const entry = await shared.reader.entry(input.entryId, withAbortSignal3(ctx.signal, context));
3506
4409
  const content = entry?.model?.[0]?.content;
3507
4410
  const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
3508
4411
  if (image?.type !== "image")
3509
- throw new ChordError("invalid", "Image is not part of this conversation");
4412
+ throw new ChordError2("invalid", "Image is not part of this conversation");
3510
4413
  const bytes = Buffer.from(image.data, "base64");
3511
4414
  if (input.offset > bytes.length)
3512
- throw new ChordError("invalid", "Invalid image range");
4415
+ throw new ChordError2("invalid", "Invalid image range");
3513
4416
  return {
3514
4417
  data: bytes.subarray(input.offset, input.offset + IMAGE_CHUNK_BYTES).toString("base64"),
3515
4418
  total: bytes.length,
@@ -3529,8 +4432,8 @@ function serveSessions(host, runtime, context) {
3529
4432
  uploads.value.release(id);
3530
4433
  } catch (error) {
3531
4434
  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");
4435
+ throw new ChordError2("invalid", "DEEPSEEK_KEY_REQUIRED: Configure DeepSeek in /login or Web settings.");
4436
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot submit message");
3534
4437
  }
3535
4438
  return null;
3536
4439
  },
@@ -3552,16 +4455,16 @@ function serveSessions(host, runtime, context) {
3552
4455
  configure: async (change) => {
3553
4456
  const applied = await runtime.configureConversation(projection.value.session.id, change);
3554
4457
  if (!applied) {
3555
- throw new ChordError("invalid", `Session ${String(projection.value.session.id)} is unavailable or archived`);
4458
+ throw new ChordError2("invalid", `Session ${String(projection.value.session.id)} is unavailable or archived`);
3556
4459
  }
3557
4460
  return null;
3558
4461
  }
3559
4462
  };
3560
4463
  const guard = async () => {
3561
4464
  if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
3562
- throw new ChordError("gone", "Attachment is no longer active");
4465
+ throw new ChordError2("gone", "Attachment is no longer active");
3563
4466
  if (!await runtime.getConversation(Number(input.sessionId)))
3564
- throw new ChordError("gone", "Session unavailable or archived");
4467
+ throw new ChordError2("gone", "Session unavailable or archived");
3565
4468
  };
3566
4469
  for (const name of Object.keys(NekoChat.def.variants.ready.methods)) {
3567
4470
  const method = behavior[name];
@@ -3576,7 +4479,7 @@ function serveSessions(host, runtime, context) {
3576
4479
  }
3577
4480
 
3578
4481
  // packages/gateway/src/skill-market.ts
3579
- import { createHash, randomUUID } from "crypto";
4482
+ import { createHash, randomUUID as randomUUID2 } from "crypto";
3580
4483
  import { mkdir as mkdir3, readdir as readdir2, rename as rename3, rm as rm3, stat as stat2, writeFile as writeFile3 } from "fs/promises";
3581
4484
  import { homedir as homedir6 } from "os";
3582
4485
  import { dirname as dirname6, join as join8 } from "path";
@@ -4150,7 +5053,7 @@ function createSkillMarketClient(options) {
4150
5053
  const skill = await detail(name);
4151
5054
  const files = unpack(skill, await download(skill));
4152
5055
  await sweepTempDirs(skillsDir);
4153
- const temp = join8(dirname6(skillsDir), `.tmp-${randomUUID()}`);
5056
+ const temp = join8(dirname6(skillsDir), `.tmp-${randomUUID2()}`);
4154
5057
  try {
4155
5058
  for (const [relative, bytes] of files) {
4156
5059
  const dest = join8(temp, ...relative.split("/"));
@@ -4271,14 +5174,14 @@ async function listDrives() {
4271
5174
  }
4272
5175
 
4273
5176
  // packages/gateway/src/server-services.ts
4274
- var ServerState = local2("neko.server-state", {
5177
+ var ServerState = local3("neko.server-state", {
4275
5178
  version: 1,
4276
- input: Type4.Object({}),
5179
+ input: Type7.Object({}),
4277
5180
  members: {
4278
5181
  directory: DirectoryStateSchema,
4279
5182
  workspaces: WorkspaceStateSchema,
4280
5183
  credentials: CredentialStateSchema,
4281
- refreshCredentials: method2(Type4.Null(), Type4.Null())
5184
+ refreshCredentials: method2(Type7.Null(), Type7.Null())
4282
5185
  }
4283
5186
  });
4284
5187
  async function createNekoServerServices(options) {
@@ -4298,9 +5201,9 @@ async function createNekoServerServices(options) {
4298
5201
  listener(updates.state);
4299
5202
  };
4300
5203
  host.serve(NekoApplicationInfo, (_input, _principal, self) => {
4301
- const state = self.state({ info: cell2(ApplicationInfoSchema, updates.state) });
5204
+ const state = self.state({ info: cell3(ApplicationInfoSchema, updates.state) });
4302
5205
  self.listen({
4303
- updates: listener2({
5206
+ updates: listener3({
4304
5207
  subscribe: (fn) => {
4305
5208
  updateListeners.add(fn);
4306
5209
  return () => updateListeners.delete(fn);
@@ -4332,7 +5235,7 @@ async function createNekoServerServices(options) {
4332
5235
  const toRemote = (error) => {
4333
5236
  if (error instanceof SkillMarketError) {
4334
5237
  const code = error.code === "skill_not_found" || error.code === "skill_not_installed" ? "gone" : "invalid";
4335
- throw new ChordError2(code, error.message, { reason: error.code });
5238
+ throw new ChordError3(code, error.message, { reason: error.code });
4336
5239
  }
4337
5240
  throw error;
4338
5241
  };
@@ -4366,12 +5269,12 @@ async function createNekoServerServices(options) {
4366
5269
  };
4367
5270
  host.serve(ServerState, (_input, _principal, self) => {
4368
5271
  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 })
5272
+ directory: cell3(DirectoryStateSchema, { revision: 1, sessions }),
5273
+ workspaces: cell3(WorkspaceStateSchema, { revision: 1, workspaces }),
5274
+ credentials: cell3(CredentialStateSchema, { revision: 1, statuses })
4372
5275
  });
4373
5276
  self.listen({
4374
- directory: listener2({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
5277
+ directory: listener3({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
4375
5278
  if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
4376
5279
  "app.neko.session-metadata",
4377
5280
  "app.neko.activity",
@@ -4398,7 +5301,7 @@ async function createNekoServerServices(options) {
4398
5301
  }
4399
5302
  };
4400
5303
  }, { eager: { input: {} } });
4401
- const shared = (self) => self.get(ServerState, {}, { principal: hostPrincipal2 });
5304
+ const shared = (self) => self.get(ServerState, {}, { principal: hostPrincipal3 });
4402
5305
  host.serve(NekoSessionDirectory, (_input, _principal, self) => ({ kind: "ready", state: shared(self).directory }));
4403
5306
  host.serve(NekoWorkspaces, (_input, _principal, self) => ({
4404
5307
  kind: "ready",
@@ -4434,7 +5337,7 @@ async function createNekoServerServices(options) {
4434
5337
  search: async (input, ctx) => {
4435
5338
  const roots = await runtime.listWorkspaces();
4436
5339
  if (!roots.some((root) => root.path === input.workspaceRoot))
4437
- throw new ChordError2("invalid", "Unknown workspace.");
5340
+ throw new ChordError3("invalid", "Unknown workspace.");
4438
5341
  return runtime.workspaceFiles.search(input.workspaceRoot, input.query, ctx.signal);
4439
5342
  }
4440
5343
  }));
@@ -4444,7 +5347,7 @@ async function createNekoServerServices(options) {
4444
5347
  const summary = async (id) => {
4445
5348
  const found = (await list()).find((item) => item.id === String(id));
4446
5349
  if (!found)
4447
- throw new ChordError2("gone", "Session unavailable");
5350
+ throw new ChordError3("gone", "Session unavailable");
4448
5351
  return found;
4449
5352
  };
4450
5353
  return {
@@ -4457,9 +5360,9 @@ async function createNekoServerServices(options) {
4457
5360
  },
4458
5361
  attach: (id) => serialize(async () => {
4459
5362
  if (!await runtime.getConversation(toConversationId(id)))
4460
- throw new ChordError2("gone", `Unknown session: ${id}`);
5363
+ throw new ChordError3("gone", `Unknown session: ${id}`);
4461
5364
  if (owner.owner.active?.sessionId !== id) {
4462
- owner.owner.active = { sessionId: id, attachmentId: randomUUID2() };
5365
+ owner.owner.active = { sessionId: id, attachmentId: randomUUID3() };
4463
5366
  await runtime.touchConversation(toConversationId(id));
4464
5367
  }
4465
5368
  return owner.owner.active;
@@ -4495,7 +5398,7 @@ async function createNekoServerServices(options) {
4495
5398
  function toConversationId(sessionId) {
4496
5399
  const numeric = Number(sessionId);
4497
5400
  if (!Number.isSafeInteger(numeric) || numeric < 1 || String(numeric) !== sessionId)
4498
- throw new ChordError2("invalid", `Invalid neko session id: ${sessionId}`);
5401
+ throw new ChordError3("invalid", `Invalid neko session id: ${sessionId}`);
4499
5402
  return numeric;
4500
5403
  }
4501
5404
 
@@ -4792,7 +5695,7 @@ var DEFAULT_HOST = "127.0.0.1";
4792
5695
  var DEFAULT_PORT = 8321;
4793
5696
  var SERVER_ID_FILENAME = "gateway-server-id";
4794
5697
  async function startGateway(options = {}) {
4795
- const context = options.context ?? BACKGROUND_CONTEXT3;
5698
+ const context = options.context ?? BACKGROUND_CONTEXT4;
4796
5699
  const fileConfig = loadNekoConfig(options.configDir);
4797
5700
  const model = options.model ?? fileConfig.defaultModel ?? DEFAULT_MODEL;
4798
5701
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
@@ -4862,7 +5765,7 @@ async function bindGateway(runtime, options) {
4862
5765
  }
4863
5766
  try {
4864
5767
  for (const listener of listeners)
4865
- await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID3() } }));
5768
+ await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID4() } }));
4866
5769
  } catch (error) {
4867
5770
  await Promise.allSettled(listeners.map((listener) => listener.close()));
4868
5771
  await chord.close();
@@ -4932,7 +5835,7 @@ async function loadOrCreateServerId(nekoDir) {
4932
5835
  if (existing !== "")
4933
5836
  return existing;
4934
5837
  } catch {}
4935
- const created = randomUUID3();
5838
+ const created = randomUUID4();
4936
5839
  await mkdir5(nekoDir, { recursive: true, mode: 448 });
4937
5840
  await writeFile4(file, `${created}
4938
5841
  `, { mode: 384 });
@@ -4955,7 +5858,7 @@ if (gateway.socketPath !== undefined)
4955
5858
  console.log(` Unix socket: ${gateway.socketPath}`);
4956
5859
  if (gateway.wsUrl !== undefined)
4957
5860
  console.log(` WebSocket: ${gateway.wsUrl}`);
4958
- console.log("Ctrl-C \u505C\u6B62\u3002");
5861
+ console.log("\u6309 Ctrl+C \u505C\u6B62\u3002");
4959
5862
  var stopping = false;
4960
5863
  var stop = () => {
4961
5864
  if (stopping)