neko-harness 0.3.2 → 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,
@@ -691,6 +810,17 @@ function projectChatView(view, modelLabel, options = {}) {
691
810
  const items = [];
692
811
  for (const entry of view.entries) {
693
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
+ }
694
824
  if (message?.role === "user") {
695
825
  const defaultImagePrompt = entry.kind === NekoInput.kind ? savedDefaultImagePrompt(message.content, entry.data?.defaultImagePrompt) : undefined;
696
826
  items.push({
@@ -912,7 +1042,7 @@ import { deepseekProvider } from "@earendil-works/pi-ai/providers/deepseek";
912
1042
  import {
913
1043
  AssistantEntry as AssistantEntry2,
914
1044
  createRegistry,
915
- defineDoc as defineDoc4,
1045
+ defineDoc as defineDoc5,
916
1046
  Harness,
917
1047
  LiveDoc as importLiveDoc,
918
1048
  MemoryStorage,
@@ -1151,150 +1281,20 @@ var ImageDetails = defineExtension2({
1151
1281
  ]
1152
1282
  });
1153
1283
 
1154
- // packages/core/src/prompt/invoke.ts
1155
- import { dirname as dirname4 } from "path";
1156
-
1157
- // packages/core/src/prompt/diagnostics.ts
1158
- var ignorePromptDiagnostic = () => {};
1159
- var warnPromptDiagnostic = ({ path, message }) => {
1160
- console.warn(`[neko-prompt] ${JSON.stringify(path)}: ${message}`);
1161
- };
1162
- var isMissing = (error) => typeof error === "object" && error !== null && ("code" in error) && (error.code === "ENOENT" || error.code === "ENOTDIR");
1163
-
1164
- // packages/core/src/prompt/skills.ts
1165
- import { readdir, readFile as readFile2 } from "fs/promises";
1166
- import { basename, dirname as dirname3, join as join3 } from "path";
1167
- var MAX_NAME_LENGTH = 64;
1168
- var MAX_DESCRIPTION_LENGTH = 1024;
1169
- var parseSkillFile = (content, filePath, report = ignorePromptDiagnostic) => {
1170
- const skip = (message) => {
1171
- report({ path: filePath, message });
1172
- return null;
1173
- };
1174
- try {
1175
- const { frontmatter } = parseFrontmatter(content);
1176
- const description = frontmatter.description;
1177
- if (typeof description !== "string" || description.trim() === "")
1178
- return skip("Skill description is missing or empty; skipped.");
1179
- if (description.length > MAX_DESCRIPTION_LENGTH)
1180
- return skip("Skill description is too long; skipped.");
1181
- const name = typeof frontmatter.name === "string" && frontmatter.name.trim() !== "" ? frontmatter.name : basename(dirname3(filePath));
1182
- if (name.length > MAX_NAME_LENGTH || /[\s\p{Cc}]/u.test(name))
1183
- return skip("Skill name is too long or contains whitespace/control characters; skipped.");
1184
- return {
1185
- name,
1186
- description,
1187
- filePath,
1188
- disableModelInvocation: frontmatter["disable-model-invocation"] === true
1189
- };
1190
- } catch {
1191
- return skip("Invalid skill frontmatter; skipped.");
1192
- }
1193
- };
1194
- var scanSkillsDir = async (dir, report = ignorePromptDiagnostic, capture) => {
1195
- let entries;
1196
- try {
1197
- entries = await readdir(dir, { withFileTypes: true });
1198
- } catch (error) {
1199
- if (!isMissing(error))
1200
- report({ path: dir, message: "Cannot scan skills directory; skipped." });
1201
- return [];
1202
- }
1203
- const skills = [];
1204
- for (const entry of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
1205
- if (!entry.isDirectory())
1206
- continue;
1207
- const filePath = join3(dir, entry.name, "SKILL.md");
1208
- let content;
1209
- try {
1210
- content = await readFile2(filePath, "utf8");
1211
- } catch (error) {
1212
- if (!isMissing(error))
1213
- report({ path: filePath, message: "Cannot read skill; skipped." });
1214
- continue;
1215
- }
1216
- const skill = parseSkillFile(content, filePath, report);
1217
- if (skill) {
1218
- skills.push(skill);
1219
- capture?.(skill, content);
1220
- }
1221
- }
1222
- return skills;
1223
- };
1224
- var mergeSkills = (groups) => {
1225
- const byName = new Map;
1226
- for (const group of groups)
1227
- for (const skill of group)
1228
- byName.set(skill.name, skill);
1229
- return [...byName.values()];
1230
- };
1231
- var escapeXml = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1232
- var formatSkillsForPrompt = (skills) => {
1233
- const visible = skills.filter((skill) => !skill.disableModelInvocation);
1234
- if (visible.length === 0)
1235
- return "";
1236
- const lines = [
1237
- "The following skills provide specialized instructions for specific tasks.",
1238
- "Use the read tool to load a skill's file when the task matches its description.",
1239
- "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.",
1240
- "",
1241
- "<available_skills>"
1242
- ];
1243
- for (const skill of visible) {
1244
- lines.push(" <skill>");
1245
- lines.push(` <name>${escapeXml(skill.name)}</name>`);
1246
- lines.push(` <description>${escapeXml(skill.description)}</description>`);
1247
- lines.push(` <location>${escapeXml(skill.filePath)}</location>`);
1248
- lines.push(" </skill>");
1249
- }
1250
- lines.push("</available_skills>");
1251
- return lines.join(`
1252
- `);
1253
- };
1254
-
1255
- // packages/core/src/prompt/invoke.ts
1256
- var expandSkillCommand = async (text, skills, readFile, report = warnPromptDiagnostic, metadata) => {
1257
- if (!text.startsWith("/skill:"))
1258
- return text;
1259
- const spaceIndex = text.indexOf(" ");
1260
- const skillName = spaceIndex === -1 ? text.slice(7) : text.slice(7, spaceIndex);
1261
- const args = spaceIndex === -1 ? "" : text.slice(spaceIndex + 1).trim();
1262
- const skill = skills.find((s) => s.name === skillName);
1263
- if (!skill)
1264
- return text;
1265
- let body;
1266
- try {
1267
- body = stripFrontmatter(await readFile(skill.filePath)).trim();
1268
- } catch {
1269
- report({ path: skill.filePath, message: "Cannot expand known skill; original input preserved." });
1270
- return text;
1271
- }
1272
- metadata?.({
1273
- name: skill.name,
1274
- location: skill.filePath,
1275
- body: `References are relative to ${dirname4(skill.filePath)}.
1276
-
1277
- ${body}`,
1278
- args
1279
- });
1280
- const block = `<skill name="${escapeXml(skill.name)}" location="${escapeXml(skill.filePath)}">
1281
- References are relative to ${dirname4(skill.filePath)}.
1282
-
1283
- ${body}
1284
- </skill>`;
1285
- return args ? `${block}
1286
-
1287
- ${args}` : block;
1288
- };
1289
-
1290
- // packages/core/src/prompt/prompt.ts
1291
- 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";
1292
1292
 
1293
1293
  // packages/core/src/subagents.ts
1294
1294
  import {
1295
1295
  AssistantEntry,
1296
1296
  configure,
1297
- defineDoc,
1297
+ defineDoc as defineDoc2,
1298
1298
  defineExtension as defineExtension3,
1299
1299
  defineTask,
1300
1300
  defineTool as defineTool2,
@@ -1348,7 +1348,7 @@ function subagentUsageTotals(state) {
1348
1348
 
1349
1349
  // packages/core/src/subagents.ts
1350
1350
  var agentIdFor = (childId) => `agent-${childId}`;
1351
- var DelegationsDoc = defineDoc({
1351
+ var DelegationsDoc = defineDoc2({
1352
1352
  kind: "app.neko.delegations",
1353
1353
  version: 1,
1354
1354
  scope: "conversation",
@@ -1356,7 +1356,7 @@ var DelegationsDoc = defineDoc({
1356
1356
  fork: "initial",
1357
1357
  initial: () => ({ items: [] })
1358
1358
  });
1359
- var SubagentsDoc = defineDoc({
1359
+ var SubagentsDoc = defineDoc2({
1360
1360
  kind: "app.neko.subagents",
1361
1361
  version: 1,
1362
1362
  scope: "conversation",
@@ -1364,7 +1364,7 @@ var SubagentsDoc = defineDoc({
1364
1364
  fork: "initial",
1365
1365
  initial: () => ({ items: [] })
1366
1366
  });
1367
- var SubagentDoc = defineDoc({
1367
+ var SubagentDoc = defineDoc2({
1368
1368
  kind: "app.neko.subagent",
1369
1369
  version: 1,
1370
1370
  scope: "conversation",
@@ -1372,7 +1372,7 @@ var SubagentDoc = defineDoc({
1372
1372
  fork: "initial",
1373
1373
  initial: () => ({ role: null, parentId: null })
1374
1374
  });
1375
- var SlotsDoc = defineDoc({
1375
+ var SlotsDoc = defineDoc2({
1376
1376
  kind: "app.neko.subagent-slots",
1377
1377
  version: 1,
1378
1378
  scope: "session",
@@ -1703,7 +1703,7 @@ function createSubagentExtension(prepare, discard) {
1703
1703
  thinkingLevel: agent.thinkingLevel,
1704
1704
  cwd: agent.cwd,
1705
1705
  extensions: agent.extensions.filter((e) => e.name !== extension.name),
1706
- 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"))
1707
1707
  });
1708
1708
  Object.assign(await tx.doc(SubagentDoc, created.id), { role, parentId: api.conversationId });
1709
1709
  identity = {
@@ -1844,7 +1844,567 @@ async function installSubagentCoordinator(harness, parents, context) {
1844
1844
  harness.closed.finally(stop).catch(() => {});
1845
1845
  }
1846
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
+
1847
2406
  // packages/core/src/prompt/prompt.ts
2407
+ import { defineExtension as defineExtension5, section } from "@earendil-works/pi-durable";
1848
2408
  var KEYS = ["preamble", "rules", "project_context", "skills", "cwd"];
1849
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.";
1850
2410
  var TOOL_GUIDELINES = {
@@ -1866,6 +2426,9 @@ var TOOL_GUIDELINES = {
1866
2426
  "Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions."
1867
2427
  ],
1868
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
+ ],
1869
2432
  web_search: ["Use web_search for current information from the internet instead of guessing."],
1870
2433
  web_extract: ["Use web_extract to read the full content of a web page when you have its URL."]
1871
2434
  };
@@ -1905,7 +2468,7 @@ var createNekoPrompt = (loadResources) => {
1905
2468
  };
1906
2469
  return sections;
1907
2470
  };
1908
- return defineExtension4({
2471
+ return defineExtension5({
1909
2472
  name: "neko-prompt",
1910
2473
  sections: KEYS.map((key) => section(key, async (input, context) => {
1911
2474
  let pending = built.get(input);
@@ -2057,13 +2620,13 @@ var createResourceSnapshots = (load, submission) => {
2057
2620
  import { stripVTControlCharacters } from "util";
2058
2621
  import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
2059
2622
  import {
2060
- defineDoc as defineDoc2,
2061
- defineExtension as defineExtension5,
2062
- defineTask as defineTask2,
2623
+ defineDoc as defineDoc3,
2624
+ defineExtension as defineExtension6,
2625
+ defineTask as defineTask3,
2063
2626
  InboxDoc,
2064
2627
  LiveDoc as LiveDoc2
2065
2628
  } from "@earendil-works/pi-durable";
2066
- var SessionMetadataDoc = defineDoc2({
2629
+ var SessionMetadataDoc = defineDoc3({
2067
2630
  kind: "app.neko.session-metadata",
2068
2631
  version: 1,
2069
2632
  scope: "conversation",
@@ -2126,7 +2689,7 @@ function mergeUsage(base, extra) {
2126
2689
  function namingTask() {
2127
2690
  let active = 0;
2128
2691
  const waits = new Set;
2129
- return defineTask2({
2692
+ return defineTask3({
2130
2693
  name: "app.neko.name",
2131
2694
  version: 1,
2132
2695
  initial: () => ({ phase: "wait", until: Date.now() + 100 }),
@@ -2250,7 +2813,7 @@ class SessionMetadataManager {
2250
2813
  this.storage = storage;
2251
2814
  this.context = context;
2252
2815
  this.report = report;
2253
- registry.install(defineExtension5({ name: "neko-naming", tasks: [this.task] }));
2816
+ registry.install(defineExtension6({ name: "neko-naming", tasks: [this.task] }));
2254
2817
  this.#unsubscribe = harness.subscribeCommits(({ changes }) => {
2255
2818
  const ids = new Set;
2256
2819
  const submissions = new Map;
@@ -2394,14 +2957,14 @@ import { randomUUID } from "crypto";
2394
2957
  import { realpath } from "fs/promises";
2395
2958
  import {
2396
2959
  configure as configure2,
2397
- defineDoc as defineDoc3,
2398
- defineExtension as defineExtension6,
2960
+ defineDoc as defineDoc4,
2961
+ defineExtension as defineExtension7,
2399
2962
  hook as hook2,
2400
2963
  ToolTask as ToolTask2,
2401
2964
  wrapTool
2402
2965
  } from "@earendil-works/pi-durable";
2403
2966
  import { createBashTool } from "@earendil-works/pi-durable/tools";
2404
- var ApprovalsDoc = defineDoc3({
2967
+ var ApprovalsDoc = defineDoc4({
2405
2968
  kind: "app.neko.approvals",
2406
2969
  version: 1,
2407
2970
  scope: "conversation",
@@ -2509,7 +3072,7 @@ class ToolApprovals {
2509
3072
  }
2510
3073
  }
2511
3074
  function createApprovalExtension(service) {
2512
- return defineExtension6({
3075
+ return defineExtension7({
2513
3076
  name: "neko-tool-approvals",
2514
3077
  hooks: [
2515
3078
  hook2(ToolTask2, {
@@ -2573,8 +3136,8 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
2573
3136
  }
2574
3137
 
2575
3138
  // packages/core/src/web-search.ts
2576
- import { Type as Type3 } from "@earendil-works/pi-ai";
2577
- 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";
2578
3141
  var SEARCH_URL = "https://api.tavily.com/search";
2579
3142
  var EXTRACT_URL = "https://api.tavily.com/extract";
2580
3143
  var REQUEST_TIMEOUT_MS = 30000;
@@ -2692,15 +3255,15 @@ function urlKey(value) {
2692
3255
  }
2693
3256
  }
2694
3257
  function createWebSearchTools(resolveKey, fetchImpl = fetch) {
2695
- return defineExtension7({
3258
+ return defineExtension8({
2696
3259
  name: "neko-web-search",
2697
3260
  tools: [
2698
- defineTool3({
3261
+ defineTool4({
2699
3262
  name: "web_search",
2700
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.",
2701
- parameters: Type3.Object({
2702
- query: Type3.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
2703
- 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})` }))
2704
3267
  }),
2705
3268
  async execute({ query, maxResults }, _api, context) {
2706
3269
  try {
@@ -2725,11 +3288,11 @@ ${truncate(String(item.url ?? ""), 300)}
2725
3288
  }
2726
3289
  }
2727
3290
  }),
2728
- defineTool3({
3291
+ defineTool4({
2729
3292
  name: "web_extract",
2730
3293
  description: "Extract bounded markdown excerpts from web pages by URL. Every requested URL has a result or failure entry; long content is truncated.",
2731
- parameters: Type3.Object({
2732
- 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" }), {
2733
3296
  minItems: 1,
2734
3297
  maxItems: MAX_URLS,
2735
3298
  description: `URLs to extract (1-${MAX_URLS})`
@@ -2776,7 +3339,7 @@ ${truncate(String(item.url ?? ""), 300)}
2776
3339
  // packages/core/src/workspace-files.ts
2777
3340
  import { realpath as realpath2, stat } from "fs/promises";
2778
3341
  import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
2779
- 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";
2780
3343
  import { getOrThrow as getOrThrow2 } from "@earendil-works/pi-durable/env";
2781
3344
  import { Connection, LocalExecutionEnv, localDaemon } from "@earendil-works/pi-env";
2782
3345
  function relativeQuery(query) {
@@ -2942,7 +3505,7 @@ class WorkspaceFiles {
2942
3505
  for (const user of [...current.users])
2943
3506
  this.#closeWatcher(user);
2944
3507
  }
2945
- }, withAbortSignal2(current.controller.signal, BACKGROUND_CONTEXT2));
3508
+ }, withAbortSignal3(current.controller.signal, BACKGROUND_CONTEXT2));
2946
3509
  if (result.ok) {
2947
3510
  current.watcher = result.value;
2948
3511
  if (current.controller.signal.aborted)
@@ -2970,7 +3533,7 @@ class WorkspaceFiles {
2970
3533
  async#scan(item) {
2971
3534
  const controller = new AbortController;
2972
3535
  item.controller = controller;
2973
- const context = withAbortSignal2(controller.signal, BACKGROUND_CONTEXT2);
3536
+ const context = withAbortSignal3(controller.signal, BACKGROUND_CONTEXT2);
2974
3537
  while (this.#active >= this.limits.concurrent) {
2975
3538
  await new Promise((resolve, reject) => {
2976
3539
  const wake = () => {
@@ -3136,7 +3699,7 @@ function cancellable(work, signal) {
3136
3699
  // packages/core/src/runtime.ts
3137
3700
  var DEFAULT_MODEL = { provider: "deepseek", modelId: "deepseek-flash" };
3138
3701
  var DEFAULT_STORAGE_PATH = join5(homedir4(), ".neko", "neko.sqlite");
3139
- var ArchivedDoc = defineDoc4({
3702
+ var ArchivedDoc = defineDoc5({
3140
3703
  kind: "app.neko.conversation",
3141
3704
  version: 1,
3142
3705
  scope: "conversation",
@@ -3144,7 +3707,7 @@ var ArchivedDoc = defineDoc4({
3144
3707
  fork: "initial",
3145
3708
  initial: () => ({ archived: false })
3146
3709
  });
3147
- var WorkspacesDoc = defineDoc4({
3710
+ var WorkspacesDoc = defineDoc5({
3148
3711
  kind: "app.neko.workspaces",
3149
3712
  version: 1,
3150
3713
  scope: "conversation",
@@ -3152,7 +3715,7 @@ var WorkspacesDoc = defineDoc4({
3152
3715
  fork: "initial",
3153
3716
  initial: () => ({ items: [] })
3154
3717
  });
3155
- var ActivityDoc = defineDoc4({
3718
+ var ActivityDoc = defineDoc5({
3156
3719
  kind: "app.neko.activity",
3157
3720
  version: 1,
3158
3721
  scope: "conversation",
@@ -3210,6 +3773,8 @@ async function createNekoRuntime(options = {}) {
3210
3773
  registry.install(createSubagentExtension(async (id, requestId, cwd) => {
3211
3774
  resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
3212
3775
  }, resourceSnapshots.discard));
3776
+ const jobs = new JobManager;
3777
+ registry.install(jobs.extension);
3213
3778
  for (const extension of options.extensions ?? [])
3214
3779
  registry.install(extension);
3215
3780
  let approvals;
@@ -3273,6 +3838,10 @@ async function createNekoRuntime(options = {}) {
3273
3838
  if (target)
3274
3839
  await registerWorkspace(savedWorkspace((await target.agent(context)).cwd));
3275
3840
  }
3841
+ const jobIo = (id) => ({
3842
+ commit: (change) => harness.commit(change, context),
3843
+ state: () => harness.snapshot(JobsDoc, id, context)
3844
+ });
3276
3845
  let lastActivity = 0;
3277
3846
  for (const record of existing)
3278
3847
  lastActivity = Math.max(lastActivity, (await harness.snapshot(ActivityDoc, record.id, context))?.lastUsedAt ?? 0);
@@ -3295,6 +3864,7 @@ async function createNekoRuntime(options = {}) {
3295
3864
  const cleanup = () => {
3296
3865
  if (!closing)
3297
3866
  closing = (async () => {
3867
+ jobs.shutdown();
3298
3868
  await workspaceFiles.close();
3299
3869
  await metadata.close();
3300
3870
  })();
@@ -3427,6 +3997,16 @@ async function createNekoRuntime(options = {}) {
3427
3997
  compactionRecord() {
3428
3998
  return readCompaction(harness, storage, target.id, context);
3429
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
+ },
3430
4010
  async compact(instructions) {
3431
4011
  const agent = await target.agent(context);
3432
4012
  if (agent.model?.provider === "deepseek" && !await models.checkAuth("deepseek"))
@@ -3618,6 +4198,7 @@ async function createNekoRuntime(options = {}) {
3618
4198
  getConversation,
3619
4199
  async archiveConversation(id) {
3620
4200
  await (await sessionFor(id)).abort();
4201
+ await jobs.stopAll(jobIo(id), id);
3621
4202
  await metadata.archive(id);
3622
4203
  await harness.commit(async (tx) => {
3623
4204
  (await tx.doc(ArchivedDoc, id)).archived = true;
@@ -3759,7 +4340,7 @@ import { dirname as dirname5, join as join7 } from "path";
3759
4340
  import { fileURLToPath as fileURLToPath2 } from "url";
3760
4341
 
3761
4342
  // packages/core/src/application-version.ts
3762
- var RUNNING_VERSION = "0.3.2";
4343
+ var RUNNING_VERSION = "0.3.3";
3763
4344
  var APPLICATION_PROTOCOL = 4;
3764
4345
  function stableVersion(value) {
3765
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)));
@@ -3909,11 +4490,11 @@ class UpdateChecker {
3909
4490
  }
3910
4491
 
3911
4492
  // packages/gateway/src/server-services.ts
3912
- import { Type as Type7 } from "typebox";
4493
+ import { Type as Type8 } from "typebox";
3913
4494
 
3914
4495
  // packages/gateway/src/contract.ts
3915
4496
  import { method, remote } from "@earendil-works/chord";
3916
- import { Type as Type4 } from "typebox";
4497
+ import { Type as Type5 } from "typebox";
3917
4498
  var rpc = (args, result) => [args, result];
3918
4499
  function contract(name, def) {
3919
4500
  const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
@@ -3925,19 +4506,19 @@ function contract(name, def) {
3925
4506
  ]));
3926
4507
  return remote(name, { ...def, variants });
3927
4508
  }
3928
- var str = Type4.String();
3929
- var num = Type4.Number();
3930
- var bool = Type4.Boolean();
3931
- var nil = Type4.Null();
3932
- var integer = Type4.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
3933
- var nullableString = Type4.Union([str, nil]);
3934
- var phase = Type4.Enum(["pending", "running", "done"]);
3935
- var level = Type4.Enum(["project", "user", "builtin"]);
3936
- var thinking = Type4.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
3937
- var model = Type4.Object({ provider: str, modelId: str });
3938
- var change = Type4.Object({ model: Type4.Optional(model), thinkingLevel: Type4.Optional(thinking) }, { additionalProperties: false });
3939
- var usage = Type4.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
3940
- 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({
3941
4522
  name: str,
3942
4523
  mimeType: str,
3943
4524
  width: integer,
@@ -3946,8 +4527,8 @@ var image = Type4.Object({
3946
4527
  entryId: integer,
3947
4528
  contentIndex: integer
3948
4529
  });
3949
- var skill = Type4.Object({ name: str, location: str, body: str, args: str });
3950
- var call = Type4.Object({
4530
+ var skill = Type5.Object({ name: str, location: str, body: str, args: str });
4531
+ var call = Type5.Object({
3951
4532
  callId: str,
3952
4533
  name: str,
3953
4534
  summary: str,
@@ -3955,94 +4536,116 @@ var call = Type4.Object({
3955
4536
  phase,
3956
4537
  output: nullableString,
3957
4538
  diff: nullableString,
3958
- diffAdded: Type4.Union([num, nil]),
3959
- diffRemoved: Type4.Union([num, nil]),
3960
- diffTruncated: Type4.Optional(bool)
4539
+ diffAdded: Type5.Union([num, nil]),
4540
+ diffRemoved: Type5.Union([num, nil]),
4541
+ diffTruncated: Type5.Optional(bool)
3961
4542
  });
3962
- var DelegationSchema = Type4.Object({
4543
+ var DelegationSchema = Type5.Object({
3963
4544
  parentId: integer,
3964
- agentId: Type4.Optional(str),
3965
- round: Type4.Optional(integer),
3966
- runnerId: Type4.Optional(integer),
3967
- inputEntryId: Type4.Optional(integer),
3968
- answerEntryId: Type4.Optional(integer),
3969
- 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])),
3970
4551
  taskId: integer,
3971
4552
  callId: str,
3972
4553
  childId: integer,
3973
- role: Type4.Enum(["explore", "plan", "coder"]),
4554
+ role: Type5.Enum(["explore", "plan", "coder"]),
3974
4555
  title: str,
3975
4556
  createdAt: num,
3976
- startedAt: Type4.Union([num, nil]),
3977
- endedAt: Type4.Union([num, nil]),
3978
- 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"]),
3979
4560
  report: nullableString,
3980
- approvalCount: Type4.Optional(integer),
3981
- historyThrough: Type4.Optional(integer),
3982
- historyUsage: Type4.Optional(usage)
4561
+ approvalCount: Type5.Optional(integer),
4562
+ historyThrough: Type5.Optional(integer),
4563
+ historyUsage: Type5.Optional(usage)
3983
4564
  });
3984
- var SubagentSummarySchema = Type4.Object({
4565
+ var SubagentSummarySchema = Type5.Object({
3985
4566
  agentId: str,
3986
4567
  parentId: integer,
3987
4568
  childId: integer,
3988
- role: Type4.Enum(["explore", "plan", "coder"]),
4569
+ role: Type5.Enum(["explore", "plan", "coder"]),
3989
4570
  title: str,
3990
- status: Type4.Enum(["idle", "queued", "running"]),
3991
- activeRoundId: Type4.Union([integer, nil]),
4571
+ status: Type5.Enum(["idle", "queued", "running"]),
4572
+ activeRoundId: Type5.Union([integer, nil]),
3992
4573
  lastRoundId: integer,
3993
- lastReport: Type4.Optional(nullableString),
4574
+ lastReport: Type5.Optional(nullableString),
3994
4575
  roundCount: integer,
3995
4576
  taskTitle: str,
3996
4577
  approvalCount: integer,
3997
4578
  createdAt: num,
3998
- historyThrough: Type4.Optional(integer),
3999
- historyUsage: Type4.Optional(usage)
4579
+ historyThrough: Type5.Optional(integer),
4580
+ historyUsage: Type5.Optional(usage)
4000
4581
  });
4001
- var ChatViewSchema = Type4.Object({
4002
- historyBefore: Type4.Optional(Type4.Union([integer, nil])),
4003
- agents: Type4.Optional(Type4.Array(SubagentSummarySchema)),
4004
- rounds: Type4.Optional(Type4.Array(DelegationSchema)),
4005
- subagents: Type4.Optional(Type4.Array(DelegationSchema)),
4006
- 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),
4007
4610
  model: str,
4008
- 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]) }),
4009
4612
  cwd: str,
4010
4613
  busy: bool,
4011
4614
  status: nullableString,
4012
4615
  usage,
4013
- contextUsage: Type4.Optional(Type4.Object({ tokens: Type4.Union([num, nil]), window: num, percent: Type4.Union([num, nil]) })),
4014
- compaction: Type4.Optional(Type4.Object({
4015
- failure: Type4.Optional(Type4.Object({ taskId: integer, reason: Type4.Enum(["manual", "threshold", "overflow"]), error: str })),
4016
- 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({
4017
4620
  taskId: integer,
4018
- reason: Type4.Enum(["manual", "threshold", "overflow"]),
4621
+ reason: Type5.Enum(["manual", "threshold", "overflow"]),
4019
4622
  blocking: bool,
4020
4623
  attempt: integer,
4021
- retry: Type4.Optional(Type4.Object({ at: num, error: str }))
4624
+ retry: Type5.Optional(Type5.Object({ at: num, error: str }))
4022
4625
  })),
4023
- latest: Type4.Union([
4626
+ latest: Type5.Union([
4024
4627
  nil,
4025
- Type4.Object({
4628
+ Type5.Object({
4026
4629
  taskId: integer,
4027
- 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"]),
4028
4631
  at: num
4029
4632
  })
4030
4633
  ])
4031
4634
  })),
4032
- items: Type4.Array(Type4.Union([
4033
- Type4.Object({
4034
- type: Type4.Literal("user"),
4035
- order: Type4.Optional(num),
4635
+ items: Type5.Array(Type5.Union([
4636
+ Type5.Object({
4637
+ type: Type5.Literal("user"),
4638
+ order: Type5.Optional(num),
4036
4639
  key: str,
4037
4640
  id: integer,
4038
4641
  text: str,
4039
- images: Type4.Optional(Type4.Array(image)),
4040
- skill: Type4.Optional(skill),
4041
- 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 }))
4042
4645
  }),
4043
- Type4.Object({
4044
- type: Type4.Literal("assistant"),
4045
- order: Type4.Optional(num),
4646
+ Type5.Object({
4647
+ type: Type5.Literal("assistant"),
4648
+ order: Type5.Optional(num),
4046
4649
  key: str,
4047
4650
  id: integer,
4048
4651
  thinking: str,
@@ -4051,11 +4654,11 @@ var ChatViewSchema = Type4.Object({
4051
4654
  errorMessage: nullableString,
4052
4655
  note: nullableString
4053
4656
  }),
4054
- Type4.Object({
4055
- type: Type4.Literal("tool"),
4056
- order: Type4.Optional(num),
4657
+ Type5.Object({
4658
+ type: Type5.Literal("tool"),
4659
+ order: Type5.Optional(num),
4057
4660
  key: str,
4058
- images: Type4.Optional(Type4.Array(image)),
4661
+ images: Type5.Optional(Type5.Array(image)),
4059
4662
  callId: str,
4060
4663
  name: str,
4061
4664
  summary: str,
@@ -4064,54 +4667,61 @@ var ChatViewSchema = Type4.Object({
4064
4667
  isError: bool,
4065
4668
  output: nullableString,
4066
4669
  result: nullableString,
4067
- durationMs: Type4.Union([num, nil]),
4670
+ durationMs: Type5.Union([num, nil]),
4068
4671
  note: nullableString,
4069
4672
  diff: nullableString,
4070
- diffAdded: Type4.Union([num, nil]),
4071
- diffRemoved: Type4.Union([num, nil]),
4072
- diffTruncated: Type4.Optional(bool)
4673
+ diffAdded: Type5.Union([num, nil]),
4674
+ diffRemoved: Type5.Union([num, nil]),
4675
+ diffTruncated: Type5.Optional(bool)
4073
4676
  }),
4074
- 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
+ })
4075
4685
  ])),
4076
- 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) })])
4077
4687
  });
4078
- var SessionSummarySchema = Type4.Object({
4688
+ var SessionSummarySchema = Type5.Object({
4079
4689
  workspaceRoot: str,
4080
4690
  lastUsedAt: num,
4081
4691
  title: str,
4082
4692
  firstPromptPreview: str,
4083
4693
  createdAt: num,
4084
- lastMessageAt: Type4.Union([num, nil]),
4085
- model: Type4.Union([model, nil]),
4086
- 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"]),
4087
4697
  id: str
4088
4698
  });
4089
- var DirectoryStateSchema = Type4.Object({ revision: integer, sessions: Type4.Array(SessionSummarySchema) });
4090
- var WorkspaceSummarySchema = Type4.Object({ path: str, createdAt: num });
4091
- var WorkspaceStateSchema = Type4.Object({ revision: integer, workspaces: Type4.Array(WorkspaceSummarySchema) });
4092
- 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({
4093
4703
  savedConfigured: bool,
4094
4704
  environmentConfigured: bool,
4095
- source: Type4.Enum(["saved", "environment", "runtime", "keyless", "none"])
4705
+ source: Type5.Enum(["saved", "environment", "runtime", "keyless", "none"])
4096
4706
  });
4097
- var CredentialStateSchema = Type4.Object({
4707
+ var CredentialStateSchema = Type5.Object({
4098
4708
  revision: integer,
4099
- statuses: Type4.Object({ deepseek: credentialStatus, tavily: credentialStatus })
4709
+ statuses: Type5.Object({ deepseek: credentialStatus, tavily: credentialStatus })
4100
4710
  });
4101
- var AttachmentSchema = Type4.Object({ sessionId: str, attachmentId: str });
4102
- 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 });
4103
4713
  var NekoSessionDirectory = contract("neko.session-directory", {
4104
4714
  version: 1,
4105
- input: Type4.Object({}, { additionalProperties: false }),
4715
+ input: Type5.Object({}, { additionalProperties: false }),
4106
4716
  variants: { ready: { state: { state: DirectoryStateSchema } } }
4107
4717
  });
4108
4718
  var NekoSessionManagement = contract("neko.session-management", {
4109
4719
  version: 1,
4110
- input: Type4.Object({}, { additionalProperties: false }),
4720
+ input: Type5.Object({}, { additionalProperties: false }),
4111
4721
  variants: {
4112
4722
  ready: {
4113
4723
  methods: {
4114
- 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),
4115
4725
  archive: rpc(str, nil),
4116
4726
  attach: rpc(str, AttachmentSchema),
4117
4727
  detach: rpc(nil, nil),
@@ -4120,30 +4730,30 @@ var NekoSessionManagement = contract("neko.session-management", {
4120
4730
  }
4121
4731
  }
4122
4732
  });
4123
- var resize = Type4.Object({
4124
- maxWidth: Type4.Optional(integer),
4125
- maxHeight: Type4.Optional(integer),
4126
- maxBytes: Type4.Optional(integer),
4127
- 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)
4128
4738
  });
4129
- var inputLimits = Type4.Object({
4130
- maxRequestBytes: Type4.Optional(integer),
4131
- images: Type4.Optional(Type4.Object({
4132
- resize: Type4.Optional(resize),
4133
- maxPerMessage: Type4.Optional(integer),
4134
- 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)
4135
4745
  }))
4136
4746
  });
4137
- var modelInfo = Type4.Object({
4747
+ var modelInfo = Type5.Object({
4138
4748
  provider: str,
4139
4749
  modelId: str,
4140
4750
  name: str,
4141
4751
  contextWindow: num,
4142
- input: Type4.Array(str),
4143
- inputLimits: Type4.Optional(inputLimits),
4752
+ input: Type5.Array(str),
4753
+ inputLimits: Type5.Optional(inputLimits),
4144
4754
  reasoning: bool,
4145
- supportedThinkingLevels: Type4.Array(thinking),
4146
- thinkingLevelAdjustments: Type4.Object({
4755
+ supportedThinkingLevels: Type5.Array(thinking),
4756
+ thinkingLevelAdjustments: Type5.Object({
4147
4757
  off: thinking,
4148
4758
  minimal: thinking,
4149
4759
  low: thinking,
@@ -4155,31 +4765,31 @@ var modelInfo = Type4.Object({
4155
4765
  });
4156
4766
  var NekoModels = contract("neko.models", {
4157
4767
  version: 1,
4158
- input: Type4.Object({}, { additionalProperties: false }),
4159
- 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)) } } }
4160
4770
  });
4161
- var imageChunk = Type4.Object({ data: str, total: integer, mimeType: str });
4771
+ var imageChunk = Type5.Object({ data: str, total: integer, mimeType: str });
4162
4772
  var NekoSubagent = contract("neko.subagent", {
4163
4773
  version: 1,
4164
- input: Type4.Object({
4774
+ input: Type5.Object({
4165
4775
  sessionId: str,
4166
4776
  attachmentId: str,
4167
- taskId: Type4.Optional(integer),
4168
- agentId: Type4.Optional(str)
4777
+ taskId: Type5.Optional(integer),
4778
+ agentId: Type5.Optional(str)
4169
4779
  }),
4170
4780
  variants: {
4171
4781
  ready: {
4172
4782
  state: { revision: integer },
4173
4783
  methods: {
4174
- chunk: rpc(Type4.Object({
4175
- kind: Type4.Enum(["view", "history"]),
4176
- before: Type4.Optional(integer),
4177
- 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),
4178
4788
  offset: integer
4179
- }), Type4.Object({ token: integer, data: str, next: Type4.Union([integer, nil]) })),
4180
- getImage: rpc(Type4.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
4181
- decideApproval: rpc(Type4.Object({ id: integer, version: str, decision: Type4.Enum(["allowed", "denied"]) }), Type4.Enum(["pending", "allowed", "denied", "cancelled"])),
4182
- 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)
4183
4793
  }
4184
4794
  }
4185
4795
  }
@@ -4191,79 +4801,81 @@ var NekoChat = contract("neko.chat", {
4191
4801
  ready: {
4192
4802
  state: { view: ChatViewSchema },
4193
4803
  methods: {
4194
- submit: rpc(Type4.Object({
4804
+ submit: rpc(Type5.Object({
4195
4805
  content: str,
4196
- images: Type4.Optional(Type4.Array(str, { maxItems: 4 })),
4197
- requestId: Type4.Optional(str),
4198
- 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"]))
4199
4809
  }, { additionalProperties: false }), nil),
4200
4810
  abort: rpc(nil, nil),
4201
4811
  usage: rpc(nil, usage),
4202
- 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),
4203
4815
  configure: rpc(change, nil),
4204
- compact: rpc(Type4.Object({ instructions: Type4.Optional(str) }, { additionalProperties: false }), nil),
4205
- importImage: rpc(str, Type4.Object({ id: str, name: str })),
4206
- draftImage: rpc(Type4.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
4207
- beginImage: rpc(Type4.Object({ name: str, mimeType: str, size: Type4.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
4208
- 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),
4209
4821
  finishImage: rpc(str, nil),
4210
4822
  releaseImage: rpc(str, nil),
4211
- 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)
4212
4824
  }
4213
4825
  }
4214
4826
  }
4215
4827
  });
4216
- var browse = Type4.Object({ path: str, query: Type4.Optional(str), showHidden: Type4.Optional(bool), cursor: Type4.Optional(str) }, { additionalProperties: false });
4217
- 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({
4218
4830
  path: str,
4219
4831
  parent: nullableString,
4220
- 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) })),
4221
4833
  next: nullableString
4222
4834
  });
4223
4835
  var NekoWorkspaces = contract("neko.workspaces", {
4224
4836
  version: 1,
4225
- input: Type4.Object({}, { additionalProperties: false }),
4837
+ input: Type5.Object({}, { additionalProperties: false }),
4226
4838
  variants: {
4227
4839
  ready: {
4228
4840
  state: { state: WorkspaceStateSchema },
4229
4841
  methods: {
4230
- add: rpc(Type4.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
4842
+ add: rpc(Type5.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
4231
4843
  browse: rpc(browse, page)
4232
4844
  }
4233
4845
  }
4234
4846
  }
4235
4847
  });
4236
- var provider = Type4.Enum(["deepseek", "tavily"]);
4848
+ var provider = Type5.Enum(["deepseek", "tavily"]);
4237
4849
  var NekoCredentials = contract("neko.credentials", {
4238
4850
  version: 1,
4239
- input: Type4.Object({}, { additionalProperties: false }),
4851
+ input: Type5.Object({}, { additionalProperties: false }),
4240
4852
  variants: {
4241
4853
  ready: {
4242
4854
  state: { state: CredentialStateSchema },
4243
4855
  methods: {
4244
- save: rpc(Type4.Object({ provider, key: Type4.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
4245
- 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)
4246
4858
  }
4247
4859
  }
4248
4860
  }
4249
4861
  });
4250
- var FileCandidatesSchema = Type4.Object({
4251
- 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 }),
4252
4864
  limited: bool,
4253
4865
  incomplete: bool
4254
4866
  });
4255
4867
  var NekoWorkspaceFiles = contract("neko.workspace-files", {
4256
4868
  version: 1,
4257
- input: Type4.Object({}, { additionalProperties: false }),
4869
+ input: Type5.Object({}, { additionalProperties: false }),
4258
4870
  variants: {
4259
4871
  ready: {
4260
4872
  methods: {
4261
- search: rpc(Type4.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
4873
+ search: rpc(Type5.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
4262
4874
  }
4263
4875
  }
4264
4876
  }
4265
4877
  });
4266
- var marketSkill = Type4.Object({
4878
+ var marketSkill = Type5.Object({
4267
4879
  name: str,
4268
4880
  description: str,
4269
4881
  category: str,
@@ -4273,38 +4885,38 @@ var marketSkill = Type4.Object({
4273
4885
  sha256: str,
4274
4886
  sizeBytes: integer,
4275
4887
  uploadedAt: str,
4276
- installed: Type4.Object({ project: bool, user: bool, builtin: bool }),
4277
- activeLevel: Type4.Union([level, nil])
4888
+ installed: Type5.Object({ project: bool, user: bool, builtin: bool }),
4889
+ activeLevel: Type5.Union([level, nil])
4278
4890
  });
4279
- var preview = Type4.Object({ body: str, files: Type4.Array(str) });
4280
- var scope = Type4.Enum(["user", "project"]);
4281
- 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) });
4282
4894
  var NekoSkillMarket = contract("neko.skill-market", {
4283
4895
  version: 1,
4284
- input: Type4.Object({}, { additionalProperties: false }),
4896
+ input: Type5.Object({}, { additionalProperties: false }),
4285
4897
  variants: {
4286
4898
  ready: {
4287
4899
  methods: {
4288
- list: rpc(Type4.Object({ query: Type4.Optional(str), workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), Type4.Array(marketSkill)),
4289
- detail: rpc(Type4.Object({ name: str, workspaceRoot: Type4.Optional(str) }, { additionalProperties: false }), marketSkill),
4290
- preview: rpc(Type4.Object({ name: str }, { additionalProperties: false }), preview),
4291
- install: rpc(Type4.Object({ name: str, scope, workspaceRoot: Type4.Optional(str), overwrite: Type4.Optional(bool) }, { additionalProperties: false }), installResult),
4292
- 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)
4293
4905
  }
4294
4906
  }
4295
4907
  }
4296
4908
  });
4297
- var ApplicationInfoSchema = Type4.Object({
4909
+ var ApplicationInfoSchema = Type5.Object({
4298
4910
  runningVersion: str,
4299
4911
  installedVersion: nullableString,
4300
- protocolVersion: Type4.Integer({ minimum: 1 }),
4912
+ protocolVersion: Type5.Integer({ minimum: 1 }),
4301
4913
  latestVersion: nullableString,
4302
- checkedAt: Type4.Union([integer, nil]),
4303
- checkState: Type4.Enum(["unknown", "checking", "ok", "failed", "development"])
4914
+ checkedAt: Type5.Union([integer, nil]),
4915
+ checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"])
4304
4916
  });
4305
4917
  var NekoApplicationInfo = contract("neko.application-info", {
4306
4918
  version: 1,
4307
- input: Type4.Object({}),
4919
+ input: Type5.Object({}),
4308
4920
  variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
4309
4921
  });
4310
4922
 
@@ -4320,9 +4932,9 @@ import {
4320
4932
  opaque as opaque2,
4321
4933
  resource as resource2
4322
4934
  } from "@earendil-works/chord";
4323
- import { withAbortSignal as withAbortSignal3 } from "@earendil-works/chord/context";
4935
+ import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
4324
4936
  import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
4325
- import { Type as Type6 } from "typebox";
4937
+ import { Type as Type7 } from "typebox";
4326
4938
 
4327
4939
  // packages/gateway/src/image-uploads.ts
4328
4940
  var IMAGE_CHUNK_BYTES = 256 * 1024;
@@ -4429,7 +5041,7 @@ import {
4429
5041
  resource
4430
5042
  } from "@earendil-works/chord";
4431
5043
  import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
4432
- import { Type as Type5 } from "typebox";
5044
+ import { Type as Type6 } from "typebox";
4433
5045
  async function openReader(runtime, childId, parentId, context, through, historyUsage) {
4434
5046
  const child = await runtime.harness.conversation(childId, context);
4435
5047
  if (!child)
@@ -4554,15 +5166,15 @@ async function openReader(runtime, childId, parentId, context, through, historyU
4554
5166
  }
4555
5167
  var ChildProjection = local("neko.subagent-projection", {
4556
5168
  version: 1,
4557
- input: Type5.Object({ parentId: Type5.Integer(), agentId: Type5.String() }),
5169
+ input: Type6.Object({ parentId: Type6.Integer(), agentId: Type6.String() }),
4558
5170
  members: {
4559
- revision: Type5.Integer(),
5171
+ revision: Type6.Integer(),
4560
5172
  reader: opaque("neko.subagent-reader@1")
4561
5173
  }
4562
5174
  });
4563
5175
  function installSubagentHost(host, runtime, context) {
4564
5176
  host.serve(ChildProjection, (input, _principal, self) => {
4565
- const { revision } = self.state({ revision: cell(Type5.Integer(), 0) });
5177
+ const { revision } = self.state({ revision: cell(Type6.Integer(), 0) });
4566
5178
  const { reader } = self.resources({
4567
5179
  reader: resource({
4568
5180
  rev: 1,
@@ -4604,7 +5216,7 @@ function installSubagentHost(host, runtime, context) {
4604
5216
  };
4605
5217
  const { target } = self.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
4606
5218
  const shared = self.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
4607
- const { authorizedRevision } = self.state({ authorizedRevision: cell(Type5.Integer(), 0) });
5219
+ const { authorizedRevision } = self.state({ authorizedRevision: cell(Type6.Integer(), 0) });
4608
5220
  self.listen({
4609
5221
  authorizedView: listener(shared.revision, async (value, ctx) => {
4610
5222
  try {
@@ -4659,7 +5271,7 @@ function installSubagentHost(host, runtime, context) {
4659
5271
  }
4660
5272
 
4661
5273
  // packages/gateway/src/session-host.ts
4662
- var OwnerInput = Type6.Object({});
5274
+ var OwnerInput = Type7.Object({});
4663
5275
  var Owner = local2("neko.attachment-owner", {
4664
5276
  version: 1,
4665
5277
  input: OwnerInput,
@@ -4667,7 +5279,7 @@ var Owner = local2("neko.attachment-owner", {
4667
5279
  });
4668
5280
  var Projection = local2("neko.projection", {
4669
5281
  version: 1,
4670
- input: Type6.Object({ id: Type6.String() }),
5282
+ input: Type7.Object({ id: Type7.String() }),
4671
5283
  members: {
4672
5284
  view: ChatViewSchema,
4673
5285
  reader: opaque2("neko.projection-reader@1")
@@ -4683,6 +5295,8 @@ var emptyView = {
4683
5295
  items: [],
4684
5296
  streaming: null
4685
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;
4686
5300
  async function openProjection(runtime, session, context) {
4687
5301
  const view = await session.viewState();
4688
5302
  const history = new Map;
@@ -4698,7 +5312,7 @@ async function openProjection(runtime, session, context) {
4698
5312
  const stopMetadata = runtime.onDirectoryChange(() => changed?.(null));
4699
5313
  let childIds = new Set;
4700
5314
  const stopChildren = runtime.harness.subscribeCommits(({ changes }) => {
4701
- 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))))
4702
5316
  changed?.(null);
4703
5317
  });
4704
5318
  return {
@@ -4724,6 +5338,7 @@ async function openProjection(runtime, session, context) {
4724
5338
  }, naming)
4725
5339
  }
4726
5340
  }, "", { models: runtime.listModels(), contextEntries: view.value.entries, compaction });
5341
+ const jobs = (await session.jobs()).map(clipJob);
4727
5342
  const subagents = await listDelegations(runtime.harness, session.id, context);
4728
5343
  const approvals = await runtime.approvals.list(session.id, context);
4729
5344
  for (const entry of subagents)
@@ -4738,6 +5353,7 @@ async function openProjection(runtime, session, context) {
4738
5353
  const u = totalUsage(aggregate);
4739
5354
  return {
4740
5355
  ...projected,
5356
+ jobs,
4741
5357
  agents: summarizeSubagents(subagents, (await runtime.harness.snapshot(SubagentsDoc, session.id, context))?.items),
4742
5358
  subagents: subagents.map(({ usageStart: _start, usageEnd: _end, ...entry }) => ({
4743
5359
  ...entry,
@@ -4909,7 +5525,7 @@ function serveSessions(host, runtime, context) {
4909
5525
  getImage: async (input, ctx) => {
4910
5526
  if (!Number.isSafeInteger(input.offset) || input.offset < 0 || !Number.isSafeInteger(input.contentIndex) || input.contentIndex < 0)
4911
5527
  throw new ChordError2("invalid", "Invalid image range");
4912
- 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));
4913
5529
  const content = entry?.model?.[0]?.content;
4914
5530
  const image = Array.isArray(content) ? content[input.contentIndex] : undefined;
4915
5531
  if (image?.type !== "image")
@@ -4946,6 +5562,21 @@ function serveSessions(host, runtime, context) {
4946
5562
  return null;
4947
5563
  },
4948
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
+ },
4949
5580
  usage: async () => {
4950
5581
  const usage = totalUsage(await projection.value.session.usage());
4951
5582
  return {
@@ -5680,12 +6311,12 @@ async function listDrives() {
5680
6311
  // packages/gateway/src/server-services.ts
5681
6312
  var ServerState = local3("neko.server-state", {
5682
6313
  version: 1,
5683
- input: Type7.Object({}),
6314
+ input: Type8.Object({}),
5684
6315
  members: {
5685
6316
  directory: DirectoryStateSchema,
5686
6317
  workspaces: WorkspaceStateSchema,
5687
6318
  credentials: CredentialStateSchema,
5688
- refreshCredentials: method2(Type7.Null(), Type7.Null())
6319
+ refreshCredentials: method2(Type8.Null(), Type8.Null())
5689
6320
  }
5690
6321
  });
5691
6322
  async function createNekoServerServices(options) {