neko-harness 0.3.1 → 0.3.3

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
@@ -434,6 +434,125 @@ async function prepareInput(content, model) {
434
434
  let index = 0;
435
435
  return content.map((b) => b.type === "image" ? prepared[index++] : b);
436
436
  }
437
+ // packages/core/src/jobs.ts
438
+ import { defineDoc } from "@earendil-works/pi-durable";
439
+ var JOB_LIMITS = {
440
+ maxRunning: 4,
441
+ keepTerminal: 20,
442
+ startWindowMs: 2000,
443
+ tailBytes: 64 * 1024,
444
+ tailLines: 1000,
445
+ reportBytes: 4 * 1024,
446
+ reportLines: 40,
447
+ readBytes: 64 * 1024,
448
+ titleChars: 200,
449
+ commandBytes: 32 * 1024,
450
+ maxTimeoutSeconds: 7 * 24 * 3600
451
+ };
452
+ var JobsDoc = defineDoc({
453
+ kind: "app.neko.jobs",
454
+ version: 1,
455
+ scope: "conversation",
456
+ history: "latest",
457
+ fork: "initial",
458
+ initial: () => ({ next: 1, items: [] })
459
+ });
460
+ var summarizeJobRecord = (job, outputBytes = job.totalBytes ?? 0) => {
461
+ const {
462
+ tail: _tail,
463
+ totalBytes: _total,
464
+ runnerId: _runner,
465
+ reporterId: _reporter,
466
+ stopOrigin: _origin,
467
+ ...rest
468
+ } = job;
469
+ return { ...rest, outputBytes };
470
+ };
471
+ var isTerminalJob = (status) => status !== "running";
472
+ var encoder = new TextEncoder;
473
+ var decoder = new TextDecoder;
474
+ var byteLength = (text) => encoder.encode(text).length;
475
+ function tailOf(text, maxBytes, maxLines) {
476
+ let bytes = encoder.encode(text);
477
+ if (bytes.length > maxBytes) {
478
+ let start = bytes.length - maxBytes;
479
+ while (start < bytes.length && (bytes[start] & 192) === 128)
480
+ start++;
481
+ bytes = bytes.subarray(start);
482
+ text = decoder.decode(bytes);
483
+ }
484
+ let index = text.length;
485
+ let seen = 0;
486
+ if (text.endsWith(`
487
+ `))
488
+ index--;
489
+ while (index > 0) {
490
+ const found = text.lastIndexOf(`
491
+ `, index - 1);
492
+ if (found < 0)
493
+ break;
494
+ if (++seen >= maxLines)
495
+ return text.slice(found + 1);
496
+ index = found;
497
+ }
498
+ return text;
499
+ }
500
+
501
+ class OutputBuffer {
502
+ #chunks = [];
503
+ #bytes = 0;
504
+ #total = 0;
505
+ #maxBytes;
506
+ #maxLines;
507
+ constructor(maxBytes = JOB_LIMITS.tailBytes, maxLines = JOB_LIMITS.tailLines) {
508
+ this.#maxBytes = maxBytes;
509
+ this.#maxLines = maxLines;
510
+ }
511
+ static fromTail(tail, totalBytes) {
512
+ const buffer = new OutputBuffer;
513
+ buffer.#chunks = [tail];
514
+ buffer.#bytes = byteLength(tail);
515
+ buffer.#total = totalBytes;
516
+ return buffer;
517
+ }
518
+ get totalBytes() {
519
+ return this.#total;
520
+ }
521
+ append(text) {
522
+ if (!text)
523
+ return;
524
+ const size = byteLength(text);
525
+ this.#total += size;
526
+ this.#bytes += size;
527
+ this.#chunks.push(text);
528
+ if (this.#bytes > this.#maxBytes * 2)
529
+ this.#trim();
530
+ }
531
+ #trim() {
532
+ const text = tailOf(this.#chunks.join(""), this.#maxBytes, this.#maxLines);
533
+ this.#chunks = [text];
534
+ this.#bytes = byteLength(text);
535
+ return text;
536
+ }
537
+ tail() {
538
+ return this.#trim();
539
+ }
540
+ read(since, maxBytes = JOB_LIMITS.readBytes) {
541
+ const tail = this.#trim();
542
+ const start = this.#total - this.#bytes;
543
+ const all = encoder.encode(tail);
544
+ let begin = since === undefined ? start : Math.min(Math.max(since, start), this.#total);
545
+ let bytes = all.subarray(begin - start);
546
+ let cut = Math.max(0, bytes.length - maxBytes);
547
+ while (cut < bytes.length && (bytes[cut] & 192) === 128)
548
+ cut++;
549
+ begin += cut;
550
+ bytes = bytes.subarray(cut);
551
+ const dropped = Math.max(0, begin - (since ?? 0));
552
+ return { text: decoder.decode(bytes), offset: this.#total, truncated: dropped > 0, dropped };
553
+ }
554
+ }
555
+ var reportTail = (text) => tailOf(text, JOB_LIMITS.reportBytes, JOB_LIMITS.reportLines);
437
556
  // packages/core/src/image-prompt.ts
438
557
  var DEFAULT_IMAGE_PROMPT = {
439
558
  version: 2,
@@ -560,6 +679,53 @@ function toolResultText(content) {
560
679
  return content.filter((block) => block.type === "text" && typeof block.text === "string").map((block) => block.text ?? "").join(`
561
680
  `);
562
681
  }
682
+ var WRITE_DIFF_MAX_LINES = 2000;
683
+ var WRITE_DIFF_MAX_BYTES = 64 * 1024;
684
+ function countDiffLines(diff) {
685
+ let added = 0;
686
+ let removed = 0;
687
+ for (const line of diff.split(`
688
+ `)) {
689
+ if (line.startsWith("+") && !line.startsWith("+++"))
690
+ added++;
691
+ else if (line.startsWith("-") && !line.startsWith("---"))
692
+ removed++;
693
+ }
694
+ return { added, removed };
695
+ }
696
+ function editResultDiff(details) {
697
+ if (typeof details !== "object" || details === null)
698
+ return null;
699
+ const diff = details.diff;
700
+ return typeof diff === "string" ? diff : null;
701
+ }
702
+ function synthesizedWriteDiff(args) {
703
+ const record = args ?? {};
704
+ if (typeof record.content !== "string")
705
+ return null;
706
+ const content = record.content;
707
+ const shown = [];
708
+ let offset = 0;
709
+ let bytes = 0;
710
+ while (offset < content.length && shown.length < WRITE_DIFF_MAX_LINES) {
711
+ const newline = content.indexOf(`
712
+ `, offset);
713
+ const end = newline === -1 ? content.length : newline;
714
+ const overhead = shown.length === 0 ? 2 : 3;
715
+ const remaining = WRITE_DIFF_MAX_BYTES - bytes - overhead;
716
+ if (end - offset > remaining)
717
+ break;
718
+ const line = content.slice(offset, end);
719
+ const lineBytes = Buffer.byteLength(line, "utf8");
720
+ if (lineBytes > remaining)
721
+ break;
722
+ shown.push(`+ ${line}`);
723
+ bytes += lineBytes + overhead;
724
+ offset = newline === -1 ? content.length : end + 1;
725
+ }
726
+ return { diff: shown.join(`
727
+ `), added: shown.length, removed: 0, truncated: offset < content.length };
728
+ }
563
729
  function totalUsage(state) {
564
730
  const total = {
565
731
  input: 0,
@@ -592,6 +758,8 @@ function projectChatView(view, modelLabel, options = {}) {
592
758
  if (message.role !== "toolResult")
593
759
  continue;
594
760
  const diagnostics = entry.data?.diagnostics ?? [];
761
+ const diff = message.toolName === "edit" ? editResultDiff(message.details) : null;
762
+ const counts = diff === null ? null : countDiffLines(diff);
595
763
  results.set(message.toolCallId, {
596
764
  type: "tool",
597
765
  key: `t${message.toolCallId}`,
@@ -608,7 +776,10 @@ function projectChatView(view, modelLabel, options = {}) {
608
776
  ].filter(Boolean).join(`
609
777
  `),
610
778
  durationMs: message.durationMs ?? null,
611
- note: null
779
+ note: null,
780
+ diff,
781
+ diffAdded: counts?.added ?? null,
782
+ diffRemoved: counts?.removed ?? null
612
783
  });
613
784
  }
614
785
  }
@@ -639,6 +810,17 @@ function projectChatView(view, modelLabel, options = {}) {
639
810
  const items = [];
640
811
  for (const entry of view.entries) {
641
812
  const message = entry.model?.[0];
813
+ const jobReport = entry.kind === NekoInput.kind ? entry.data?.jobReport : undefined;
814
+ if (message?.role === "user" && jobReport) {
815
+ items.push({
816
+ type: "system",
817
+ key: `e${entry.id}`,
818
+ id: entry.id,
819
+ text: userText(message.content),
820
+ jobId: jobReport.jobId
821
+ });
822
+ continue;
823
+ }
642
824
  if (message?.role === "user") {
643
825
  const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
644
826
  items.push({
@@ -666,6 +848,7 @@ function projectChatView(view, modelLabel, options = {}) {
666
848
  } else {
667
849
  const summary = summarizeToolCall(block.name, block.arguments);
668
850
  const slot = live.tools?.find((s) => s.callId === block.id);
851
+ const synthesized = block.name === "write" ? synthesizedWriteDiff(block.arguments) : null;
669
852
  items.push({
670
853
  type: "tool",
671
854
  key: `t${block.id}`,
@@ -678,7 +861,11 @@ function projectChatView(view, modelLabel, options = {}) {
678
861
  output: slot?.output ?? null,
679
862
  result: null,
680
863
  durationMs: null,
681
- note: ran ? null : "Not run: response interrupted."
864
+ note: ran ? null : "Not run: response interrupted.",
865
+ diff: synthesized?.diff ?? null,
866
+ diffAdded: synthesized?.added ?? null,
867
+ diffRemoved: synthesized?.removed ?? null,
868
+ diffTruncated: synthesized?.truncated ?? false
682
869
  });
683
870
  }
684
871
  }
@@ -726,7 +913,17 @@ function projectChatView(view, modelLabel, options = {}) {
726
913
  }
727
914
  function withArgs(item, name, args) {
728
915
  const summary = summarizeToolCall(name, args);
729
- return { ...item, name, summary: summary.args, detail: summary.detail ?? null };
916
+ const synthesized = name === "write" ? synthesizedWriteDiff(args) : null;
917
+ return {
918
+ ...item,
919
+ name,
920
+ summary: summary.args,
921
+ detail: summary.detail ?? null,
922
+ diff: synthesized?.diff ?? item.diff,
923
+ diffAdded: synthesized?.added ?? item.diffAdded,
924
+ diffRemoved: synthesized?.removed ?? item.diffRemoved,
925
+ diffTruncated: synthesized?.truncated ?? false
926
+ };
730
927
  }
731
928
  function assistantItem(message, id) {
732
929
  let thinking = "";
@@ -779,13 +976,18 @@ ${block.text}`;
779
976
  if (block.type === "toolCall") {
780
977
  const slot = live.tools?.find((candidate) => candidate.callId === block.id);
781
978
  const summary = summarizeToolCall(block.name, block.arguments);
979
+ const synthesized = block.name === "write" ? synthesizedWriteDiff(block.arguments) : null;
782
980
  toolCalls.push({
783
981
  callId: block.id,
784
982
  name: block.name,
785
983
  summary: summary.args,
786
984
  detail: summary.detail ?? null,
787
985
  phase: slot === undefined || slot.status === "pending" ? "pending" : slot.status,
788
- output: slot?.status === "running" ? slot.output ?? null : null
986
+ output: slot?.status === "running" ? slot.output ?? null : null,
987
+ diff: synthesized?.diff ?? null,
988
+ diffAdded: synthesized?.added ?? null,
989
+ diffRemoved: synthesized?.removed ?? null,
990
+ diffTruncated: synthesized?.truncated ?? false
789
991
  });
790
992
  }
791
993
  }
@@ -840,7 +1042,7 @@ import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
840
1042
  import {
841
1043
  AssistantEntry as AssistantEntry2,
842
1044
  createRegistry,
843
- defineDoc as defineDoc4,
1045
+ defineDoc as defineDoc5,
844
1046
  Harness,
845
1047
  LiveDoc as importLiveDoc,
846
1048
  MemoryStorage,
@@ -1079,150 +1281,20 @@ var ImageDetails = defineExtension2({
1079
1281
  ]
1080
1282
  });
1081
1283
 
1082
- // packages/core/src/prompt/invoke.ts
1083
- import { dirname as dirname4 } from "path";
1084
-
1085
- // packages/core/src/prompt/diagnostics.ts
1086
- var ignorePromptDiagnostic = () => {};
1087
- var warnPromptDiagnostic = ({ path, message }) => {
1088
- console.warn(`[neko-prompt] ${JSON.stringify(path)}: ${message}`);
1089
- };
1090
- var isMissing = (error) => typeof error === "object" && error !== null && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
1091
-
1092
- // packages/core/src/prompt/skills.ts
1093
- import { readdir, readFile as readFile2 } from "fs/promises";
1094
- import { basename, dirname as dirname3, join as join3 } from "path";
1095
- var MAX_NAME_LENGTH = 64;
1096
- var MAX_DESCRIPTION_LENGTH = 1024;
1097
- var parseSkillFile = (content, filePath, report = ignorePromptDiagnostic) => {
1098
- const skip = (message) => {
1099
- report({ path: filePath, message });
1100
- return null;
1101
- };
1102
- try {
1103
- const { frontmatter } = parseFrontmatter(content);
1104
- const description = frontmatter.description;
1105
- if (typeof description !== "string" || description.trim() === "")
1106
- return skip("Skill description is missing or empty; skipped.");
1107
- if (description.length > MAX_DESCRIPTION_LENGTH)
1108
- return skip("Skill description is too long; skipped.");
1109
- const name = typeof frontmatter.name === "string" && frontmatter.name.trim() !== "" ? frontmatter.name : basename(dirname3(filePath));
1110
- if (name.length > MAX_NAME_LENGTH || /[\s\p{Cc}]/u.test(name))
1111
- return skip("Skill name is too long or contains whitespace/control characters; skipped.");
1112
- return {
1113
- name,
1114
- description,
1115
- filePath,
1116
- disableModelInvocation: frontmatter["disable-model-invocation"] === true
1117
- };
1118
- } catch {
1119
- return skip("Invalid skill frontmatter; skipped.");
1120
- }
1121
- };
1122
- var scanSkillsDir = async (dir, report = ignorePromptDiagnostic, capture) => {
1123
- let entries;
1124
- try {
1125
- entries = await readdir(dir, { withFileTypes: true });
1126
- } catch (error) {
1127
- if (!isMissing(error))
1128
- report({ path: dir, message: "Cannot scan skills directory; skipped." });
1129
- return [];
1130
- }
1131
- const skills = [];
1132
- for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
1133
- if (!entry.isDirectory())
1134
- continue;
1135
- const filePath = join3(dir, entry.name, "SKILL.md");
1136
- let content;
1137
- try {
1138
- content = await readFile2(filePath, "utf8");
1139
- } catch (error) {
1140
- if (!isMissing(error))
1141
- report({ path: filePath, message: "Cannot read skill; skipped." });
1142
- continue;
1143
- }
1144
- const skill = parseSkillFile(content, filePath, report);
1145
- if (skill) {
1146
- skills.push(skill);
1147
- capture?.(skill, content);
1148
- }
1149
- }
1150
- return skills;
1151
- };
1152
- var mergeSkills = (groups) => {
1153
- const byName = new Map;
1154
- for (const group of groups)
1155
- for (const skill of group)
1156
- byName.set(skill.name, skill);
1157
- return [...byName.values()];
1158
- };
1159
- var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1160
- var formatSkillsForPrompt = (skills) => {
1161
- const visible = skills.filter((skill) => !skill.disableModelInvocation);
1162
- if (visible.length === 0)
1163
- return "";
1164
- const lines = [
1165
- "The following skills provide specialized instructions for specific tasks.",
1166
- "Use the read tool to load a skill's file when the task matches its description.",
1167
- "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.",
1168
- "",
1169
- "<available_skills>"
1170
- ];
1171
- for (const skill of visible) {
1172
- lines.push(" <skill>");
1173
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
1174
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
1175
- lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
1176
- lines.push(" </skill>");
1177
- }
1178
- lines.push("</available_skills>");
1179
- return lines.join(`
1180
- `);
1181
- };
1182
-
1183
- // packages/core/src/prompt/invoke.ts
1184
- var expandSkillCommand = async (text, skills, readFile, report = warnPromptDiagnostic, metadata) => {
1185
- if (!text.startsWith("/skill:"))
1186
- return text;
1187
- const spaceIndex = text.indexOf(" ");
1188
- const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
1189
- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
1190
- const skill = skills.find((s) => s.name === skillName);
1191
- if (!skill)
1192
- return text;
1193
- let body;
1194
- try {
1195
- body = stripFrontmatter(await readFile(skill.filePath)).trim();
1196
- } catch {
1197
- report({ path: skill.filePath, message: "Cannot expand known skill; original input preserved." });
1198
- return text;
1199
- }
1200
- metadata?.({
1201
- name: skill.name,
1202
- location: skill.filePath,
1203
- body: `References are relative to ${dirname4(skill.filePath)}.
1204
-
1205
- ${body}`,
1206
- args
1207
- });
1208
- const block = `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">
1209
- References are relative to ${dirname4(skill.filePath)}.
1210
-
1211
- ${body}
1212
- </skill>`;
1213
- return args ? `${block}
1214
-
1215
- ${args}` : block;
1216
- };
1217
-
1218
- // packages/core/src/prompt/prompt.ts
1219
- import { defineExtension as defineExtension4, section } from "@earendil-works/pi-durable";
1284
+ // packages/core/src/job-engine.ts
1285
+ import { withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
1286
+ import {
1287
+ defineExtension as defineExtension4,
1288
+ defineTask as defineTask2,
1289
+ defineTool as defineTool3
1290
+ } from "@earendil-works/pi-durable";
1291
+ import { Type as Type3 } from "typebox";
1220
1292
 
1221
1293
  // packages/core/src/subagents.ts
1222
1294
  import {
1223
1295
  AssistantEntry,
1224
1296
  configure,
1225
- defineDoc,
1297
+ defineDoc as defineDoc2,
1226
1298
  defineExtension as defineExtension3,
1227
1299
  defineTask,
1228
1300
  defineTool as defineTool2,
@@ -1276,7 +1348,7 @@ function subagentUsageTotals(state) {
1276
1348
 
1277
1349
  // packages/core/src/subagents.ts
1278
1350
  var agentIdFor = (childId) => `agent-${childId}`;
1279
- var DelegationsDoc = defineDoc({
1351
+ var DelegationsDoc = defineDoc2({
1280
1352
  kind: "app.neko.delegations",
1281
1353
  version: 1,
1282
1354
  scope: "conversation",
@@ -1284,7 +1356,7 @@ var DelegationsDoc = defineDoc({
1284
1356
  fork: "initial",
1285
1357
  initial: () => ({ items: [] })
1286
1358
  });
1287
- var SubagentsDoc = defineDoc({
1359
+ var SubagentsDoc = defineDoc2({
1288
1360
  kind: "app.neko.subagents",
1289
1361
  version: 1,
1290
1362
  scope: "conversation",
@@ -1292,7 +1364,7 @@ var SubagentsDoc = defineDoc({
1292
1364
  fork: "initial",
1293
1365
  initial: () => ({ items: [] })
1294
1366
  });
1295
- var SubagentDoc = defineDoc({
1367
+ var SubagentDoc = defineDoc2({
1296
1368
  kind: "app.neko.subagent",
1297
1369
  version: 1,
1298
1370
  scope: "conversation",
@@ -1300,7 +1372,7 @@ var SubagentDoc = defineDoc({
1300
1372
  fork: "initial",
1301
1373
  initial: () => ({ role: null, parentId: null })
1302
1374
  });
1303
- var SlotsDoc = defineDoc({
1375
+ var SlotsDoc = defineDoc2({
1304
1376
  kind: "app.neko.subagent-slots",
1305
1377
  version: 1,
1306
1378
  scope: "session",
@@ -1631,7 +1703,7 @@ function createSubagentExtension(prepare, discard) {
1631
1703
  thinkingLevel: agent.thinkingLevel,
1632
1704
  cwd: agent.cwd,
1633
1705
  extensions: agent.extensions.filter((e) => e.name !== extension.name),
1634
- tools: agent.tools.filter((t) => t.name !== "subagent" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
1706
+ tools: agent.tools.filter((t) => t.name !== "subagent" && t.name !== "job" && (role === "coder" || READ_TOOLS.has(t.name) || role === "explore" && t.name === "bash"))
1635
1707
  });
1636
1708
  Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
1637
1709
  identity = {
@@ -1772,7 +1844,567 @@ async function installSubagentCoordinator(harness, parents, context) {
1772
1844
  harness.closed.finally(stop).catch(() => {});
1773
1845
  }
1774
1846
 
1847
+ // packages/core/src/job-engine.ts
1848
+ var plain = (value) => JSON.parse(JSON.stringify(value));
1849
+
1850
+ class JobEntry {
1851
+ controller = new AbortController;
1852
+ buffer = new OutputBuffer;
1853
+ windowOpen = false;
1854
+ suppressReport = false;
1855
+ exited;
1856
+ #resolve;
1857
+ constructor() {
1858
+ this.exited = new Promise((resolve) => {
1859
+ this.#resolve = resolve;
1860
+ });
1861
+ }
1862
+ finish() {
1863
+ this.#resolve();
1864
+ }
1865
+ }
1866
+
1867
+ class JobRegistry {
1868
+ #entries = new Map;
1869
+ static key(conversationId, jobId) {
1870
+ return `${conversationId}:${jobId}`;
1871
+ }
1872
+ ensure(conversationId, jobId) {
1873
+ const key = JobRegistry.key(conversationId, jobId);
1874
+ let entry = this.#entries.get(key);
1875
+ if (!entry) {
1876
+ entry = new JobEntry;
1877
+ this.#entries.set(key, entry);
1878
+ }
1879
+ return entry;
1880
+ }
1881
+ get(conversationId, jobId) {
1882
+ return this.#entries.get(JobRegistry.key(conversationId, jobId));
1883
+ }
1884
+ delete(conversationId, jobId) {
1885
+ this.#entries.delete(JobRegistry.key(conversationId, jobId));
1886
+ }
1887
+ abortAll() {
1888
+ for (const entry of this.#entries.values())
1889
+ entry.controller.abort();
1890
+ }
1891
+ abortConversation(conversationId) {
1892
+ for (const [key, entry] of this.#entries)
1893
+ if (key.startsWith(`${conversationId}:`))
1894
+ entry.controller.abort();
1895
+ }
1896
+ }
1897
+ var statusWord = {
1898
+ running: "running",
1899
+ exited: "exited",
1900
+ failed: "failed",
1901
+ stopped: "stopped",
1902
+ lost: "lost"
1903
+ };
1904
+ var describe = (job) => {
1905
+ const parts = [statusWord[job.status]];
1906
+ if (job.exitCode !== null)
1907
+ parts.push(`exit code ${job.exitCode}`);
1908
+ if (job.reason === "timeout")
1909
+ parts.push(`timed out after ${job.timeoutSeconds} seconds`);
1910
+ if (job.reason === "spawn-error")
1911
+ parts.push("could not start");
1912
+ if (job.reason === "restart")
1913
+ parts.push("the process was lost when the gateway restarted and was not re-run");
1914
+ return parts.join(", ");
1915
+ };
1916
+ function reportText(job, tail) {
1917
+ const body = reportTail(tail);
1918
+ return [
1919
+ `[Background job ${job.jobId} "${job.title}" ${describe(job)}]`,
1920
+ `Command: ${job.command}`,
1921
+ body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
1922
+ ${body}` : "It produced no output."
1923
+ ].join(`
1924
+ `);
1925
+ }
1926
+ async function settleJob(tx, conversationId, jobId, outcome, tail, quiet, reporter) {
1927
+ const state = await tx.doc(JobsDoc, conversationId);
1928
+ const job = state.items.find((item) => item.jobId === jobId);
1929
+ if (!job || isTerminalJob(job.status))
1930
+ return;
1931
+ job.status = outcome.status;
1932
+ job.exitCode = outcome.exitCode;
1933
+ if (outcome.reason)
1934
+ job.reason = outcome.reason;
1935
+ job.endedAt = Date.now();
1936
+ job.tail = tail.text;
1937
+ job.totalBytes = tail.totalBytes;
1938
+ if (!quiet && (job.status !== "stopped" || job.stopOrigin === "user"))
1939
+ job.reporterId = await tx.createTask(reporter, { jobId, text: reportText(job, tail.text), status: job.status, exitCode: job.exitCode }, { ownership: { kind: "conversation" }, conversationId, background: true });
1940
+ const terminal = state.items.filter((item) => isTerminalJob(item.status));
1941
+ if (terminal.length > JOB_LIMITS.keepTerminal) {
1942
+ const drop = new Set(terminal.sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0)).slice(0, terminal.length - JOB_LIMITS.keepTerminal).map((item) => item.jobId));
1943
+ state.items = state.items.filter((item) => !drop.has(item.jobId));
1944
+ }
1945
+ return job;
1946
+ }
1947
+ function createReporter() {
1948
+ return defineTask2({
1949
+ name: "app.neko.job-reporter",
1950
+ version: 1,
1951
+ initial: () => ({ phase: "report" }),
1952
+ phases: {
1953
+ report: async (task, runtime, ctx) => {
1954
+ const target = await runtime.conversation(runtime.conversationId, ctx);
1955
+ await target.submit({
1956
+ type: "input",
1957
+ content: task.input.text,
1958
+ whenBusy: "followUp",
1959
+ requestId: `job-report:${task.input.jobId}`,
1960
+ entry: {
1961
+ kind: NekoInput.kind,
1962
+ data: {
1963
+ at: Date.now(),
1964
+ prompt: task.input.text,
1965
+ images: [],
1966
+ jobReport: { jobId: task.input.jobId, status: task.input.status, exitCode: task.input.exitCode }
1967
+ }
1968
+ }
1969
+ }, ctx);
1970
+ await runtime.commit(() => ({ status: "terminal", outcome: { status: "completed", result: null } }), ctx);
1971
+ }
1972
+ },
1973
+ abort: (_task, runtime, ctx) => runtime.commit(() => ({ status: "terminal", outcome: { status: "aborted" } }), ctx)
1974
+ });
1975
+ }
1976
+ var delay = (ms, signal) => new Promise((resolve) => {
1977
+ const timer = setTimeout(done, ms);
1978
+ function done() {
1979
+ clearTimeout(timer);
1980
+ signal?.removeEventListener("abort", done);
1981
+ resolve();
1982
+ }
1983
+ if (signal?.aborted)
1984
+ done();
1985
+ else
1986
+ signal?.addEventListener("abort", done, { once: true });
1987
+ });
1988
+ var action = Type3.Union([Type3.Literal("start"), Type3.Literal("output"), Type3.Literal("stop"), Type3.Literal("list")]);
1989
+ var parameters2 = Type3.Object({
1990
+ action,
1991
+ command: Type3.Optional(Type3.String({ description: "start: shell command to run in the session's working directory." })),
1992
+ title: Type3.Optional(Type3.String({ description: "start: short label shown in the job list." })),
1993
+ timeout: Type3.Optional(Type3.Number({ description: "start: optional timeout in seconds; no default timeout." })),
1994
+ jobId: Type3.Optional(Type3.String({ description: "output/stop: the job id returned by start or list." })),
1995
+ since: Type3.Optional(Type3.Integer({ description: "output: resume after this offset, as returned by the previous output call." }))
1996
+ });
1997
+ var stringField = (value, name) => {
1998
+ if (typeof value !== "string" || !value.trim())
1999
+ throw new Error(`Invalid job arguments: ${name} must be a nonempty string.`);
2000
+ return value;
2001
+ };
2002
+
2003
+ class JobManager {
2004
+ registry = new JobRegistry;
2005
+ #reporter = createReporter();
2006
+ #runner = this.#createRunner();
2007
+ extension;
2008
+ constructor() {
2009
+ this.extension = this.#createExtension();
2010
+ }
2011
+ #closing = false;
2012
+ #createRunner() {
2013
+ const registry = this.registry;
2014
+ const reporter = this.#reporter;
2015
+ return defineTask2({
2016
+ name: "app.neko.job-runner",
2017
+ version: 1,
2018
+ initial: () => ({ phase: "start" }),
2019
+ phases: {
2020
+ start: (_task, runtime, ctx) => runtime.commit(() => ({ status: "running", checkpoint: { phase: "exec" } }), ctx),
2021
+ exec: async (task, runtime, ctx) => {
2022
+ const { jobId, command, cwd, timeoutSeconds } = task.input;
2023
+ const entry = registry.ensure(runtime.conversationId, jobId);
2024
+ let outcome;
2025
+ try {
2026
+ const env = await runtime.env(ctx);
2027
+ if (!env) {
2028
+ outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
2029
+ } else {
2030
+ const result = await env.exec(command, {
2031
+ ...cwd ? { cwd } : {},
2032
+ ...timeoutSeconds === undefined ? {} : { timeout: timeoutSeconds },
2033
+ onOutput: (text) => entry.buffer.append(text)
2034
+ }, withAbortSignal2(entry.controller.signal, ctx));
2035
+ if (result.ok)
2036
+ outcome = {
2037
+ status: result.value.exitCode === 0 ? "exited" : "failed",
2038
+ exitCode: result.value.exitCode
2039
+ };
2040
+ else if (result.error.code === "timeout")
2041
+ outcome = { status: "failed", exitCode: null, reason: "timeout" };
2042
+ else if (result.error.code === "aborted")
2043
+ outcome = { status: "stopped", exitCode: null, reason: "aborted" };
2044
+ else
2045
+ outcome = { status: "failed", exitCode: null, reason: "spawn-error" };
2046
+ }
2047
+ if (this.#closing) {
2048
+ await new Promise((resolve) => {
2049
+ if (!ctx.abortSignal || ctx.abortSignal.aborted)
2050
+ resolve();
2051
+ else
2052
+ ctx.abortSignal.addEventListener("abort", () => resolve(), { once: true });
2053
+ });
2054
+ throw ctx.abortSignal?.reason ?? new Error("Job runner interrupted by shutdown");
2055
+ }
2056
+ const quiet = entry.windowOpen;
2057
+ if (quiet)
2058
+ entry.suppressReport = true;
2059
+ const tail = { text: entry.buffer.tail(), totalBytes: entry.buffer.totalBytes };
2060
+ await runtime.commit(async (tx) => {
2061
+ const job = await settleJob(tx, runtime.conversationId, jobId, outcome, tail, quiet, reporter);
2062
+ return {
2063
+ status: "terminal",
2064
+ outcome: { status: "completed", result: { status: job?.status ?? outcome.status } }
2065
+ };
2066
+ }, ctx);
2067
+ } finally {
2068
+ registry.delete(runtime.conversationId, jobId);
2069
+ entry.finish();
2070
+ }
2071
+ }
2072
+ },
2073
+ abort: async (task, runtime, ctx) => {
2074
+ const { jobId } = task.input;
2075
+ const entry = registry.get(runtime.conversationId, jobId);
2076
+ entry?.controller.abort();
2077
+ const restart = task.abortReason === "restart";
2078
+ const outcome = restart ? { status: "lost", exitCode: null, reason: "restart" } : { status: "stopped", exitCode: null, reason: "aborted" };
2079
+ try {
2080
+ await runtime.commit(async (tx) => {
2081
+ const known = (await tx.doc(JobsDoc, runtime.conversationId)).items.find((item) => item.jobId === jobId);
2082
+ const tail = entry ? { text: entry.buffer.tail(), totalBytes: entry.buffer.totalBytes } : { text: known?.tail ?? "", totalBytes: known?.totalBytes ?? 0 };
2083
+ await settleJob(tx, runtime.conversationId, jobId, outcome, tail, false, reporter);
2084
+ return { status: "terminal", outcome: { status: "aborted" } };
2085
+ }, ctx);
2086
+ } finally {
2087
+ registry.delete(runtime.conversationId, jobId);
2088
+ entry?.finish();
2089
+ }
2090
+ }
2091
+ });
2092
+ }
2093
+ #createExtension() {
2094
+ return defineExtension4({
2095
+ name: "neko-jobs",
2096
+ tasks: [this.#runner, this.#reporter],
2097
+ tools: [
2098
+ defineTool3({
2099
+ name: "job",
2100
+ description: "Run long-lived commands (dev servers, watchers, long builds) in the background. start: command (optional title, timeout seconds) returns a jobId immediately; the command has no stdin. output: jobId (optional since offset) reads the output tail or what is new. stop: jobId terminates the whole process tree. list: show this session's jobs. When a job ends you receive a report; do not poll. At most 4 jobs run at once.",
2101
+ parameters: parameters2,
2102
+ replay: "unsafe",
2103
+ execute: async (input, api, context) => {
2104
+ const args = input;
2105
+ if ((await api.snapshot(SubagentDoc, api.conversationId, context))?.role)
2106
+ throw new Error("Subagents cannot use jobs.");
2107
+ const io = {
2108
+ commit: (change) => api.commit(change, context),
2109
+ state: async () => api.snapshot(JobsDoc, api.conversationId, context)
2110
+ };
2111
+ const kind = args.action;
2112
+ if (kind === "list")
2113
+ return this.#text(this.#listText((await io.state())?.items ?? []));
2114
+ if (kind === "output") {
2115
+ const jobId = stringField(args.jobId, "jobId");
2116
+ const since = args.since;
2117
+ if (since !== undefined && (!Number.isInteger(since) || since < 0))
2118
+ throw new Error("Invalid job arguments: since must be a non-negative integer.");
2119
+ return this.#text(await this.output(io, api.conversationId, jobId, since));
2120
+ }
2121
+ if (kind === "stop") {
2122
+ const jobId = stringField(args.jobId, "jobId");
2123
+ const job = await this.stop(io, api.conversationId, jobId, "model", context);
2124
+ return this.#text(`Job ${job.jobId} ${describe(job)}.`);
2125
+ }
2126
+ if (kind !== "start")
2127
+ throw new Error("Invalid job arguments: action must be start, output, stop or list.");
2128
+ const command = stringField(args.command, "command");
2129
+ if (Buffer.byteLength(command) > JOB_LIMITS.commandBytes)
2130
+ throw new Error("Invalid job arguments: command is too long.");
2131
+ const timeout = args.timeout;
2132
+ if (timeout !== undefined && (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0 || timeout > JOB_LIMITS.maxTimeoutSeconds))
2133
+ throw new Error("Invalid job arguments: timeout must be a positive number of seconds.");
2134
+ const title = typeof args.title === "string" && args.title.trim() ? args.title.trim() : command;
2135
+ const agent = await api.agent(context);
2136
+ return this.#text(await this.start(io, api.conversationId, agent.cwd ?? "", {
2137
+ command,
2138
+ title: title.slice(0, JOB_LIMITS.titleChars),
2139
+ ...timeout === undefined ? {} : { timeoutSeconds: timeout }
2140
+ }, context));
2141
+ }
2142
+ })
2143
+ ]
2144
+ });
2145
+ }
2146
+ #text(text) {
2147
+ return { output: [{ type: "text", text }] };
2148
+ }
2149
+ #listText(items) {
2150
+ if (!items.length)
2151
+ return "No jobs.";
2152
+ return items.map((job) => `${job.jobId} [${describe(job)}] "${job.title}" \u2014 ${job.command}`).join(`
2153
+ `);
2154
+ }
2155
+ async start(io, conversationId, cwd, args, context) {
2156
+ let allocated;
2157
+ let entry;
2158
+ let created;
2159
+ try {
2160
+ created = await io.commit(async (tx) => {
2161
+ const state = await tx.doc(JobsDoc, conversationId);
2162
+ if (state.items.filter((item) => item.status === "running").length >= JOB_LIMITS.maxRunning)
2163
+ throw new Error(`Too many running jobs (limit ${JOB_LIMITS.maxRunning}). Stop one with the job tool's stop action first.`);
2164
+ const jobId = `job-${state.next++}`;
2165
+ const record = {
2166
+ jobId,
2167
+ title: args.title,
2168
+ command: args.command,
2169
+ cwd,
2170
+ status: "running",
2171
+ startedAt: Date.now(),
2172
+ endedAt: null,
2173
+ exitCode: null,
2174
+ ...args.timeoutSeconds === undefined ? {} : { timeoutSeconds: args.timeoutSeconds },
2175
+ runnerId: 0
2176
+ };
2177
+ allocated = jobId;
2178
+ entry = this.registry.ensure(conversationId, jobId);
2179
+ entry.windowOpen = true;
2180
+ record.runnerId = await tx.createTask(this.#runner, {
2181
+ jobId,
2182
+ command: args.command,
2183
+ cwd,
2184
+ ...args.timeoutSeconds === undefined ? {} : { timeoutSeconds: args.timeoutSeconds }
2185
+ }, { ownership: { kind: "conversation" }, conversationId, background: true, abandonOnRestart: true });
2186
+ state.items.push(record);
2187
+ return plain(record);
2188
+ });
2189
+ } catch (error) {
2190
+ if (allocated)
2191
+ this.registry.delete(conversationId, allocated);
2192
+ throw error;
2193
+ }
2194
+ const live = entry;
2195
+ await Promise.race([live.exited, delay(JOB_LIMITS.startWindowMs, context.abortSignal)]);
2196
+ live.windowOpen = false;
2197
+ if (live.suppressReport)
2198
+ await live.exited;
2199
+ const job = (await io.state())?.items.find((item) => item.jobId === created.jobId) ?? created;
2200
+ const read = live.buffer.read();
2201
+ return this.#render(job, read.text, read.offset, read.dropped);
2202
+ }
2203
+ #render(job, text, offset, dropped) {
2204
+ const lines = [`Job ${job.jobId} ${describe(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
2205
+ if (dropped > 0)
2206
+ lines.push(`(${dropped} bytes of earlier output were dropped)`);
2207
+ lines.push(text ? `Output (next offset ${offset}):
2208
+ ${text}` : `No new output (next offset ${offset}).`);
2209
+ return lines.join(`
2210
+ `);
2211
+ }
2212
+ async output(io, conversationId, jobId, since) {
2213
+ const job = await this.#find(io, jobId);
2214
+ const output = this.read(conversationId, job, since);
2215
+ return this.#render(job, output.text, output.offset, output.dropped);
2216
+ }
2217
+ async readOutput(io, conversationId, jobId, since) {
2218
+ const job = await this.#find(io, jobId);
2219
+ const output = this.read(conversationId, job, since);
2220
+ return { job: summarizeJobRecord(job, output.offset), output };
2221
+ }
2222
+ async#find(io, jobId) {
2223
+ const job = (await io.state())?.items.find((item) => item.jobId === jobId);
2224
+ if (!job)
2225
+ throw new Error(`Unknown job ${jobId}.`);
2226
+ return job;
2227
+ }
2228
+ async list(io, conversationId) {
2229
+ return ((await io.state())?.items ?? []).map((job) => summarizeJobRecord(job, this.registry.get(conversationId, job.jobId)?.buffer.totalBytes ?? job.totalBytes ?? 0));
2230
+ }
2231
+ read(conversationId, job, since) {
2232
+ const entry = isTerminalJob(job.status) ? undefined : this.registry.get(conversationId, job.jobId);
2233
+ return (entry?.buffer ?? OutputBuffer.fromTail(job.tail ?? "", job.totalBytes ?? 0)).read(since);
2234
+ }
2235
+ async stop(io, conversationId, jobId, origin, context) {
2236
+ let live;
2237
+ const found = await io.commit(async (tx) => {
2238
+ live = undefined;
2239
+ const job = (await tx.doc(JobsDoc, conversationId)).items.find((item) => item.jobId === jobId);
2240
+ if (!job)
2241
+ throw new Error(`Unknown job ${jobId}.`);
2242
+ if (isTerminalJob(job.status))
2243
+ return { job: plain(job) };
2244
+ job.stopOrigin ??= origin;
2245
+ live = this.registry.get(conversationId, jobId);
2246
+ if (live)
2247
+ return { job: plain(job) };
2248
+ const settled = await settleJob(tx, conversationId, jobId, { status: "stopped", exitCode: null }, { text: job.tail ?? "", totalBytes: job.totalBytes ?? 0 }, false, this.#reporter);
2249
+ return { job: plain(settled ?? job) };
2250
+ });
2251
+ const entry = live;
2252
+ if (entry) {
2253
+ entry.controller.abort();
2254
+ await Promise.race([entry.exited, delay(1e4, context?.abortSignal)]);
2255
+ return (await io.state())?.items.find((item) => item.jobId === jobId) ?? found.job;
2256
+ }
2257
+ return found.job;
2258
+ }
2259
+ async stopAll(io, conversationId, origin = "archive") {
2260
+ const running = ((await io.state())?.items ?? []).filter((item) => item.status === "running");
2261
+ for (const job of running)
2262
+ await this.stop(io, conversationId, job.jobId, origin);
2263
+ }
2264
+ shutdown() {
2265
+ this.#closing = true;
2266
+ this.registry.abortAll();
2267
+ }
2268
+ }
2269
+
2270
+ // packages/core/src/prompt/invoke.ts
2271
+ import { dirname as dirname4 } from "path";
2272
+
2273
+ // packages/core/src/prompt/diagnostics.ts
2274
+ var ignorePromptDiagnostic = () => {};
2275
+ var warnPromptDiagnostic = ({ path, message }) => {
2276
+ console.warn(`[neko-prompt] ${JSON.stringify(path)}: ${message}`);
2277
+ };
2278
+ var isMissing = (error) => typeof error === "object" && error !== null && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
2279
+
2280
+ // packages/core/src/prompt/skills.ts
2281
+ import { readdir, readFile as readFile2 } from "fs/promises";
2282
+ import { basename, dirname as dirname3, join as join3 } from "path";
2283
+ var MAX_NAME_LENGTH = 64;
2284
+ var MAX_DESCRIPTION_LENGTH = 1024;
2285
+ var parseSkillFile = (content, filePath, report = ignorePromptDiagnostic) => {
2286
+ const skip = (message) => {
2287
+ report({ path: filePath, message });
2288
+ return null;
2289
+ };
2290
+ try {
2291
+ const { frontmatter } = parseFrontmatter(content);
2292
+ const description = frontmatter.description;
2293
+ if (typeof description !== "string" || description.trim() === "")
2294
+ return skip("Skill description is missing or empty; skipped.");
2295
+ if (description.length > MAX_DESCRIPTION_LENGTH)
2296
+ return skip("Skill description is too long; skipped.");
2297
+ const name = typeof frontmatter.name === "string" && frontmatter.name.trim() !== "" ? frontmatter.name : basename(dirname3(filePath));
2298
+ if (name.length > MAX_NAME_LENGTH || /[\s\p{Cc}]/u.test(name))
2299
+ return skip("Skill name is too long or contains whitespace/control characters; skipped.");
2300
+ return {
2301
+ name,
2302
+ description,
2303
+ filePath,
2304
+ disableModelInvocation: frontmatter["disable-model-invocation"] === true
2305
+ };
2306
+ } catch {
2307
+ return skip("Invalid skill frontmatter; skipped.");
2308
+ }
2309
+ };
2310
+ var scanSkillsDir = async (dir, report = ignorePromptDiagnostic, capture) => {
2311
+ let entries;
2312
+ try {
2313
+ entries = await readdir(dir, { withFileTypes: true });
2314
+ } catch (error) {
2315
+ if (!isMissing(error))
2316
+ report({ path: dir, message: "Cannot scan skills directory; skipped." });
2317
+ return [];
2318
+ }
2319
+ const skills = [];
2320
+ for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
2321
+ if (!entry.isDirectory())
2322
+ continue;
2323
+ const filePath = join3(dir, entry.name, "SKILL.md");
2324
+ let content;
2325
+ try {
2326
+ content = await readFile2(filePath, "utf8");
2327
+ } catch (error) {
2328
+ if (!isMissing(error))
2329
+ report({ path: filePath, message: "Cannot read skill; skipped." });
2330
+ continue;
2331
+ }
2332
+ const skill = parseSkillFile(content, filePath, report);
2333
+ if (skill) {
2334
+ skills.push(skill);
2335
+ capture?.(skill, content);
2336
+ }
2337
+ }
2338
+ return skills;
2339
+ };
2340
+ var mergeSkills = (groups) => {
2341
+ const byName = new Map;
2342
+ for (const group of groups)
2343
+ for (const skill of group)
2344
+ byName.set(skill.name, skill);
2345
+ return [...byName.values()];
2346
+ };
2347
+ var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
2348
+ var formatSkillsForPrompt = (skills) => {
2349
+ const visible = skills.filter((skill) => !skill.disableModelInvocation);
2350
+ if (visible.length === 0)
2351
+ return "";
2352
+ const lines = [
2353
+ "The following skills provide specialized instructions for specific tasks.",
2354
+ "Use the read tool to load a skill's file when the task matches its description.",
2355
+ "When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md) and use that absolute path in tool commands.",
2356
+ "",
2357
+ "<available_skills>"
2358
+ ];
2359
+ for (const skill of visible) {
2360
+ lines.push(" <skill>");
2361
+ lines.push(` <name>${escapeXml(skill.name)}</name>`);
2362
+ lines.push(` <description>${escapeXml(skill.description)}</description>`);
2363
+ lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
2364
+ lines.push(" </skill>");
2365
+ }
2366
+ lines.push("</available_skills>");
2367
+ return lines.join(`
2368
+ `);
2369
+ };
2370
+
2371
+ // packages/core/src/prompt/invoke.ts
2372
+ var expandSkillCommand = async (text, skills, readFile, report = warnPromptDiagnostic, metadata) => {
2373
+ if (!text.startsWith("/skill:"))
2374
+ return text;
2375
+ const spaceIndex = text.indexOf(" ");
2376
+ const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
2377
+ const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
2378
+ const skill = skills.find((s) => s.name === skillName);
2379
+ if (!skill)
2380
+ return text;
2381
+ let body;
2382
+ try {
2383
+ body = stripFrontmatter(await readFile(skill.filePath)).trim();
2384
+ } catch {
2385
+ report({ path: skill.filePath, message: "Cannot expand known skill; original input preserved." });
2386
+ return text;
2387
+ }
2388
+ metadata?.({
2389
+ name: skill.name,
2390
+ location: skill.filePath,
2391
+ body: `References are relative to ${dirname4(skill.filePath)}.
2392
+
2393
+ ${body}`,
2394
+ args
2395
+ });
2396
+ const block = `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">
2397
+ References are relative to ${dirname4(skill.filePath)}.
2398
+
2399
+ ${body}
2400
+ </skill>`;
2401
+ return args ? `${block}
2402
+
2403
+ ${args}` : block;
2404
+ };
2405
+
1775
2406
  // packages/core/src/prompt/prompt.ts
2407
+ import { defineExtension as defineExtension5, section } from "@earendil-works/pi-durable";
1776
2408
  var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
1777
2409
  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.";
1778
2410
  var TOOL_GUIDELINES = {
@@ -1794,6 +2426,9 @@ var TOOL_GUIDELINES = {
1794
2426
  "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions."
1795
2427
  ],
1796
2428
  bash: ["Use bash for file operations like ls, rg, find"],
2429
+ job: [
2430
+ 'Use job {action:"start", command, title?, timeout?} for long-running commands (dev servers, watchers, long builds or tests) instead of bash, which would block. Jobs have no stdin. start returns within 2 seconds, with the early result if the command already ended; a job that ends later reports back to you automatically, so do not poll. Read new output with {action:"output", jobId, since?} using the offset from the previous call, list jobs with {action:"list"}, end one with {action:"stop", jobId}. Run short commands with bash.'
2431
+ ],
1797
2432
  web_search: ["Use web_search for current information from the internet instead of guessing."],
1798
2433
  web_extract: ["Use web_extract to read the full content of a web page when you have its URL."]
1799
2434
  };
@@ -1833,7 +2468,7 @@ var createNekoPrompt = (loadResources) => {
1833
2468
  };
1834
2469
  return sections;
1835
2470
  };
1836
- return defineExtension4({
2471
+ return defineExtension5({
1837
2472
  name: "neko-prompt",
1838
2473
  sections: KEYS.map((key) => section(key, async (input, context) => {
1839
2474
  let pending = built.get(input);
@@ -1985,13 +2620,13 @@ var createResourceSnapshots = (load, submission) => {
1985
2620
  import { stripVTControlCharacters } from "util";
1986
2621
  import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
1987
2622
  import {
1988
- defineDoc as defineDoc2,
1989
- defineExtension as defineExtension5,
1990
- defineTask as defineTask2,
2623
+ defineDoc as defineDoc3,
2624
+ defineExtension as defineExtension6,
2625
+ defineTask as defineTask3,
1991
2626
  InboxDoc,
1992
2627
  LiveDoc as LiveDoc2
1993
2628
  } from "@earendil-works/pi-durable";
1994
- var SessionMetadataDoc = defineDoc2({
2629
+ var SessionMetadataDoc = defineDoc3({
1995
2630
  kind: "app.neko.session-metadata",
1996
2631
  version: 1,
1997
2632
  scope: "conversation",
@@ -2054,7 +2689,7 @@ function mergeUsage(base, extra) {
2054
2689
  function namingTask() {
2055
2690
  let active = 0;
2056
2691
  const waits = new Set;
2057
- return defineTask2({
2692
+ return defineTask3({
2058
2693
  name: "app.neko.name",
2059
2694
  version: 1,
2060
2695
  initial: () => ({ phase: "wait", until: Date.now() + 100 }),
@@ -2178,7 +2813,7 @@ class SessionMetadataManager {
2178
2813
  this.storage = storage;
2179
2814
  this.context = context;
2180
2815
  this.report = report;
2181
- registry.install(defineExtension5({ name: "neko-naming", tasks: [this.task] }));
2816
+ registry.install(defineExtension6({ name: "neko-naming", tasks: [this.task] }));
2182
2817
  this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
2183
2818
  const ids = new Set;
2184
2819
  const submissions = new Map;
@@ -2322,14 +2957,14 @@ import { randomUUID } from "crypto";
2322
2957
  import { realpath } from "fs/promises";
2323
2958
  import {
2324
2959
  configure as configure2,
2325
- defineDoc as defineDoc3,
2326
- defineExtension as defineExtension6,
2960
+ defineDoc as defineDoc4,
2961
+ defineExtension as defineExtension7,
2327
2962
  hook as hook2,
2328
2963
  ToolTask as ToolTask2,
2329
2964
  wrapTool
2330
2965
  } from "@earendil-works/pi-durable";
2331
2966
  import { createBashTool } from "@earendil-works/pi-durable/tools";
2332
- var ApprovalsDoc = defineDoc3({
2967
+ var ApprovalsDoc = defineDoc4({
2333
2968
  kind: "app.neko.approvals",
2334
2969
  version: 1,
2335
2970
  scope: "conversation",
@@ -2437,7 +3072,7 @@ class ToolApprovals {
2437
3072
  }
2438
3073
  }
2439
3074
  function createApprovalExtension(service) {
2440
- return defineExtension6({
3075
+ return defineExtension7({
2441
3076
  name: "neko-tool-approvals",
2442
3077
  hooks: [
2443
3078
  hook2(ToolTask2, {
@@ -2501,8 +3136,8 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
2501
3136
  }
2502
3137
 
2503
3138
  // packages/core/src/web-search.ts
2504
- import { Type as Type3 } from "@earendil-works/pi-ai";
2505
- import { defineExtension as defineExtension7, defineTool as defineTool3 } from "@earendil-works/pi-durable";
3139
+ import { Type as Type4 } from "@earendil-works/pi-ai";
3140
+ import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
2506
3141
  var SEARCH_URL = "https://api.tavily.com/search";
2507
3142
  var EXTRACT_URL = "https://api.tavily.com/extract";
2508
3143
  var REQUEST_TIMEOUT_MS = 30000;
@@ -2620,15 +3255,15 @@ function urlKey(value) {
2620
3255
  }
2621
3256
  }
2622
3257
  function createWebSearchTools(resolveKey, fetchImpl = fetch) {
2623
- return defineExtension7({
3258
+ return defineExtension8({
2624
3259
  name: "neko-web-search",
2625
3260
  tools: [
2626
- defineTool3({
3261
+ defineTool4({
2627
3262
  name: "web_search",
2628
3263
  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.",
2629
- parameters: Type3.Object({
2630
- query: Type3.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
2631
- maxResults: Type3.Optional(Type3.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
3264
+ parameters: Type4.Object({
3265
+ query: Type4.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
3266
+ maxResults: Type4.Optional(Type4.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
2632
3267
  }),
2633
3268
  async execute({ query, maxResults }, _api, context) {
2634
3269
  try {
@@ -2653,11 +3288,11 @@ ${truncate(String(item.url ?? ""), 300)}
2653
3288
  }
2654
3289
  }
2655
3290
  }),
2656
- defineTool3({
3291
+ defineTool4({
2657
3292
  name: "web_extract",
2658
3293
  description: "Extract bounded markdown excerpts from web pages by URL. Every requested URL has a result or failure entry; long content is truncated.",
2659
- parameters: Type3.Object({
2660
- urls: Type3.Array(Type3.String({ maxLength: 2048, description: "http(s) URL" }), {
3294
+ parameters: Type4.Object({
3295
+ urls: Type4.Array(Type4.String({ maxLength: 2048, description: "http(s) URL" }), {
2661
3296
  minItems: 1,
2662
3297
  maxItems: MAX_URLS,
2663
3298
  description: `URLs to extract (1-${MAX_URLS})`
@@ -2704,7 +3339,7 @@ ${truncate(String(item.url ?? ""), 300)}
2704
3339
  // packages/core/src/workspace-files.ts
2705
3340
  import { realpath as realpath2, stat } from "fs/promises";
2706
3341
  import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
2707
- import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal2 } from "@earendil-works/chord/context";
3342
+ import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT2, withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
2708
3343
  import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
2709
3344
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
2710
3345
  function relativeQuery(query) {
@@ -2870,7 +3505,7 @@ class WorkspaceFiles {
2870
3505
  for (const user of [...current.users])
2871
3506
  this.#closeWatcher(user);
2872
3507
  }
2873
- }, withAbortSignal2(current.controller.signal, BACKGROUND_CONTEXT2));
3508
+ }, withAbortSignal3(current.controller.signal, BACKGROUND_CONTEXT2));
2874
3509
  if (result.ok) {
2875
3510
  current.watcher = result.value;
2876
3511
  if (current.controller.signal.aborted)
@@ -2898,7 +3533,7 @@ class WorkspaceFiles {
2898
3533
  async#scan(item) {
2899
3534
  const controller = new AbortController;
2900
3535
  item.controller = controller;
2901
- const context = withAbortSignal2(controller.signal, BACKGROUND_CONTEXT2);
3536
+ const context = withAbortSignal3(controller.signal, BACKGROUND_CONTEXT2);
2902
3537
  while (this.#active >= this.limits.concurrent) {
2903
3538
  await new Promise((resolve, reject) => {
2904
3539
  const wake = () => {
@@ -3064,7 +3699,7 @@ function cancellable(work, signal) {
3064
3699
  // packages/core/src/runtime.ts
3065
3700
  var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
3066
3701
  var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
3067
- var ArchivedDoc = defineDoc4({
3702
+ var ArchivedDoc = defineDoc5({
3068
3703
  kind: "app.neko.conversation",
3069
3704
  version: 1,
3070
3705
  scope: "conversation",
@@ -3072,7 +3707,7 @@ var ArchivedDoc = defineDoc4({
3072
3707
  fork: "initial",
3073
3708
  initial: () => ({ archived: false })
3074
3709
  });
3075
- var WorkspacesDoc = defineDoc4({
3710
+ var WorkspacesDoc = defineDoc5({
3076
3711
  kind: "app.neko.workspaces",
3077
3712
  version: 1,
3078
3713
  scope: "conversation",
@@ -3080,7 +3715,7 @@ var WorkspacesDoc = defineDoc4({
3080
3715
  fork: "initial",
3081
3716
  initial: () => ({ items: [] })
3082
3717
  });
3083
- var ActivityDoc = defineDoc4({
3718
+ var ActivityDoc = defineDoc5({
3084
3719
  kind: "app.neko.activity",
3085
3720
  version: 1,
3086
3721
  scope: "conversation",
@@ -3138,6 +3773,8 @@ async function createNekoRuntime(options = {}) {
3138
3773
  registry.install(createSubagentExtension(async (id, requestId, cwd) => {
3139
3774
  resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
3140
3775
  }, resourceSnapshots.discard));
3776
+ const jobs = new JobManager;
3777
+ registry.install(jobs.extension);
3141
3778
  for (const extension of options.extensions ?? [])
3142
3779
  registry.install(extension);
3143
3780
  let approvals;
@@ -3201,6 +3838,10 @@ async function createNekoRuntime(options = {}) {
3201
3838
  if (target)
3202
3839
  await registerWorkspace(savedWorkspace((await target.agent(context)).cwd));
3203
3840
  }
3841
+ const jobIo = (id) => ({
3842
+ commit: (change) => harness.commit(change, context),
3843
+ state: () => harness.snapshot(JobsDoc, id, context)
3844
+ });
3204
3845
  let lastActivity = 0;
3205
3846
  for (const record of existing)
3206
3847
  lastActivity = Math.max(lastActivity, (await harness.snapshot(ActivityDoc, record.id, context))?.lastUsedAt ?? 0);
@@ -3223,6 +3864,7 @@ async function createNekoRuntime(options = {}) {
3223
3864
  const cleanup = () => {
3224
3865
  if (!closing)
3225
3866
  closing = (async () => {
3867
+ jobs.shutdown();
3226
3868
  await workspaceFiles.close();
3227
3869
  await metadata.close();
3228
3870
  })();
@@ -3355,6 +3997,16 @@ async function createNekoRuntime(options = {}) {
3355
3997
  compactionRecord() {
3356
3998
  return readCompaction(harness, storage, target.id, context);
3357
3999
  },
4000
+ jobs() {
4001
+ return jobs.list(jobIo(target.id), target.id);
4002
+ },
4003
+ jobOutput(jobId, since) {
4004
+ return jobs.readOutput(jobIo(target.id), target.id, jobId, since);
4005
+ },
4006
+ async stopJob(jobId) {
4007
+ const job = await jobs.stop(jobIo(target.id), target.id, jobId, "user", context);
4008
+ return summarizeJobRecord(job);
4009
+ },
3358
4010
  async compact(instructions) {
3359
4011
  const agent = await target.agent(context);
3360
4012
  if (agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek"))
@@ -3546,6 +4198,7 @@ async function createNekoRuntime(options = {}) {
3546
4198
  getConversation,
3547
4199
  async archiveConversation(id) {
3548
4200
  await (await sessionFor(id)).abort();
4201
+ await jobs.stopAll(jobIo(id), id);
3549
4202
  await metadata.archive(id);
3550
4203
  await harness.commit(async (tx) => {
3551
4204
  (await tx.doc(ArchivedDoc, id)).archived = true;
@@ -3687,7 +4340,7 @@ import { dirname as dirname5, join as join7 } from "path";
3687
4340
  import { fileURLToPath as fileURLToPath2 } from "url";
3688
4341
 
3689
4342
  // packages/core/src/application-version.ts
3690
- var RUNNING_VERSION = "0.3.1";
4343
+ var RUNNING_VERSION = "0.3.3";
3691
4344
  var APPLICATION_PROTOCOL = 4;
3692
4345
  function stableVersion(value) {
3693
4346
  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)));
@@ -3837,11 +4490,11 @@ class UpdateChecker {
3837
4490
  }
3838
4491
 
3839
4492
  // packages/gateway/src/server-services.ts
3840
- import { Type as Type7 } from "typebox";
4493
+ import { Type as Type8 } from "typebox";
3841
4494
 
3842
4495
  // packages/gateway/src/contract.ts
3843
4496
  import { method, remote } from "@earendil-works/chord";
3844
- import { Type as Type4 } from "typebox";
4497
+ import { Type as Type5 } from "typebox";
3845
4498
  var rpc = (args, result) => [args, result];
3846
4499
  function contract(name, def) {
3847
4500
  const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
@@ -3853,19 +4506,19 @@ function contract(name, def) {
3853
4506
  ]));
3854
4507
  return remote(name, { ...def, variants });
3855
4508
  }
3856
- var str = Type4.String();
3857
- var num = Type4.Number();
3858
- var bool = Type4.Boolean();
3859
- var nil = Type4.Null();
3860
- var integer = Type4.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
3861
- var nullableString = Type4.Union([str, nil]);
3862
- var phase = Type4.Enum(["pending", "running", "done"]);
3863
- var level = Type4.Enum(["project", "user", "builtin"]);
3864
- var thinking = Type4.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
3865
- var model = Type4.Object({ provider: str, modelId: str });
3866
- var change = Type4.Object({ model: Type4.Optional(model), thinkingLevel: Type4.Optional(thinking) }, { additionalProperties: false });
3867
- var usage = Type4.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
3868
- var image = Type4.Object({
4509
+ var str = Type5.String();
4510
+ var num = Type5.Number();
4511
+ var bool = Type5.Boolean();
4512
+ var nil = Type5.Null();
4513
+ var integer = Type5.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
4514
+ var nullableString = Type5.Union([str, nil]);
4515
+ var phase = Type5.Enum(["pending", "running", "done"]);
4516
+ var level = Type5.Enum(["project", "user", "builtin"]);
4517
+ var thinking = Type5.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
4518
+ var model = Type5.Object({ provider: str, modelId: str });
4519
+ var change = Type5.Object({ model: Type5.Optional(model), thinkingLevel: Type5.Optional(thinking) }, { additionalProperties: false });
4520
+ var usage = Type5.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
4521
+ var image = Type5.Object({
3869
4522
  name: str,
3870
4523
  mimeType: str,
3871
4524
  width: integer,
@@ -3874,99 +4527,125 @@ var image = Type4.Object({
3874
4527
  entryId: integer,
3875
4528
  contentIndex: integer
3876
4529
  });
3877
- var skill = Type4.Object({ name: str, location: str, body: str, args: str });
3878
- var call = Type4.Object({
4530
+ var skill = Type5.Object({ name: str, location: str, body: str, args: str });
4531
+ var call = Type5.Object({
3879
4532
  callId: str,
3880
4533
  name: str,
3881
4534
  summary: str,
3882
4535
  detail: nullableString,
3883
4536
  phase,
3884
- output: nullableString
4537
+ output: nullableString,
4538
+ diff: nullableString,
4539
+ diffAdded: Type5.Union([num, nil]),
4540
+ diffRemoved: Type5.Union([num, nil]),
4541
+ diffTruncated: Type5.Optional(bool)
3885
4542
  });
3886
- var DelegationSchema = Type4.Object({
4543
+ var DelegationSchema = Type5.Object({
3887
4544
  parentId: integer,
3888
- agentId: Type4.Optional(str),
3889
- round: Type4.Optional(integer),
3890
- runnerId: Type4.Optional(integer),
3891
- inputEntryId: Type4.Optional(integer),
3892
- answerEntryId: Type4.Optional(integer),
3893
- roundUsage: Type4.Optional(Type4.Union([usage, nil])),
4545
+ agentId: Type5.Optional(str),
4546
+ round: Type5.Optional(integer),
4547
+ runnerId: Type5.Optional(integer),
4548
+ inputEntryId: Type5.Optional(integer),
4549
+ answerEntryId: Type5.Optional(integer),
4550
+ roundUsage: Type5.Optional(Type5.Union([usage, nil])),
3894
4551
  taskId: integer,
3895
4552
  callId: str,
3896
4553
  childId: integer,
3897
- role: Type4.Enum(["explore", "plan", "coder"]),
4554
+ role: Type5.Enum(["explore", "plan", "coder"]),
3898
4555
  title: str,
3899
4556
  createdAt: num,
3900
- startedAt: Type4.Union([num, nil]),
3901
- endedAt: Type4.Union([num, nil]),
3902
- status: Type4.Enum(["queued", "running", "completed", "failed", "cancelled"]),
4557
+ startedAt: Type5.Union([num, nil]),
4558
+ endedAt: Type5.Union([num, nil]),
4559
+ status: Type5.Enum(["queued", "running", "completed", "failed", "cancelled"]),
3903
4560
  report: nullableString,
3904
- approvalCount: Type4.Optional(integer),
3905
- historyThrough: Type4.Optional(integer),
3906
- historyUsage: Type4.Optional(usage)
4561
+ approvalCount: Type5.Optional(integer),
4562
+ historyThrough: Type5.Optional(integer),
4563
+ historyUsage: Type5.Optional(usage)
3907
4564
  });
3908
- var SubagentSummarySchema = Type4.Object({
4565
+ var SubagentSummarySchema = Type5.Object({
3909
4566
  agentId: str,
3910
4567
  parentId: integer,
3911
4568
  childId: integer,
3912
- role: Type4.Enum(["explore", "plan", "coder"]),
4569
+ role: Type5.Enum(["explore", "plan", "coder"]),
3913
4570
  title: str,
3914
- status: Type4.Enum(["idle", "queued", "running"]),
3915
- activeRoundId: Type4.Union([integer, nil]),
4571
+ status: Type5.Enum(["idle", "queued", "running"]),
4572
+ activeRoundId: Type5.Union([integer, nil]),
3916
4573
  lastRoundId: integer,
3917
- lastReport: Type4.Optional(nullableString),
4574
+ lastReport: Type5.Optional(nullableString),
3918
4575
  roundCount: integer,
3919
4576
  taskTitle: str,
3920
4577
  approvalCount: integer,
3921
4578
  createdAt: num,
3922
- historyThrough: Type4.Optional(integer),
3923
- historyUsage: Type4.Optional(usage)
4579
+ historyThrough: Type5.Optional(integer),
4580
+ historyUsage: Type5.Optional(usage)
3924
4581
  });
3925
- var ChatViewSchema = Type4.Object({
3926
- historyBefore: Type4.Optional(Type4.Union([integer, nil])),
3927
- agents: Type4.Optional(Type4.Array(SubagentSummarySchema)),
3928
- rounds: Type4.Optional(Type4.Array(DelegationSchema)),
3929
- subagents: Type4.Optional(Type4.Array(DelegationSchema)),
3930
- taskUsage: Type4.Optional(usage),
4582
+ var jobStatus = Type5.Enum(["running", "exited", "failed", "stopped", "lost"]);
4583
+ var JobSummarySchema = Type5.Object({
4584
+ jobId: str,
4585
+ title: str,
4586
+ command: str,
4587
+ cwd: str,
4588
+ status: jobStatus,
4589
+ startedAt: num,
4590
+ endedAt: Type5.Union([num, nil]),
4591
+ exitCode: Type5.Union([num, nil]),
4592
+ reason: Type5.Optional(Type5.Enum(["timeout", "spawn-error", "restart", "aborted"])),
4593
+ timeoutSeconds: Type5.Optional(num),
4594
+ outputBytes: integer
4595
+ });
4596
+ var JobOutputSchema = Type5.Object({
4597
+ job: JobSummarySchema,
4598
+ text: str,
4599
+ offset: integer,
4600
+ truncated: bool,
4601
+ dropped: integer
4602
+ });
4603
+ var ChatViewSchema = Type5.Object({
4604
+ historyBefore: Type5.Optional(Type5.Union([integer, nil])),
4605
+ agents: Type5.Optional(Type5.Array(SubagentSummarySchema)),
4606
+ jobs: Type5.Optional(Type5.Array(JobSummarySchema)),
4607
+ rounds: Type5.Optional(Type5.Array(DelegationSchema)),
4608
+ subagents: Type5.Optional(Type5.Array(DelegationSchema)),
4609
+ taskUsage: Type5.Optional(usage),
3931
4610
  model: str,
3932
- agent: Type4.Object({ model: Type4.Union([model, nil]), thinkingLevel: Type4.Union([thinking, nil]) }),
4611
+ agent: Type5.Object({ model: Type5.Union([model, nil]), thinkingLevel: Type5.Union([thinking, nil]) }),
3933
4612
  cwd: str,
3934
4613
  busy: bool,
3935
4614
  status: nullableString,
3936
4615
  usage,
3937
- contextUsage: Type4.Optional(Type4.Object({ tokens: Type4.Union([num, nil]), window: num, percent: Type4.Union([num, nil]) })),
3938
- compaction: Type4.Optional(Type4.Object({
3939
- failure: Type4.Optional(Type4.Object({ taskId: integer, reason: Type4.Enum(["manual", "threshold", "overflow"]), error: str })),
3940
- active: Type4.Array(Type4.Object({
4616
+ contextUsage: Type5.Optional(Type5.Object({ tokens: Type5.Union([num, nil]), window: num, percent: Type5.Union([num, nil]) })),
4617
+ compaction: Type5.Optional(Type5.Object({
4618
+ failure: Type5.Optional(Type5.Object({ taskId: integer, reason: Type5.Enum(["manual", "threshold", "overflow"]), error: str })),
4619
+ active: Type5.Array(Type5.Object({
3941
4620
  taskId: integer,
3942
- reason: Type4.Enum(["manual", "threshold", "overflow"]),
4621
+ reason: Type5.Enum(["manual", "threshold", "overflow"]),
3943
4622
  blocking: bool,
3944
4623
  attempt: integer,
3945
- retry: Type4.Optional(Type4.Object({ at: num, error: str }))
4624
+ retry: Type5.Optional(Type5.Object({ at: num, error: str }))
3946
4625
  })),
3947
- latest: Type4.Union([
4626
+ latest: Type5.Union([
3948
4627
  nil,
3949
- Type4.Object({
4628
+ Type5.Object({
3950
4629
  taskId: integer,
3951
- phase: Type4.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
4630
+ phase: Type5.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
3952
4631
  at: num
3953
4632
  })
3954
4633
  ])
3955
4634
  })),
3956
- items: Type4.Array(Type4.Union([
3957
- Type4.Object({
3958
- type: Type4.Literal("user"),
3959
- order: Type4.Optional(num),
4635
+ items: Type5.Array(Type5.Union([
4636
+ Type5.Object({
4637
+ type: Type5.Literal("user"),
4638
+ order: Type5.Optional(num),
3960
4639
  key: str,
3961
4640
  id: integer,
3962
4641
  text: str,
3963
- images: Type4.Optional(Type4.Array(image)),
3964
- skill: Type4.Optional(skill),
3965
- defaultImagePrompt: Type4.Optional(Type4.Object({ version: Type4.Integer({ minimum: 1 }), text: str }))
4642
+ images: Type5.Optional(Type5.Array(image)),
4643
+ skill: Type5.Optional(skill),
4644
+ defaultImagePrompt: Type5.Optional(Type5.Object({ version: Type5.Integer({ minimum: 1 }), text: str }))
3966
4645
  }),
3967
- Type4.Object({
3968
- type: Type4.Literal("assistant"),
3969
- order: Type4.Optional(num),
4646
+ Type5.Object({
4647
+ type: Type5.Literal("assistant"),
4648
+ order: Type5.Optional(num),
3970
4649
  key: str,
3971
4650
  id: integer,
3972
4651
  thinking: str,
@@ -3975,11 +4654,11 @@ var ChatViewSchema = Type4.Object({
3975
4654
  errorMessage: nullableString,
3976
4655
  note: nullableString
3977
4656
  }),
3978
- Type4.Object({
3979
- type: Type4.Literal("tool"),
3980
- order: Type4.Optional(num),
4657
+ Type5.Object({
4658
+ type: Type5.Literal("tool"),
4659
+ order: Type5.Optional(num),
3981
4660
  key: str,
3982
- images: Type4.Optional(Type4.Array(image)),
4661
+ images: Type5.Optional(Type5.Array(image)),
3983
4662
  callId: str,
3984
4663
  name: str,
3985
4664
  summary: str,
@@ -3988,50 +4667,61 @@ var ChatViewSchema = Type4.Object({
3988
4667
  isError: bool,
3989
4668
  output: nullableString,
3990
4669
  result: nullableString,
3991
- durationMs: Type4.Union([num, nil]),
3992
- note: nullableString
4670
+ durationMs: Type5.Union([num, nil]),
4671
+ note: nullableString,
4672
+ diff: nullableString,
4673
+ diffAdded: Type5.Union([num, nil]),
4674
+ diffRemoved: Type5.Union([num, nil]),
4675
+ diffTruncated: Type5.Optional(bool)
3993
4676
  }),
3994
- Type4.Object({ type: Type4.Literal("system"), order: Type4.Optional(num), key: str, id: integer, text: str })
4677
+ Type5.Object({
4678
+ type: Type5.Literal("system"),
4679
+ order: Type5.Optional(num),
4680
+ key: str,
4681
+ id: integer,
4682
+ text: str,
4683
+ jobId: Type5.Optional(str)
4684
+ })
3995
4685
  ])),
3996
- streaming: Type4.Union([nil, Type4.Object({ thinking: str, text: str, toolCalls: Type4.Array(call) })])
4686
+ streaming: Type5.Union([nil, Type5.Object({ thinking: str, text: str, toolCalls: Type5.Array(call) })])
3997
4687
  });
3998
- var SessionSummarySchema = Type4.Object({
4688
+ var SessionSummarySchema = Type5.Object({
3999
4689
  workspaceRoot: str,
4000
4690
  lastUsedAt: num,
4001
4691
  title: str,
4002
4692
  firstPromptPreview: str,
4003
4693
  createdAt: num,
4004
- lastMessageAt: Type4.Union([num, nil]),
4005
- model: Type4.Union([model, nil]),
4006
- status: Type4.Enum(["idle", "running", "error"]),
4694
+ lastMessageAt: Type5.Union([num, nil]),
4695
+ model: Type5.Union([model, nil]),
4696
+ status: Type5.Enum(["idle", "running", "error"]),
4007
4697
  id: str
4008
4698
  });
4009
- var DirectoryStateSchema = Type4.Object({ revision: integer, sessions: Type4.Array(SessionSummarySchema) });
4010
- var WorkspaceSummarySchema = Type4.Object({ path: str, createdAt: num });
4011
- var WorkspaceStateSchema = Type4.Object({ revision: integer, workspaces: Type4.Array(WorkspaceSummarySchema) });
4012
- var credentialStatus = Type4.Object({
4699
+ var DirectoryStateSchema = Type5.Object({ revision: integer, sessions: Type5.Array(SessionSummarySchema) });
4700
+ var WorkspaceSummarySchema = Type5.Object({ path: str, createdAt: num });
4701
+ var WorkspaceStateSchema = Type5.Object({ revision: integer, workspaces: Type5.Array(WorkspaceSummarySchema) });
4702
+ var credentialStatus = Type5.Object({
4013
4703
  savedConfigured: bool,
4014
4704
  environmentConfigured: bool,
4015
- source: Type4.Enum(["saved", "environment", "runtime", "keyless", "none"])
4705
+ source: Type5.Enum(["saved", "environment", "runtime", "keyless", "none"])
4016
4706
  });
4017
- var CredentialStateSchema = Type4.Object({
4707
+ var CredentialStateSchema = Type5.Object({
4018
4708
  revision: integer,
4019
- statuses: Type4.Object({ deepseek: credentialStatus, tavily: credentialStatus })
4709
+ statuses: Type5.Object({ deepseek: credentialStatus, tavily: credentialStatus })
4020
4710
  });
4021
- var AttachmentSchema = Type4.Object({ sessionId: str, attachmentId: str });
4022
- var chatInput = Type4.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
4711
+ var AttachmentSchema = Type5.Object({ sessionId: str, attachmentId: str });
4712
+ var chatInput = Type5.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
4023
4713
  var NekoSessionDirectory = contract("neko.session-directory", {
4024
4714
  version: 1,
4025
- input: Type4.Object({}, { additionalProperties: false }),
4715
+ input: Type5.Object({}, { additionalProperties: false }),
4026
4716
  variants: { ready: { state: { state: DirectoryStateSchema } } }
4027
4717
  });
4028
4718
  var NekoSessionManagement = contract("neko.session-management", {
4029
4719
  version: 1,
4030
- input: Type4.Object({}, { additionalProperties: false }),
4720
+ input: Type5.Object({}, { additionalProperties: false }),
4031
4721
  variants: {
4032
4722
  ready: {
4033
4723
  methods: {
4034
- create: rpc(Type4.Object({ ...change.properties, cwd: Type4.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
4724
+ create: rpc(Type5.Object({ ...change.properties, cwd: Type5.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
4035
4725
  archive: rpc(str, nil),
4036
4726
  attach: rpc(str, AttachmentSchema),
4037
4727
  detach: rpc(nil, nil),
@@ -4040,30 +4730,30 @@ var NekoSessionManagement = contract("neko.session-management", {
4040
4730
  }
4041
4731
  }
4042
4732
  });
4043
- var resize = Type4.Object({
4044
- maxWidth: Type4.Optional(integer),
4045
- maxHeight: Type4.Optional(integer),
4046
- maxBytes: Type4.Optional(integer),
4047
- jpegQuality: Type4.Optional(integer)
4733
+ var resize = Type5.Object({
4734
+ maxWidth: Type5.Optional(integer),
4735
+ maxHeight: Type5.Optional(integer),
4736
+ maxBytes: Type5.Optional(integer),
4737
+ jpegQuality: Type5.Optional(integer)
4048
4738
  });
4049
- var inputLimits = Type4.Object({
4050
- maxRequestBytes: Type4.Optional(integer),
4051
- images: Type4.Optional(Type4.Object({
4052
- resize: Type4.Optional(resize),
4053
- maxPerMessage: Type4.Optional(integer),
4054
- maxPerRequest: Type4.Optional(integer)
4739
+ var inputLimits = Type5.Object({
4740
+ maxRequestBytes: Type5.Optional(integer),
4741
+ images: Type5.Optional(Type5.Object({
4742
+ resize: Type5.Optional(resize),
4743
+ maxPerMessage: Type5.Optional(integer),
4744
+ maxPerRequest: Type5.Optional(integer)
4055
4745
  }))
4056
4746
  });
4057
- var modelInfo = Type4.Object({
4747
+ var modelInfo = Type5.Object({
4058
4748
  provider: str,
4059
4749
  modelId: str,
4060
4750
  name: str,
4061
4751
  contextWindow: num,
4062
- input: Type4.Array(str),
4063
- inputLimits: Type4.Optional(inputLimits),
4752
+ input: Type5.Array(str),
4753
+ inputLimits: Type5.Optional(inputLimits),
4064
4754
  reasoning: bool,
4065
- supportedThinkingLevels: Type4.Array(thinking),
4066
- thinkingLevelAdjustments: Type4.Object({
4755
+ supportedThinkingLevels: Type5.Array(thinking),
4756
+ thinkingLevelAdjustments: Type5.Object({
4067
4757
  off: thinking,
4068
4758
  minimal: thinking,
4069
4759
  low: thinking,
@@ -4075,31 +4765,31 @@ var modelInfo = Type4.Object({
4075
4765
  });
4076
4766
  var NekoModels = contract("neko.models", {
4077
4767
  version: 1,
4078
- input: Type4.Object({}, { additionalProperties: false }),
4079
- variants: { ready: { methods: { list: rpc(nil, Type4.Array(modelInfo)) } } }
4768
+ input: Type5.Object({}, { additionalProperties: false }),
4769
+ variants: { ready: { methods: { list: rpc(nil, Type5.Array(modelInfo)) } } }
4080
4770
  });
4081
- var imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
4771
+ var imageChunk = Type5.Object({ data: str, total: integer, mimeType: str });
4082
4772
  var NekoSubagent = contract("neko.subagent", {
4083
4773
  version: 1,
4084
- input: Type4.Object({
4774
+ input: Type5.Object({
4085
4775
  sessionId: str,
4086
4776
  attachmentId: str,
4087
- taskId: Type4.Optional(integer),
4088
- agentId: Type4.Optional(str)
4777
+ taskId: Type5.Optional(integer),
4778
+ agentId: Type5.Optional(str)
4089
4779
  }),
4090
4780
  variants: {
4091
4781
  ready: {
4092
4782
  state: { revision: integer },
4093
4783
  methods: {
4094
- chunk: rpc(Type4.Object({
4095
- kind: Type4.Enum(["view", "history"]),
4096
- before: Type4.Optional(integer),
4097
- token: Type4.Optional(integer),
4784
+ chunk: rpc(Type5.Object({
4785
+ kind: Type5.Enum(["view", "history"]),
4786
+ before: Type5.Optional(integer),
4787
+ token: Type5.Optional(integer),
4098
4788
  offset: integer
4099
- }), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
4100
- getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
4101
- decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
4102
- stop: rpc(Type4.Union([nil, Type4.Object({ taskId: integer })]), nil)
4789
+ }), Type5.Object({ token: integer, data: str, next: Type5.Union([integer, nil]) })),
4790
+ getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
4791
+ decideApproval: rpc(Type5.Object({ id: integer, version: str, decision: Type5.Enum(["allowed", "denied"]) }), Type5.Enum(["pending", "allowed", "denied", "cancelled"])),
4792
+ stop: rpc(Type5.Union([nil, Type5.Object({ taskId: integer })]), nil)
4103
4793
  }
4104
4794
  }
4105
4795
  }
@@ -4111,79 +4801,81 @@ var NekoChat = contract("neko.chat", {
4111
4801
  ready: {
4112
4802
  state: { view: ChatViewSchema },
4113
4803
  methods: {
4114
- submit: rpc(Type4.Object({
4804
+ submit: rpc(Type5.Object({
4115
4805
  content: str,
4116
- images: Type4.Optional(Type4.Array(str, { maxItems: 4 })),
4117
- requestId: Type4.Optional(str),
4118
- whenBusy: Type4.Optional(Type4.Enum(["steer", "followUp"]))
4806
+ images: Type5.Optional(Type5.Array(str, { maxItems: 4 })),
4807
+ requestId: Type5.Optional(str),
4808
+ whenBusy: Type5.Optional(Type5.Enum(["steer", "followUp"]))
4119
4809
  }, { additionalProperties: false }), nil),
4120
4810
  abort: rpc(nil, nil),
4121
4811
  usage: rpc(nil, usage),
4122
- skills: rpc(nil, Type4.Array(Type4.Object({ name: str, description: str, location: str, level }))),
4812
+ skills: rpc(nil, Type5.Array(Type5.Object({ name: str, description: str, location: str, level }))),
4813
+ jobOutput: rpc(Type5.Object({ jobId: str, since: Type5.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
4814
+ jobStop: rpc(Type5.Object({ jobId: str }, { additionalProperties: false }), JobSummarySchema),
4123
4815
  configure: rpc(change, nil),
4124
- compact: rpc(Type4.Object({ instructions: Type4.Optional(str) }, { additionalProperties: false }), nil),
4125
- importImage: rpc(str, Type4.Object({ id: str, name: str })),
4126
- draftImage: rpc(Type4.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
4127
- beginImage: rpc(Type4.Object({ name: str, mimeType: str, size: Type4.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
4128
- appendImage: rpc(Type4.Object({ id: str, offset: integer, data: Type4.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
4816
+ compact: rpc(Type5.Object({ instructions: Type5.Optional(str) }, { additionalProperties: false }), nil),
4817
+ importImage: rpc(str, Type5.Object({ id: str, name: str })),
4818
+ draftImage: rpc(Type5.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
4819
+ beginImage: rpc(Type5.Object({ name: str, mimeType: str, size: Type5.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
4820
+ appendImage: rpc(Type5.Object({ id: str, offset: integer, data: Type5.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
4129
4821
  finishImage: rpc(str, nil),
4130
4822
  releaseImage: rpc(str, nil),
4131
- getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
4823
+ getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
4132
4824
  }
4133
4825
  }
4134
4826
  }
4135
4827
  });
4136
- var browse = Type4.Object({ path: str, query: Type4.Optional(str), showHidden: Type4.Optional(bool), cursor: Type4.Optional(str) }, { additionalProperties: false });
4137
- var page = Type4.Object({
4828
+ var browse = Type5.Object({ path: str, query: Type5.Optional(str), showHidden: Type5.Optional(bool), cursor: Type5.Optional(str) }, { additionalProperties: false });
4829
+ var page = Type5.Object({
4138
4830
  path: str,
4139
4831
  parent: nullableString,
4140
- entries: Type4.Array(Type4.Object({ name: str, path: str, error: Type4.Optional(str) })),
4832
+ entries: Type5.Array(Type5.Object({ name: str, path: str, error: Type5.Optional(str) })),
4141
4833
  next: nullableString
4142
4834
  });
4143
4835
  var NekoWorkspaces = contract("neko.workspaces", {
4144
4836
  version: 1,
4145
- input: Type4.Object({}, { additionalProperties: false }),
4837
+ input: Type5.Object({}, { additionalProperties: false }),
4146
4838
  variants: {
4147
4839
  ready: {
4148
4840
  state: { state: WorkspaceStateSchema },
4149
4841
  methods: {
4150
- add: rpc(Type4.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
4842
+ add: rpc(Type5.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
4151
4843
  browse: rpc(browse, page)
4152
4844
  }
4153
4845
  }
4154
4846
  }
4155
4847
  });
4156
- var provider = Type4.Enum(["deepseek", "tavily"]);
4848
+ var provider = Type5.Enum(["deepseek", "tavily"]);
4157
4849
  var NekoCredentials = contract("neko.credentials", {
4158
4850
  version: 1,
4159
- input: Type4.Object({}, { additionalProperties: false }),
4851
+ input: Type5.Object({}, { additionalProperties: false }),
4160
4852
  variants: {
4161
4853
  ready: {
4162
4854
  state: { state: CredentialStateSchema },
4163
4855
  methods: {
4164
- save: rpc(Type4.Object({ provider, key: Type4.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
4165
- remove: rpc(Type4.Object({ provider }, { additionalProperties: false }), nil)
4856
+ save: rpc(Type5.Object({ provider, key: Type5.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
4857
+ remove: rpc(Type5.Object({ provider }, { additionalProperties: false }), nil)
4166
4858
  }
4167
4859
  }
4168
4860
  }
4169
4861
  });
4170
- var FileCandidatesSchema = Type4.Object({
4171
- entries: Type4.Array(Type4.Object({ name: str, path: str, kind: Type4.Enum(["file", "directory"]) }), { maxItems: 100 }),
4862
+ var FileCandidatesSchema = Type5.Object({
4863
+ entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
4172
4864
  limited: bool,
4173
4865
  incomplete: bool
4174
4866
  });
4175
4867
  var NekoWorkspaceFiles = contract("neko.workspace-files", {
4176
4868
  version: 1,
4177
- input: Type4.Object({}, { additionalProperties: false }),
4869
+ input: Type5.Object({}, { additionalProperties: false }),
4178
4870
  variants: {
4179
4871
  ready: {
4180
4872
  methods: {
4181
- search: rpc(Type4.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
4873
+ search: rpc(Type5.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
4182
4874
  }
4183
4875
  }
4184
4876
  }
4185
4877
  });
4186
- var marketSkill = Type4.Object({
4878
+ var marketSkill = Type5.Object({
4187
4879
  name: str,
4188
4880
  description: str,
4189
4881
  category: str,
@@ -4193,38 +4885,38 @@ var marketSkill = Type4.Object({
4193
4885
  sha256: str,
4194
4886
  sizeBytes: integer,
4195
4887
  uploadedAt: str,
4196
- installed: Type4.Object({ project: bool, user: bool, builtin: bool }),
4197
- activeLevel: Type4.Union([level, nil])
4888
+ installed: Type5.Object({ project: bool, user: bool, builtin: bool }),
4889
+ activeLevel: Type5.Union([level, nil])
4198
4890
  });
4199
- var preview = Type4.Object({ body: str, files: Type4.Array(str) });
4200
- var scope = Type4.Enum(["user", "project"]);
4201
- var installResult = Type4.Object({ name: str, scope, path: str, shadowedBy: Type4.Optional(scope) });
4891
+ var preview = Type5.Object({ body: str, files: Type5.Array(str) });
4892
+ var scope = Type5.Enum(["user", "project"]);
4893
+ var installResult = Type5.Object({ name: str, scope, path: str, shadowedBy: Type5.Optional(scope) });
4202
4894
  var NekoSkillMarket = contract("neko.skill-market", {
4203
4895
  version: 1,
4204
- input: Type4.Object({}, { additionalProperties: false }),
4896
+ input: Type5.Object({}, { additionalProperties: false }),
4205
4897
  variants: {
4206
4898
  ready: {
4207
4899
  methods: {
4208
- list: rpc(Type4.Object({ query: Type4.Optional(str), workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), Type4.Array(marketSkill)),
4209
- detail: rpc(Type4.Object({ name: str, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), marketSkill),
4210
- preview: rpc(Type4.Object({ name: str }, { additionalProperties: false }), preview),
4211
- install: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str), overwrite: Type4.Optional(bool) }, { additionalProperties: false }), installResult),
4212
- uninstall: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), nil)
4900
+ list: rpc(Type5.Object({ query: Type5.Optional(str), workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), Type5.Array(marketSkill)),
4901
+ detail: rpc(Type5.Object({ name: str, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), marketSkill),
4902
+ preview: rpc(Type5.Object({ name: str }, { additionalProperties: false }), preview),
4903
+ install: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str), overwrite: Type5.Optional(bool) }, { additionalProperties: false }), installResult),
4904
+ uninstall: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), nil)
4213
4905
  }
4214
4906
  }
4215
4907
  }
4216
4908
  });
4217
- var ApplicationInfoSchema = Type4.Object({
4909
+ var ApplicationInfoSchema = Type5.Object({
4218
4910
  runningVersion: str,
4219
4911
  installedVersion: nullableString,
4220
- protocolVersion: Type4.Integer({ minimum: 1 }),
4912
+ protocolVersion: Type5.Integer({ minimum: 1 }),
4221
4913
  latestVersion: nullableString,
4222
- checkedAt: Type4.Union([integer, nil]),
4223
- checkState: Type4.Enum(["unknown", "checking", "ok", "failed", "development"])
4914
+ checkedAt: Type5.Union([integer, nil]),
4915
+ checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"])
4224
4916
  });
4225
4917
  var NekoApplicationInfo = contract("neko.application-info", {
4226
4918
  version: 1,
4227
- input: Type4.Object({}),
4919
+ input: Type5.Object({}),
4228
4920
  variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
4229
4921
  });
4230
4922
 
@@ -4240,9 +4932,9 @@ import {
4240
4932
  opaque as opaque2,
4241
4933
  resource as resource2
4242
4934
  } from "@earendil-works/chord";
4243
- import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
4935
+ import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
4244
4936
  import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
4245
- import { Type as Type6 } from "typebox";
4937
+ import { Type as Type7 } from "typebox";
4246
4938
 
4247
4939
  // packages/gateway/src/image-uploads.ts
4248
4940
  var IMAGE_CHUNK_BYTES = 256 * 1024;
@@ -4349,7 +5041,7 @@ import {
4349
5041
  resource
4350
5042
  } from "@earendil-works/chord";
4351
5043
  import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
4352
- import { Type as Type5 } from "typebox";
5044
+ import { Type as Type6 } from "typebox";
4353
5045
  async function openReader(runtime, childId, parentId, context, through, historyUsage) {
4354
5046
  const child = await runtime.harness.conversation(childId, context);
4355
5047
  if (!child)
@@ -4474,15 +5166,15 @@ async function openReader(runtime, childId, parentId, context, through, historyU
4474
5166
  }
4475
5167
  var ChildProjection = local("neko.subagent-projection", {
4476
5168
  version: 1,
4477
- input: Type5.Object({ parentId: Type5.Integer(), agentId: Type5.String() }),
5169
+ input: Type6.Object({ parentId: Type6.Integer(), agentId: Type6.String() }),
4478
5170
  members: {
4479
- revision: Type5.Integer(),
5171
+ revision: Type6.Integer(),
4480
5172
  reader: opaque("neko.subagent-reader@1")
4481
5173
  }
4482
5174
  });
4483
5175
  function installSubagentHost(host, runtime, context) {
4484
5176
  host.serve(ChildProjection, (input, _principal, self) => {
4485
- const { revision } = self.state({ revision: cell(Type5.Integer(), 0) });
5177
+ const { revision } = self.state({ revision: cell(Type6.Integer(), 0) });
4486
5178
  const { reader } = self.resources({
4487
5179
  reader: resource({
4488
5180
  rev: 1,
@@ -4524,7 +5216,7 @@ function installSubagentHost(host, runtime, context) {
4524
5216
  };
4525
5217
  const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
4526
5218
  const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
4527
- const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
5219
+ const { authorizedRevision } = self.state({ authorizedRevision: cell(Type6.Integer(), 0) });
4528
5220
  self.listen({
4529
5221
  authorizedView: listener(shared.revision, async (value, ctx) => {
4530
5222
  try {
@@ -4579,7 +5271,7 @@ function installSubagentHost(host, runtime, context) {
4579
5271
  }
4580
5272
 
4581
5273
  // packages/gateway/src/session-host.ts
4582
- var OwnerInput = Type6.Object({});
5274
+ var OwnerInput = Type7.Object({});
4583
5275
  var Owner = local2("neko.attachment-owner", {
4584
5276
  version: 1,
4585
5277
  input: OwnerInput,
@@ -4587,7 +5279,7 @@ var Owner = local2("neko.attachment-owner", {
4587
5279
  });
4588
5280
  var Projection = local2("neko.projection", {
4589
5281
  version: 1,
4590
- input: Type6.Object({ id: Type6.String() }),
5282
+ input: Type7.Object({ id: Type7.String() }),
4591
5283
  members: {
4592
5284
  view: ChatViewSchema,
4593
5285
  reader: opaque2("neko.projection-reader@1")
@@ -4603,6 +5295,8 @@ var emptyView = {
4603
5295
  items: [],
4604
5296
  streaming: null
4605
5297
  };
5298
+ var JOB_COMMAND_PREVIEW_CHARS = 1000;
5299
+ var clipJob = (job) => job.command.length > JOB_COMMAND_PREVIEW_CHARS ? { ...job, command: `${job.command.slice(0, JOB_COMMAND_PREVIEW_CHARS)}\u2026` } : job;
4606
5300
  async function openProjection(runtime, session, context) {
4607
5301
  const view = await session.viewState();
4608
5302
  const history = new Map;
@@ -4618,7 +5312,7 @@ async function openProjection(runtime, session, context) {
4618
5312
  const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
4619
5313
  let childIds = new Set;
4620
5314
  const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
4621
- if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals", "app.neko.subagents"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
5315
+ if (changes.some((change) => change.type === "document" && (["app.neko.delegations", "app.neko.approvals", "app.neko.subagents", "app.neko.jobs"].includes(change.record.kind) && change.conversationId === session.id || change.record.kind === "pi.usage" && change.conversationId !== undefined && childIds.has(change.conversationId))))
4622
5316
  changed?.(null);
4623
5317
  });
4624
5318
  return {
@@ -4644,6 +5338,7 @@ async function openProjection(runtime, session, context) {
4644
5338
  }, naming)
4645
5339
  }
4646
5340
  }, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
5341
+ const jobs = (await session.jobs()).map(clipJob);
4647
5342
  const subagents = await listDelegations(runtime.harness, session.id, context);
4648
5343
  const approvals = await runtime.approvals.list(session.id, context);
4649
5344
  for (const entry of subagents)
@@ -4658,6 +5353,7 @@ async function openProjection(runtime, session, context) {
4658
5353
  const u = totalUsage(aggregate);
4659
5354
  return {
4660
5355
  ...projected,
5356
+ jobs,
4661
5357
  agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
4662
5358
  subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
4663
5359
  ...entry,
@@ -4829,7 +5525,7 @@ function serveSessions(host, runtime, context) {
4829
5525
  getImage: async (input, ctx) => {
4830
5526
  if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
4831
5527
  throw new ChordError2("invalid", "Invalid image range");
4832
- const entry = await shared.reader.entry(input.entryId, withAbortSignal3(ctx.signal, context));
5528
+ const entry = await shared.reader.entry(input.entryId, withAbortSignal4(ctx.signal, context));
4833
5529
  const content = entry?.model?.[0]?.content;
4834
5530
  const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
4835
5531
  if (image?.type !== "image")
@@ -4866,6 +5562,21 @@ function serveSessions(host, runtime, context) {
4866
5562
  return null;
4867
5563
  },
4868
5564
  skills: () => runtime.listSkills(projection.value.session.id),
5565
+ jobOutput: async ({ jobId, since }) => {
5566
+ try {
5567
+ const { job, output } = await projection.value.session.jobOutput(jobId, since);
5568
+ return { job: clipJob(job), ...output };
5569
+ } catch (error) {
5570
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot read job output");
5571
+ }
5572
+ },
5573
+ jobStop: async ({ jobId }) => {
5574
+ try {
5575
+ return clipJob(await projection.value.session.stopJob(jobId));
5576
+ } catch (error) {
5577
+ throw new ChordError2("invalid", error instanceof Error ? error.message : "Cannot stop job");
5578
+ }
5579
+ },
4869
5580
  usage: async () => {
4870
5581
  const usage = totalUsage(await projection.value.session.usage());
4871
5582
  return {
@@ -5600,12 +6311,12 @@ async function listDrives() {
5600
6311
  // packages/gateway/src/server-services.ts
5601
6312
  var ServerState = local3("neko.server-state", {
5602
6313
  version: 1,
5603
- input: Type7.Object({}),
6314
+ input: Type8.Object({}),
5604
6315
  members: {
5605
6316
  directory: DirectoryStateSchema,
5606
6317
  workspaces: WorkspaceStateSchema,
5607
6318
  credentials: CredentialStateSchema,
5608
- refreshCredentials: method2(Type7.Null(), Type7.Null())
6319
+ refreshCredentials: method2(Type8.Null(), Type8.Null())
5609
6320
  }
5610
6321
  });
5611
6322
  async function createNekoServerServices(options) {