neko-harness 0.4.2 → 0.4.4

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
@@ -16503,7 +16503,8 @@ var GATEWAY_USAGE = `\u7528\u6CD5: neko serve [\u9009\u9879]
16503
16503
  --config <dir> neko.config.json \u67E5\u627E\u8D77\u59CB\u76EE\u5F55\uFF08\u9ED8\u8BA4\u5F53\u524D\u76EE\u5F55\uFF09
16504
16504
  -h, --help \u663E\u793A\u6B64\u5E2E\u52A9
16505
16505
 
16506
- \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY \u914D\u7F6E\u51ED\u636E\u3002`;
16506
+ \u53EF\u901A\u8FC7\u524D\u7AEF\u8BBE\u7F6E\u6216 DEEPSEEK_API_KEY / TAVILY_API_KEY \u914D\u7F6E\u51ED\u636E\uFF1B
16507
+ \u8054\u7F51\u641C\u7D22\u670D\u52A1\u53EF\u5728\u524D\u7AEF\u8BBE\u7F6E\uFF0C\u6216\u7528\u914D\u7F6E\u6587\u4EF6 webSearch.provider\uFF08tavily / deepseek\uFF0C\u9ED8\u8BA4 tavily\uFF09\u3002`;
16507
16508
  function parseGatewayArgs(argv) {
16508
16509
  let port;
16509
16510
  const options = {};
@@ -16594,13 +16595,186 @@ function parseGatewayArgs(argv) {
16594
16595
  import { randomUUID as randomUUID5 } from "crypto";
16595
16596
  import { mkdir as mkdir6, readFile as readFile7, writeFile as writeFile5 } from "fs/promises";
16596
16597
  import { basename as basename4, dirname as dirname9, join as join13 } from "path";
16597
- import { createHost as createHost3 } from "@earendil-works/chord";
16598
+ import { createHost as createHost2 } from "@earendil-works/chord";
16598
16599
  import { BACKGROUND_CONTEXT as BACKGROUND_CONTEXT4 } from "@earendil-works/chord/context";
16599
16600
 
16600
16601
  // packages/core/src/config.ts
16601
16602
  import { existsSync, readFileSync } from "fs";
16602
16603
  import { homedir } from "os";
16603
16604
  import { dirname, join, resolve } from "path";
16605
+
16606
+ // packages/core/src/context-settings.ts
16607
+ import { DEFAULT_COMPACTION_POLICY } from "@earendil-works/pi-durable";
16608
+
16609
+ // packages/core/src/context-limits.ts
16610
+ var PERCENT = { kind: "int", min: 1, max: 99 };
16611
+ var CONTEXT_FIELD_LIMITS = {
16612
+ compaction: {
16613
+ enabled: { kind: "boolean" },
16614
+ triggerPercent: PERCENT,
16615
+ keepRecentTokens: { kind: "int", min: 1, max: 1e8 }
16616
+ },
16617
+ pruning: {
16618
+ enabled: { kind: "boolean" },
16619
+ triggerPercent: PERCENT,
16620
+ keepRecent: { kind: "int", min: 0, max: 1e5 },
16621
+ minChars: { kind: "int", min: 1, max: 1e8 },
16622
+ minSavePercent: PERCENT
16623
+ }
16624
+ };
16625
+ var PRUNING_DEFAULTS = {
16626
+ enabled: false,
16627
+ triggerPercent: 35,
16628
+ keepRecent: 10,
16629
+ minChars: 4000,
16630
+ minSavePercent: 5
16631
+ };
16632
+
16633
+ // packages/core/src/context-settings.ts
16634
+ var emptySavedContextSettings = () => ({
16635
+ compaction: { enabled: null, triggerPercent: null, keepRecentTokens: null },
16636
+ pruning: { enabled: null, triggerPercent: null, keepRecent: null, minChars: null, minSavePercent: null }
16637
+ });
16638
+ var SCHEMA = CONTEXT_FIELD_LIMITS;
16639
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
16640
+ function describe(field) {
16641
+ return field.kind === "boolean" ? "a boolean" : `an integer between ${field.min} and ${field.max}`;
16642
+ }
16643
+ function validateGroups(value, prefix, allowNull) {
16644
+ if (!isObject(value))
16645
+ throw new Error(`"${prefix}" must be an object.`);
16646
+ const out = {};
16647
+ for (const key of Object.keys(value)) {
16648
+ if (!(key in SCHEMA))
16649
+ throw new Error(`"${prefix}.${key}" is not a known setting.`);
16650
+ }
16651
+ for (const [group, fields] of Object.entries(SCHEMA)) {
16652
+ const given = value[group];
16653
+ if (given === undefined)
16654
+ continue;
16655
+ if (!isObject(given))
16656
+ throw new Error(`"${prefix}.${group}" must be an object.`);
16657
+ const target = {};
16658
+ for (const key of Object.keys(given)) {
16659
+ if (!(key in fields))
16660
+ throw new Error(`"${prefix}.${group}.${key}" is not a known setting.`);
16661
+ }
16662
+ for (const [name, field] of Object.entries(fields)) {
16663
+ const item = given[name];
16664
+ if (item === undefined)
16665
+ continue;
16666
+ if (item === null && allowNull) {
16667
+ target[name] = null;
16668
+ continue;
16669
+ }
16670
+ const ok = field.kind === "boolean" ? typeof item === "boolean" : typeof item === "number" && Number.isInteger(item) && item >= field.min && item <= field.max;
16671
+ if (!ok)
16672
+ throw new Error(`"${prefix}.${group}.${name}" must be ${describe(field)}.`);
16673
+ target[name] = item;
16674
+ }
16675
+ out[group] = target;
16676
+ }
16677
+ return out;
16678
+ }
16679
+ function parseContextSettings(value, prefix = "context") {
16680
+ return validateGroups(value, prefix, false);
16681
+ }
16682
+ function parseContextSettingsPatch(value) {
16683
+ return validateGroups(value, "context", true);
16684
+ }
16685
+ function applyContextPatch(saved, patch) {
16686
+ const next = {
16687
+ compaction: { ...saved.compaction },
16688
+ pruning: { ...saved.pruning }
16689
+ };
16690
+ for (const [key, value] of Object.entries(patch.compaction ?? {})) {
16691
+ if (value !== undefined)
16692
+ next.compaction[key] = value;
16693
+ }
16694
+ for (const [key, value] of Object.entries(patch.pruning ?? {})) {
16695
+ if (value !== undefined)
16696
+ next.pruning[key] = value;
16697
+ }
16698
+ return next;
16699
+ }
16700
+ function sanitizeSavedContextSettings(raw) {
16701
+ const saved = emptySavedContextSettings();
16702
+ if (!isObject(raw))
16703
+ return saved;
16704
+ for (const [group, fields] of Object.entries(SCHEMA)) {
16705
+ const stored = raw[group];
16706
+ if (!isObject(stored))
16707
+ continue;
16708
+ for (const [name, field] of Object.entries(fields)) {
16709
+ const item = stored[name];
16710
+ const ok = field.kind === "boolean" ? typeof item === "boolean" : typeof item === "number" && Number.isInteger(item) && item >= field.min && item <= field.max;
16711
+ if (ok)
16712
+ saved[group][name] = item;
16713
+ }
16714
+ }
16715
+ return saved;
16716
+ }
16717
+ function pick(saved, config, fallback) {
16718
+ if (saved !== null && saved !== undefined)
16719
+ return { value: saved, source: "saved" };
16720
+ if (config !== undefined)
16721
+ return { value: config, source: "config" };
16722
+ return { value: fallback, source: "default" };
16723
+ }
16724
+ function resolveContextSettings(config, saved) {
16725
+ const cEnabled = pick(saved.compaction.enabled, config?.compaction?.enabled, DEFAULT_COMPACTION_POLICY.enabled);
16726
+ const cTrigger = pick(saved.compaction.triggerPercent, config?.compaction?.triggerPercent, null);
16727
+ const cKeep = pick(saved.compaction.keepRecentTokens, config?.compaction?.keepRecentTokens, DEFAULT_COMPACTION_POLICY.keepRecentTokens);
16728
+ const pEnabled = pick(saved.pruning.enabled, config?.pruning?.enabled, PRUNING_DEFAULTS.enabled);
16729
+ const pTrigger = pick(saved.pruning.triggerPercent, config?.pruning?.triggerPercent, PRUNING_DEFAULTS.triggerPercent);
16730
+ const pKeep = pick(saved.pruning.keepRecent, config?.pruning?.keepRecent, PRUNING_DEFAULTS.keepRecent);
16731
+ const pMin = pick(saved.pruning.minChars, config?.pruning?.minChars, PRUNING_DEFAULTS.minChars);
16732
+ const pSave = pick(saved.pruning.minSavePercent, config?.pruning?.minSavePercent, PRUNING_DEFAULTS.minSavePercent);
16733
+ return {
16734
+ effective: {
16735
+ compaction: { enabled: cEnabled.value, triggerPercent: cTrigger.value, keepRecentTokens: cKeep.value },
16736
+ pruning: {
16737
+ enabled: pEnabled.value,
16738
+ triggerPercent: pTrigger.value,
16739
+ keepRecent: pKeep.value,
16740
+ minChars: pMin.value,
16741
+ minSavePercent: pSave.value
16742
+ }
16743
+ },
16744
+ sources: {
16745
+ compaction: { enabled: cEnabled.source, triggerPercent: cTrigger.source, keepRecentTokens: cKeep.source },
16746
+ pruning: {
16747
+ enabled: pEnabled.source,
16748
+ triggerPercent: pTrigger.source,
16749
+ keepRecent: pKeep.source,
16750
+ minChars: pMin.source,
16751
+ minSavePercent: pSave.source
16752
+ }
16753
+ }
16754
+ };
16755
+ }
16756
+ function compactionPolicy(state, windows) {
16757
+ const { effective, sources } = state;
16758
+ const policy = {};
16759
+ if (sources.compaction.enabled !== "default")
16760
+ policy.enabled = effective.compaction.enabled;
16761
+ if (sources.compaction.keepRecentTokens !== "default")
16762
+ policy.keepRecentTokens = effective.compaction.keepRecentTokens;
16763
+ const percent = effective.compaction.triggerPercent;
16764
+ const window2 = windows.length > 0 ? Math.min(...windows) : 0;
16765
+ if (percent !== null && window2 > 0)
16766
+ policy.reserveTokens = window2 - Math.floor(window2 * percent / 100);
16767
+ return policy;
16768
+ }
16769
+
16770
+ // packages/core/src/search-providers.ts
16771
+ var SEARCH_PROVIDERS = ["tavily", "deepseek"];
16772
+ var DEFAULT_SEARCH_PROVIDER = "tavily";
16773
+ function isSearchProvider(value) {
16774
+ return typeof value === "string" && SEARCH_PROVIDERS.includes(value);
16775
+ }
16776
+
16777
+ // packages/core/src/config.ts
16604
16778
  var DEFAULT_SKILL_MARKET_URL = "https://skill.kirineko.tech";
16605
16779
  var NEKO_CONFIG_BASENAME = "neko.config.json";
16606
16780
  function findNekoConfigFile(startDir = process.cwd()) {
@@ -16714,6 +16888,23 @@ function validate(config, source) {
16714
16888
  throw new Error(`neko config: ${source}: "skills.marketUrl" must be a string.`);
16715
16889
  }
16716
16890
  }
16891
+ const webSearch = config.webSearch;
16892
+ if (webSearch !== undefined) {
16893
+ if (typeof webSearch !== "object" || webSearch === null || Array.isArray(webSearch)) {
16894
+ throw new Error(`neko config: ${source}: "webSearch" must be an object.`);
16895
+ }
16896
+ const { provider } = webSearch;
16897
+ if (provider !== undefined && !isSearchProvider(provider)) {
16898
+ throw new Error(`neko config: ${source}: "webSearch.provider" must be one of ${SEARCH_PROVIDERS.map((item) => `"${item}"`).join(", ")}.`);
16899
+ }
16900
+ }
16901
+ if (config.context !== undefined) {
16902
+ try {
16903
+ parseContextSettings(config.context);
16904
+ } catch (error) {
16905
+ throw new Error(`neko config: ${source}: ${error instanceof Error ? error.message : String(error)}`);
16906
+ }
16907
+ }
16717
16908
  }
16718
16909
  // packages/core/src/credentials.ts
16719
16910
  import { chmod, mkdir, open, readFile, rename, unlink } from "fs/promises";
@@ -17065,6 +17256,50 @@ function savedDefaultImagePrompt(content, value) {
17065
17256
  import { defineEntry } from "@earendil-works/pi-durable";
17066
17257
  var NekoInput = defineEntry("app.neko.input");
17067
17258
 
17259
+ // packages/core/src/queued.ts
17260
+ var QUEUED_PREVIEW_CHARS = 300;
17261
+ var textOf = (content) => {
17262
+ if (typeof content === "string")
17263
+ return content;
17264
+ if (!Array.isArray(content))
17265
+ return "";
17266
+ return content.flatMap((b) => b && b.type === "text" && typeof b.text === "string" ? [b.text] : []).join("");
17267
+ };
17268
+ var clip = (text) => {
17269
+ const chars = [...text.replace(/\s+/g, " ").trim()];
17270
+ return chars.length > QUEUED_PREVIEW_CHARS ? `${chars.slice(0, QUEUED_PREVIEW_CHARS - 1).join("")}\u2026` : chars.join("");
17271
+ };
17272
+ function queuedUserInput(raw) {
17273
+ const item = raw;
17274
+ if (item.mode !== "steer" && item.mode !== "followUp")
17275
+ return;
17276
+ if (item.entry?.kind !== NekoInput.kind)
17277
+ return;
17278
+ const data = item.entry.data;
17279
+ if (!data || data.jobReport || data.subagentRoundId)
17280
+ return;
17281
+ const defaultPrompt = Array.isArray(item.content) ? savedDefaultImagePrompt(item.content, data.defaultImagePrompt) : undefined;
17282
+ const content = defaultPrompt && Array.isArray(item.content) ? item.content.slice(1) : item.content;
17283
+ const skill = data.skill ? { name: data.skill.name, args: data.skill.args } : undefined;
17284
+ return {
17285
+ id: item.id,
17286
+ mode: item.mode,
17287
+ text: skill ? `/skill:${skill.name}${skill.args ? ` ${skill.args}` : ""}` : textOf(content),
17288
+ images: data.images?.length ?? 0,
17289
+ ...skill ? { skill } : {}
17290
+ };
17291
+ }
17292
+ function projectQueued(view) {
17293
+ const items = view.docs["pi.inbox"]?.items ?? [];
17294
+ return items.flatMap((raw) => {
17295
+ const input = queuedUserInput(raw);
17296
+ if (!input)
17297
+ return [];
17298
+ const { text, ...rest } = input;
17299
+ return [{ ...rest, preview: clip(text) }];
17300
+ });
17301
+ }
17302
+
17068
17303
  // packages/core/src/workspace.ts
17069
17304
  import { accessSync, constants, realpathSync, statSync } from "fs";
17070
17305
  import { isAbsolute, resolve as resolve2 } from "path";
@@ -17385,6 +17620,7 @@ function projectChatView(view, modelLabel, options = {}) {
17385
17620
  },
17386
17621
  cwd: savedWorkspace(agent?.cwd),
17387
17622
  busy: live.run !== undefined,
17623
+ queued: projectQueued(view),
17388
17624
  status,
17389
17625
  usage: {
17390
17626
  input: usage.input,
@@ -17530,6 +17766,7 @@ import {
17530
17766
  createRegistry,
17531
17767
  defineDoc as defineDoc5,
17532
17768
  Harness,
17769
+ InboxDoc as InboxDoc2,
17533
17770
  LiveDoc as importLiveDoc,
17534
17771
  MemoryStorage,
17535
17772
  UsageDoc as UsageDoc2,
@@ -17728,6 +17965,134 @@ async function readCompaction(harness, storage, id, context) {
17728
17965
  return { taskId: task.id, phase, at: task.endedAt ?? task.startedAt ?? 0 };
17729
17966
  }
17730
17967
 
17968
+ // packages/core/src/context-pruning.ts
17969
+ var PRUNE_ENTRY_KIND = "app.neko.context-prune";
17970
+ var TOKENS_PER_LATIN_CHAR = 0.3;
17971
+ var TOKENS_PER_CJK_CHAR = 0.6;
17972
+ var CJK = /[\u3000-\u9fff\uac00-\ud7af\uf900-\ufaff\uff00-\uffef]/g;
17973
+ function estimateTokens(text) {
17974
+ const cjk = text.match(CJK)?.length ?? 0;
17975
+ return cjk * TOKENS_PER_CJK_CHAR + (text.length - cjk) * TOKENS_PER_LATIN_CHAR;
17976
+ }
17977
+ var isToolResult = (entry) => {
17978
+ const message = entry.model?.[0];
17979
+ return entry.kind === "pi.tool-result" && message?.role === "toolResult";
17980
+ };
17981
+ var resultTexts = (message) => message.content.flatMap((block) => block.type === "text" ? [block.text] : []);
17982
+ function toolResultChars(message) {
17983
+ return resultTexts(message).reduce((total, text) => total + text.length, 0);
17984
+ }
17985
+ function toolResultTokens(message) {
17986
+ return resultTexts(message).reduce((total, text) => total + estimateTokens(text), 0);
17987
+ }
17988
+ function prunedPlaceholder(message, chars) {
17989
+ const { details: _details, ...rest } = message;
17990
+ return {
17991
+ ...rest,
17992
+ content: [
17993
+ {
17994
+ type: "text",
17995
+ text: `[Output of ${message.toolName} omitted from context to save space (${chars} characters). Run the tool again if the content is needed.]`
17996
+ }
17997
+ ]
17998
+ };
17999
+ }
18000
+ function planPrune(entries, settings) {
18001
+ const replaced = new Set;
18002
+ for (const entry of entries)
18003
+ for (const edit of entry.edits ?? [])
18004
+ replaced.add(edit.target);
18005
+ const results = entries.filter(isToolResult);
18006
+ const candidates = results.slice(0, Math.max(0, results.length - settings.keepRecent));
18007
+ const edits = [];
18008
+ const targets = [];
18009
+ let savedChars = 0;
18010
+ let savedTokens = 0;
18011
+ for (const entry of candidates) {
18012
+ if (replaced.has(entry.id))
18013
+ continue;
18014
+ const message = entry.model[0];
18015
+ const chars = toolResultChars(message);
18016
+ if (chars < settings.minChars)
18017
+ continue;
18018
+ const replacement = prunedPlaceholder(message, chars);
18019
+ edits.push({ target: entry.id, action: "replace", messages: [replacement] });
18020
+ targets.push(entry.id);
18021
+ savedChars += chars;
18022
+ savedTokens += toolResultTokens(message);
18023
+ }
18024
+ return edits.length === 0 ? undefined : { edits, targets, savedChars, savedTokens };
18025
+ }
18026
+
18027
+ class ContextPruner {
18028
+ #options;
18029
+ #unsubscribe;
18030
+ #tail = Promise.resolve();
18031
+ #closed = false;
18032
+ constructor(options) {
18033
+ this.#options = options;
18034
+ this.#unsubscribe = options.harness.subscribeCommits(({ changes }) => {
18035
+ if (!options.settings().enabled)
18036
+ return;
18037
+ for (const change of changes) {
18038
+ if (change.type !== "entry" || change.value.kind !== "pi.assistant")
18039
+ continue;
18040
+ const message = change.value.model?.[0];
18041
+ if (message?.role !== "assistant" || message.stopReason === "aborted" || message.stopReason === "error")
18042
+ continue;
18043
+ const usage = message.usage;
18044
+ const tokens = usage.totalTokens || usage.input + usage.output + usage.cacheRead + usage.cacheWrite;
18045
+ if (tokens <= 0)
18046
+ continue;
18047
+ const { conversationId, id } = change.value;
18048
+ this.#enqueue(() => this.#evaluate(conversationId, id, tokens));
18049
+ }
18050
+ });
18051
+ }
18052
+ async flush() {
18053
+ await this.#tail;
18054
+ }
18055
+ close() {
18056
+ this.#closed = true;
18057
+ this.#unsubscribe();
18058
+ }
18059
+ #enqueue(work) {
18060
+ if (this.#closed)
18061
+ return;
18062
+ this.#tail = this.#tail.then(work).catch((error) => {
18063
+ if (!this.#closed)
18064
+ this.#options.report(error);
18065
+ });
18066
+ }
18067
+ async#evaluate(conversationId, entryId, tokens) {
18068
+ const { harness, context } = this.#options;
18069
+ const settings = this.#options.settings();
18070
+ if (!settings.enabled)
18071
+ return;
18072
+ const window2 = await this.#options.windowOf(conversationId);
18073
+ if (window2 <= 0 || tokens / window2 * 100 < settings.triggerPercent)
18074
+ return;
18075
+ const conversation = await harness.conversation(conversationId, context);
18076
+ if (!conversation)
18077
+ return;
18078
+ const view = await conversation.context(context);
18079
+ const plan = planPrune(view.entries, settings);
18080
+ if (!plan)
18081
+ return;
18082
+ if (plan.savedTokens / window2 * 100 < settings.minSavePercent)
18083
+ return;
18084
+ await conversation.submit({
18085
+ type: "write",
18086
+ requestId: `prune:${entryId}`,
18087
+ entry: {
18088
+ kind: PRUNE_ENTRY_KIND,
18089
+ data: { targets: [...plan.targets], savedChars: plan.savedChars },
18090
+ edits: plan.edits
18091
+ }
18092
+ }, context);
18093
+ }
18094
+ }
18095
+
17731
18096
  // packages/core/src/image-metadata.ts
17732
18097
  import { defineExtension as defineExtension2, hook, ToolTask } from "@earendil-works/pi-durable";
17733
18098
  import sharp from "sharp";
@@ -18387,7 +18752,7 @@ var statusWord = {
18387
18752
  stopped: "stopped",
18388
18753
  lost: "lost"
18389
18754
  };
18390
- var describe = (job) => {
18755
+ var describe2 = (job) => {
18391
18756
  const parts = [statusWord[job.status]];
18392
18757
  if (job.exitCode !== null)
18393
18758
  parts.push(`exit code ${job.exitCode}`);
@@ -18402,7 +18767,7 @@ var describe = (job) => {
18402
18767
  function reportText(job, tail) {
18403
18768
  const body = reportTail(tail);
18404
18769
  return [
18405
- `[Background job ${job.jobId} "${job.title}" ${describe(job)}]`,
18770
+ `[Background job ${job.jobId} "${job.title}" ${describe2(job)}]`,
18406
18771
  `Command: ${job.command}`,
18407
18772
  body ? `Last output (use the job tool's output action with jobId "${job.jobId}" for more):
18408
18773
  ${body}` : "It produced no output."
@@ -18607,7 +18972,7 @@ class JobManager {
18607
18972
  if (kind === "stop") {
18608
18973
  const jobId = stringField(args.jobId, "jobId");
18609
18974
  const job = await this.stop(io, api.conversationId, jobId, "model", context);
18610
- return this.#text(`Job ${job.jobId} ${describe(job)}.`);
18975
+ return this.#text(`Job ${job.jobId} ${describe2(job)}.`);
18611
18976
  }
18612
18977
  if (kind !== "start")
18613
18978
  throw new Error("Invalid job arguments: action must be start, output, stop or list.");
@@ -18635,7 +19000,7 @@ class JobManager {
18635
19000
  #listText(items) {
18636
19001
  if (!items.length)
18637
19002
  return "No jobs.";
18638
- return items.map((job) => `${job.jobId} [${describe(job)}] "${job.title}" \u2014 ${job.command}`).join(`
19003
+ return items.map((job) => `${job.jobId} [${describe2(job)}] "${job.title}" \u2014 ${job.command}`).join(`
18639
19004
  `);
18640
19005
  }
18641
19006
  async start(io, conversationId, cwd, args, context) {
@@ -18687,7 +19052,7 @@ class JobManager {
18687
19052
  return this.#render(job, read.text, read.offset, read.dropped);
18688
19053
  }
18689
19054
  #render(job, text, offset, dropped) {
18690
- const lines = [`Job ${job.jobId} ${describe(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
19055
+ const lines = [`Job ${job.jobId} ${describe2(job)}.`, `Title: ${job.title}`, `Command: ${job.command}`];
18691
19056
  if (dropped > 0)
18692
19057
  lines.push(`(${dropped} bytes of earlier output were dropped)`);
18693
19058
  lines.push(text ? `Output (next offset ${offset}):
@@ -19096,6 +19461,14 @@ var createResourceSnapshots = (load, submission) => {
19096
19461
  discard(id, requestId) {
19097
19462
  conversations.get(id)?.pending.delete(requestId);
19098
19463
  },
19464
+ move(id, from, to) {
19465
+ const pending = conversations.get(id)?.pending;
19466
+ const snapshot = pending?.get(from);
19467
+ if (!pending || !snapshot)
19468
+ return;
19469
+ pending.delete(from);
19470
+ pending.set(to, snapshot);
19471
+ },
19099
19472
  forget(id) {
19100
19473
  conversations.delete(id);
19101
19474
  }
@@ -19621,33 +19994,32 @@ async function prepareApprovalConversations(harness, ids, extension, context) {
19621
19994
  }
19622
19995
  }
19623
19996
 
19624
- // packages/core/src/web-search.ts
19625
- import { Type as Type4 } from "@earendil-works/pi-ai";
19626
- import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
19627
- var SEARCH_URL = "https://api.tavily.com/search";
19628
- var EXTRACT_URL = "https://api.tavily.com/extract";
19997
+ // packages/core/src/web-search/http.ts
19629
19998
  var REQUEST_TIMEOUT_MS = 30000;
19630
19999
  var MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
19631
- var MAX_URLS = 20;
19632
- var MAX_RESULTS = 10;
19633
- var DEFAULT_RESULTS = 5;
19634
- var MAX_SOURCE_CHARS = 2000;
19635
- var MAX_TOTAL_CHARS = 12000;
19636
20000
 
19637
- class TavilyError extends Error {
20001
+ class WebToolError extends Error {
20002
+ status;
20003
+ constructor(message, status) {
20004
+ super(message);
20005
+ this.status = status;
20006
+ }
19638
20007
  }
19639
20008
  function redact(text, key) {
19640
20009
  for (const secret of key ? [key, encodeURIComponent(key)] : [])
19641
20010
  text = text.replaceAll(secret, "[redacted]");
19642
- return text.replace(/tvly-[a-zA-Z0-9_-]+/g, "[redacted]");
20011
+ return text.replace(/\b(?:tvly|sk)-[a-zA-Z0-9_-]+/g, "[redacted]");
19643
20012
  }
19644
20013
  function record(value) {
19645
20014
  return typeof value === "object" && value !== null && !Array.isArray(value);
19646
20015
  }
19647
- async function readBounded(response, signal) {
20016
+ function truncate(text, max) {
20017
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
20018
+ }
20019
+ async function readBounded(response, signal, label) {
19648
20020
  if (Number(response.headers.get("content-length")) > MAX_RESPONSE_BYTES) {
19649
20021
  await response.body?.cancel();
19650
- throw new TavilyError("Tavily response exceeds the 2 MiB limit.");
20022
+ throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
19651
20023
  }
19652
20024
  if (!response.body)
19653
20025
  return "";
@@ -19668,7 +20040,7 @@ async function readBounded(response, signal) {
19668
20040
  return text + decoder.decode();
19669
20041
  size += chunk.value.byteLength;
19670
20042
  if (size > MAX_RESPONSE_BYTES)
19671
- throw new TavilyError("Tavily response exceeds the 2 MiB limit.");
20043
+ throw new WebToolError(`${label} response exceeds the 2 MiB limit.`);
19672
20044
  text += decoder.decode(chunk.value, { stream: true });
19673
20045
  }
19674
20046
  } finally {
@@ -19677,7 +20049,8 @@ async function readBounded(response, signal) {
19677
20049
  reader.releaseLock();
19678
20050
  }
19679
20051
  }
19680
- async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
20052
+ async function postJson(options) {
20053
+ const { label, context } = options;
19681
20054
  let key;
19682
20055
  const signal = AbortSignal.any([
19683
20056
  AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -19685,68 +20058,188 @@ async function tavilyPost(url, body, resolveKey, fetchImpl, context) {
19685
20058
  ]);
19686
20059
  try {
19687
20060
  signal.throwIfAborted();
19688
- key = await resolveKey();
20061
+ key = await options.resolveKey();
19689
20062
  signal.throwIfAborted();
19690
- const headers = { "content-type": "application/json" };
19691
- if (key)
19692
- headers.authorization = `Bearer ${key}`;
19693
- else
19694
- headers["x-tavily-access-mode"] = "keyless";
19695
- const response = await fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal });
19696
- const text = redact(await readBounded(response, signal), key);
20063
+ const response = await options.fetchImpl(options.url, {
20064
+ method: "POST",
20065
+ headers: { "content-type": "application/json", ...options.headers(key) },
20066
+ body: JSON.stringify(options.body),
20067
+ signal
20068
+ });
20069
+ const text = redact(await readBounded(response, signal, label), key);
19697
20070
  if (!response.ok)
19698
- throw new TavilyError(`Tavily request failed (${response.status}): ${truncate(text, 500)}${key ? "" : " (keyless rate limits apply; save a Tavily API key in settings for higher limits)"}`);
19699
- const data = JSON.parse(text);
19700
- if (!record(data))
19701
- throw new TavilyError("Invalid Tavily response.");
19702
- if (data.error)
19703
- throw new TavilyError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
19704
- return data;
20071
+ throw options.httpError(response.status, text, key);
20072
+ return JSON.parse(text);
19705
20073
  } catch (error) {
19706
20074
  if (context.abortSignal?.aborted)
19707
- throw new TavilyError("Web request cancelled.");
20075
+ throw new WebToolError("Web request cancelled.");
19708
20076
  if (signal.aborted)
19709
- throw new TavilyError("Tavily request timed out. Please retry.");
19710
- if (error instanceof TavilyError)
20077
+ throw new WebToolError(`${label} request timed out. Please retry.`);
20078
+ if (error instanceof WebToolError)
19711
20079
  throw error;
19712
- throw new TavilyError("Tavily request failed. Check the connection and credentials, then retry.");
20080
+ throw new WebToolError(`${label} request failed. Check the connection and credentials, then retry.`);
19713
20081
  }
19714
20082
  }
20083
+
20084
+ // packages/core/src/web-search/deepseek.ts
20085
+ var SEARCH_URL = "https://api.deepseek.com/v1/tools/search";
20086
+ var FETCH_URL = "https://api.deepseek.com/v1/tools/fetch";
20087
+ var DEEPSEEK_QUERY_MAX_CHARS = 128;
20088
+ var FETCH_CONCURRENCY = 3;
20089
+ var MAX_RETRIES = 2;
20090
+ var BACKOFF_MS = [250, 750];
20091
+ var DEEPSEEK_MISSING_KEY = "DeepSeek API key is not configured. Save one with /login or in the Web settings, or switch the search provider to Tavily.";
20092
+ function defaultSleep(ms, signal) {
20093
+ return new Promise((resolve, reject) => {
20094
+ if (signal?.aborted)
20095
+ return reject(signal.reason);
20096
+ const timer = setTimeout(resolve, ms);
20097
+ signal?.addEventListener("abort", () => {
20098
+ clearTimeout(timer);
20099
+ reject(signal.reason);
20100
+ }, { once: true });
20101
+ });
20102
+ }
20103
+ function apiMessage(text) {
20104
+ try {
20105
+ const data = JSON.parse(text);
20106
+ if (record(data) && record(data.error) && typeof data.error.message === "string")
20107
+ return truncate(data.error.message, 300);
20108
+ } catch {}
20109
+ return truncate(text, 300);
20110
+ }
20111
+ function httpError(status, text) {
20112
+ const guidance = "Check the key in /login or the Web settings, or switch the search provider.";
20113
+ if (status === 401)
20114
+ return new WebToolError(`DeepSeek API key is missing or invalid. ${guidance}`, status);
20115
+ if (status === 402)
20116
+ return new WebToolError("DeepSeek account balance is insufficient. Recharge, or switch the search provider to Tavily.", status);
20117
+ if (status === 403)
20118
+ return new WebToolError("DeepSeek blocked this request by its security policy.", status);
20119
+ if (status === 404)
20120
+ return new WebToolError("DeepSeek could not fetch a usable page for this URL.", status);
20121
+ if (status === 429)
20122
+ return new WebToolError("DeepSeek rate limit reached. Retry shortly.", status);
20123
+ if (status === 408 || status >= 500)
20124
+ return new WebToolError(`DeepSeek service error (${status}). Retry shortly.`, status);
20125
+ return new WebToolError(`DeepSeek request failed (${status}): ${apiMessage(text)}`, status);
20126
+ }
20127
+ var retryable = (status) => status !== undefined && (status === 429 || status === 408 || status >= 500);
20128
+ var wholeCall = (status) => status === 401 || status === 402;
20129
+ function dateOnly(value) {
20130
+ return typeof value === "string" && /^\d{4}-\d{2}-\d{2}/.test(value) ? value.slice(0, 10) : undefined;
20131
+ }
20132
+ function createDeepseekProvider(resolveKey, fetchImpl = fetch, options = {}) {
20133
+ const sleep = options.sleep ?? defaultSleep;
20134
+ const requireKey = async () => {
20135
+ const key = await resolveKey();
20136
+ if (!key)
20137
+ throw new WebToolError(DEEPSEEK_MISSING_KEY);
20138
+ return key;
20139
+ };
20140
+ const post = (url, body, context) => postJson({
20141
+ label: "DeepSeek",
20142
+ url,
20143
+ body,
20144
+ fetchImpl,
20145
+ context,
20146
+ resolveKey: requireKey,
20147
+ headers: (key) => ({ authorization: `Bearer ${key}` }),
20148
+ httpError
20149
+ });
20150
+ return {
20151
+ id: "deepseek",
20152
+ async search(query, limit, context) {
20153
+ if (!query.trim())
20154
+ throw new WebToolError("Provide a non-empty query.");
20155
+ if ([...query].length > DEEPSEEK_QUERY_MAX_CHARS)
20156
+ throw new WebToolError(`DeepSeek search queries are limited to ${DEEPSEEK_QUERY_MAX_CHARS} characters. Shorten the query and retry.`);
20157
+ await requireKey();
20158
+ const data = await post(SEARCH_URL, { query, count: limit, need_content: false }, context);
20159
+ if (!record(data) || !Array.isArray(data.results) || !data.results.every(record))
20160
+ throw new WebToolError("Invalid DeepSeek response.");
20161
+ return data.results.slice(0, limit).map((item) => {
20162
+ const date = dateOnly(item.date);
20163
+ return {
20164
+ title: String(item.title ?? "(untitled)"),
20165
+ url: String(item.url ?? ""),
20166
+ text: String(item.summary || item.snippet || ""),
20167
+ ...date ? { date } : {}
20168
+ };
20169
+ });
20170
+ },
20171
+ async extract(urls, context) {
20172
+ await requireKey();
20173
+ const outcomes = new Array(urls.length);
20174
+ let next = 0;
20175
+ let fatal;
20176
+ const fetchOne = async (url) => {
20177
+ for (let attempt = 0;; attempt++) {
20178
+ try {
20179
+ const data = await post(FETCH_URL, { url }, context);
20180
+ if (!record(data) || typeof data.content !== "string")
20181
+ throw new WebToolError("Invalid DeepSeek response.");
20182
+ return { url, text: data.content };
20183
+ } catch (error) {
20184
+ const failure = error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
20185
+ if (context.abortSignal?.aborted || wholeCall(failure.status))
20186
+ throw failure;
20187
+ if (!retryable(failure.status) || attempt >= MAX_RETRIES)
20188
+ return { url, error: failure.message };
20189
+ try {
20190
+ await sleep(BACKOFF_MS[attempt] ?? 750, context.abortSignal);
20191
+ } catch {
20192
+ throw new WebToolError("Web request cancelled.");
20193
+ }
20194
+ }
20195
+ }
20196
+ };
20197
+ const worker = async () => {
20198
+ while (!fatal) {
20199
+ const index = next++;
20200
+ if (index >= urls.length)
20201
+ return;
20202
+ try {
20203
+ outcomes[index] = await fetchOne(urls[index]);
20204
+ } catch (error) {
20205
+ fatal ??= error instanceof WebToolError ? error : new WebToolError("DeepSeek request failed.");
20206
+ }
20207
+ }
20208
+ };
20209
+ await Promise.all(Array.from({ length: Math.min(FETCH_CONCURRENCY, urls.length) }, worker));
20210
+ if (fatal)
20211
+ throw fatal;
20212
+ return outcomes;
20213
+ }
20214
+ };
20215
+ }
20216
+
20217
+ // packages/core/src/web-search/index.ts
20218
+ import { Type as Type4 } from "@earendil-works/pi-ai";
20219
+ import { defineExtension as defineExtension8, defineTool as defineTool4 } from "@earendil-works/pi-durable";
20220
+ var MAX_URLS = 20;
20221
+ var MAX_RESULTS = 10;
20222
+ var DEFAULT_RESULTS = 5;
20223
+ var MAX_SOURCE_CHARS = 2000;
20224
+ var MAX_TOTAL_CHARS = 12000;
19715
20225
  function toolError(error) {
19716
20226
  return {
19717
20227
  output: [
19718
- { type: "text", text: error instanceof TavilyError ? error.message : "Web tool failed. Please retry." }
20228
+ { type: "text", text: error instanceof WebToolError ? error.message : "Web tool failed. Please retry." }
19719
20229
  ],
19720
20230
  isError: true
19721
20231
  };
19722
20232
  }
19723
- function truncate(text, max) {
19724
- return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}\u2026` : text;
19725
- }
19726
- function rows(data, field) {
19727
- const value = data[field];
19728
- if (!Array.isArray(value) || !value.every(record))
19729
- throw new TavilyError("Invalid Tavily response.");
19730
- return value;
19731
- }
19732
20233
  function content(text) {
19733
20234
  return { output: [{ type: "text", text }] };
19734
20235
  }
19735
- function urlKey(value) {
19736
- const text = String(value);
19737
- try {
19738
- return new URL(text).href;
19739
- } catch {
19740
- return text;
19741
- }
19742
- }
19743
- function createWebSearchTools(resolveKey, fetchImpl = fetch) {
20236
+ function createWebSearchTools(resolveProvider) {
19744
20237
  return defineExtension8({
19745
20238
  name: "neko-web-search",
19746
20239
  tools: [
19747
20240
  defineTool4({
19748
20241
  name: "web_search",
19749
- 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.",
20242
+ description: "Search the live web for current or unknown information. Returns ranked sources (title, URL, bounded excerpts). Keep the query short and focused (ideally under 128 characters). Use only when up-to-date or external information is needed; use web_extract to read a known URL.",
19750
20243
  parameters: Type4.Object({
19751
20244
  query: Type4.String({ minLength: 1, maxLength: 4096, description: "The search query" }),
19752
20245
  maxResults: Type4.Optional(Type4.Number({ description: `Maximum number of results (1-${MAX_RESULTS}, default ${DEFAULT_RESULTS})` }))
@@ -19754,18 +20247,19 @@ function createWebSearchTools(resolveKey, fetchImpl = fetch) {
19754
20247
  async execute({ query, maxResults }, _api, context) {
19755
20248
  try {
19756
20249
  if (!query.trim() || query.length > 4096)
19757
- throw new TavilyError("Provide a non-empty query of at most 4096 characters.");
20250
+ throw new WebToolError("Provide a non-empty query of at most 4096 characters.");
19758
20251
  if (maxResults !== undefined && !Number.isFinite(maxResults))
19759
- throw new TavilyError("maxResults must be a finite number.");
20252
+ throw new WebToolError("maxResults must be a finite number.");
19760
20253
  const limit = Math.max(1, Math.min(MAX_RESULTS, Math.trunc(maxResults ?? DEFAULT_RESULTS)));
19761
- const data = await tavilyPost(SEARCH_URL, { query, max_results: limit, search_depth: "basic" }, resolveKey, fetchImpl, context);
19762
- const results = rows(data, "results").slice(0, limit);
20254
+ const provider = await resolveProvider(context);
20255
+ const results = (await provider.search(query, limit, context)).slice(0, limit);
19763
20256
  const budget = Math.floor(MAX_TOTAL_CHARS / Math.max(1, results.length)) - 2;
19764
20257
  return content(results.map((item, index) => {
19765
- const header = `${index + 1}. ${truncate(String(item.title ?? "(untitled)"), 200)}
19766
- ${truncate(String(item.url ?? ""), 300)}
19767
- `;
19768
- return header + truncate(String(item.content ?? ""), Math.min(MAX_SOURCE_CHARS, budget - header.length));
20258
+ const header = `${index + 1}. ${truncate(item.title, 200)}
20259
+ ${truncate(item.url, 300)}
20260
+ ${item.date ? `Published: ${item.date}
20261
+ ` : ""}`;
20262
+ return header + truncate(item.text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
19769
20263
  }).join(`
19770
20264
 
19771
20265
  `) || "No results.");
@@ -19787,28 +20281,25 @@ ${truncate(String(item.url ?? ""), 300)}
19787
20281
  async execute({ urls }, _api, context) {
19788
20282
  try {
19789
20283
  if (!urls.length || urls.length > MAX_URLS)
19790
- throw new TavilyError(`Provide between 1 and ${MAX_URLS} URLs.`);
20284
+ throw new WebToolError(`Provide between 1 and ${MAX_URLS} URLs.`);
19791
20285
  for (const url of urls) {
19792
20286
  let parsed;
19793
20287
  try {
19794
20288
  parsed = new URL(url);
19795
20289
  } catch {
19796
- throw new TavilyError("Provide valid http(s) URLs.");
20290
+ throw new WebToolError("Provide valid http(s) URLs.");
19797
20291
  }
19798
20292
  if (url.length > 2048 || !["http:", "https:"].includes(parsed.protocol) || parsed.username || parsed.password)
19799
- throw new TavilyError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
20293
+ throw new WebToolError("Provide http(s) URLs of at most 2048 characters without embedded credentials.");
19800
20294
  }
19801
- const data = await tavilyPost(EXTRACT_URL, { urls, format: "markdown" }, resolveKey, fetchImpl, context);
19802
- const results = rows(data, "results");
19803
- const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
19804
- const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
19805
- const successes = byUrl(results), failures = byUrl(failed);
20295
+ const provider = await resolveProvider(context);
20296
+ const outcomes = await provider.extract(urls, context);
19806
20297
  const budget = Math.floor(MAX_TOTAL_CHARS / urls.length) - 2;
19807
20298
  return content(urls.map((url, index) => {
19808
20299
  const header = `## ${index + 1}. ${truncate(url, 300)}
19809
20300
  `;
19810
- const result = successes.get(urlKey(url)), failure = failures.get(urlKey(url));
19811
- const text = result ? String(result.raw_content ?? "") : `Extraction failed: ${String(failure?.error ?? "No result returned for this URL")}`;
20301
+ const outcome = outcomes[index];
20302
+ const text = outcome && "text" in outcome ? outcome.text : `Extraction failed: ${outcome?.error ?? "No result returned for this URL"}`;
19812
20303
  return header + truncate(text, Math.min(MAX_SOURCE_CHARS, budget - header.length));
19813
20304
  }).join(`
19814
20305
 
@@ -19822,6 +20313,69 @@ ${truncate(String(item.url ?? ""), 300)}
19822
20313
  });
19823
20314
  }
19824
20315
 
20316
+ // packages/core/src/web-search/tavily.ts
20317
+ var SEARCH_URL2 = "https://api.tavily.com/search";
20318
+ var EXTRACT_URL = "https://api.tavily.com/extract";
20319
+ function rows(data, field) {
20320
+ const value = data[field];
20321
+ if (!Array.isArray(value) || !value.every(record))
20322
+ throw new WebToolError("Invalid Tavily response.");
20323
+ return value;
20324
+ }
20325
+ function urlKey(value) {
20326
+ const text = String(value);
20327
+ try {
20328
+ return new URL(text).href;
20329
+ } catch {
20330
+ return text;
20331
+ }
20332
+ }
20333
+ function createTavilyProvider(resolveKey, fetchImpl = fetch) {
20334
+ const post = async (url, body, context) => {
20335
+ const data = await postJson({
20336
+ label: "Tavily",
20337
+ url,
20338
+ body,
20339
+ fetchImpl,
20340
+ context,
20341
+ resolveKey,
20342
+ headers: (key) => key ? { authorization: `Bearer ${key}` } : { "x-tavily-access-mode": "keyless" },
20343
+ httpError: (status, text, key) => new WebToolError(`Tavily request failed (${status}): ${truncate(text, 500)}${key ? "" : " (keyless rate limits apply; save a Tavily API key in settings for higher limits)"}`, status)
20344
+ });
20345
+ if (!record(data))
20346
+ throw new WebToolError("Invalid Tavily response.");
20347
+ if (data.error)
20348
+ throw new WebToolError(`Tavily request failed: ${truncate(JSON.stringify(data.error), 500)}`);
20349
+ return data;
20350
+ };
20351
+ return {
20352
+ id: "tavily",
20353
+ async search(query, limit, context) {
20354
+ const data = await post(SEARCH_URL2, { query, max_results: limit, search_depth: "basic" }, context);
20355
+ return rows(data, "results").slice(0, limit).map((item) => ({
20356
+ title: String(item.title ?? "(untitled)"),
20357
+ url: String(item.url ?? ""),
20358
+ text: String(item.content ?? "")
20359
+ }));
20360
+ },
20361
+ async extract(urls, context) {
20362
+ const data = await post(EXTRACT_URL, { urls, format: "markdown" }, context);
20363
+ const results = rows(data, "results");
20364
+ const failed = data.failed_results === undefined ? [] : rows(data, "failed_results");
20365
+ const byUrl = (items) => new Map(items.map((item) => [urlKey(item.url), item]));
20366
+ const successes = byUrl(results);
20367
+ const failures = byUrl(failed);
20368
+ return urls.map((url) => {
20369
+ const result = successes.get(urlKey(url));
20370
+ if (result)
20371
+ return { url, text: String(result.raw_content ?? "") };
20372
+ const failure = failures.get(urlKey(url));
20373
+ return { url, error: String(failure?.error ?? "No result returned for this URL") };
20374
+ });
20375
+ }
20376
+ };
20377
+ }
20378
+
19825
20379
  // packages/core/src/workspace-files.ts
19826
20380
  import { realpath as realpath2, stat } from "fs/promises";
19827
20381
  import { basename as basename2, isAbsolute as isAbsolute2, relative, resolve as resolve3, sep } from "path";
@@ -20201,6 +20755,22 @@ var WorkspacesDoc = defineDoc5({
20201
20755
  fork: "initial",
20202
20756
  initial: () => ({ items: [] })
20203
20757
  });
20758
+ var SearchSettingsDoc = defineDoc5({
20759
+ kind: "app.neko.search-settings",
20760
+ version: 1,
20761
+ scope: "conversation",
20762
+ history: "latest",
20763
+ fork: "initial",
20764
+ initial: () => ({ provider: null })
20765
+ });
20766
+ var ContextSettingsDoc = defineDoc5({
20767
+ kind: "app.neko.context-settings",
20768
+ version: 1,
20769
+ scope: "conversation",
20770
+ history: "latest",
20771
+ fork: "initial",
20772
+ initial: emptySavedContextSettings
20773
+ });
20204
20774
  var ActivityDoc = defineDoc5({
20205
20775
  kind: "app.neko.activity",
20206
20776
  version: 1,
@@ -20247,12 +20817,21 @@ async function createNekoRuntime(options = {}) {
20247
20817
  registry.install(createCodingTools({ images: await createNodePhotonImages() }));
20248
20818
  registry.install(ImageDetails);
20249
20819
  registry.install(AgentFileSearch);
20250
- registry.install(createWebSearchTools(async () => {
20820
+ const tavily = createTavilyProvider(async () => {
20251
20821
  const saved = await effectiveCredentials.read("tavily");
20252
20822
  if (saved?.type === "api_key" && saved.key)
20253
20823
  return saved.key;
20254
20824
  return process.env.TAVILY_API_KEY || undefined;
20255
- }));
20825
+ });
20826
+ const deepseekApiKey = async () => {
20827
+ const saved = await effectiveCredentials.read("deepseek");
20828
+ if (saved?.type === "api_key" && saved.key)
20829
+ return saved.key;
20830
+ return process.env.DEEPSEEK_API_KEY || undefined;
20831
+ };
20832
+ const deepseek = createDeepseekProvider(deepseekApiKey);
20833
+ let searchProvider = options.searchProvider ?? DEFAULT_SEARCH_PROVIDER;
20834
+ registry.install(createWebSearchTools(async () => searchProvider === "deepseek" ? deepseek : tavily));
20256
20835
  const loadResources = createResourceLoader(options.promptHome);
20257
20836
  const resourceSnapshots = createResourceSnapshots(loadResources, (id, requestId) => storage.submissionByRequest(id, requestId, context));
20258
20837
  registry.install(createNekoPrompt(resourceSnapshots.current));
@@ -20266,9 +20845,16 @@ async function createNekoRuntime(options = {}) {
20266
20845
  let approvals;
20267
20846
  const approvalExtension = createApprovalExtension(() => approvals);
20268
20847
  registry.install(approvalExtension);
20848
+ let savedContext = emptySavedContextSettings();
20849
+ let contextState = resolveContextSettings(options.contextSettings, savedContext);
20269
20850
  const harness = await Harness.open(storage, {
20270
20851
  models,
20271
20852
  registry,
20853
+ settings: {
20854
+ get compaction() {
20855
+ return compactionPolicy(contextState, models.getModels().map((model) => model.contextWindow));
20856
+ }
20857
+ },
20272
20858
  onReport: options.onReport,
20273
20859
  conversationCreated: async (tx, record) => {
20274
20860
  (await tx.doc(SessionMetadataDoc, record.id)).createdAt = Date.now();
@@ -20310,6 +20896,21 @@ async function createNekoRuntime(options = {}) {
20310
20896
  cwd: defaultCwd
20311
20897
  }
20312
20898
  });
20899
+ const savedSearch = (await harness.snapshot(SearchSettingsDoc, conversation.id, context))?.provider;
20900
+ if (isSearchProvider(savedSearch))
20901
+ searchProvider = savedSearch;
20902
+ savedContext = sanitizeSavedContextSettings(await harness.snapshot(ContextSettingsDoc, conversation.id, context));
20903
+ contextState = resolveContextSettings(options.contextSettings, savedContext);
20904
+ const persistContext = async (next) => {
20905
+ await harness.commit(async (tx) => {
20906
+ const doc = await tx.doc(ContextSettingsDoc, conversation.id);
20907
+ doc.compaction = next.compaction;
20908
+ doc.pruning = next.pruning;
20909
+ }, context);
20910
+ savedContext = next;
20911
+ contextState = resolveContextSettings(options.contextSettings, savedContext);
20912
+ return contextState;
20913
+ };
20313
20914
  const registerWorkspace = async (path) => {
20314
20915
  await harness.commit(async (tx) => {
20315
20916
  const doc = await tx.doc(WorkspacesDoc, conversation.id);
@@ -20346,10 +20947,22 @@ async function createNekoRuntime(options = {}) {
20346
20947
  for (const record of existing)
20347
20948
  if (!record.owner && !(await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
20348
20949
  await metadata.restore(record.id);
20950
+ const pruner = new ContextPruner({
20951
+ harness,
20952
+ context,
20953
+ settings: () => contextState.effective.pruning,
20954
+ windowOf: async (id) => {
20955
+ const target = await harness.conversation(id, context);
20956
+ const model = (await target?.agent(context))?.model;
20957
+ return model && models.getModel(model.provider, model.modelId)?.contextWindow || 0;
20958
+ },
20959
+ report: (error) => options.onReport?.(error)
20960
+ });
20349
20961
  let closing;
20350
20962
  const cleanup = () => {
20351
20963
  if (!closing)
20352
20964
  closing = (async () => {
20965
+ pruner.close();
20353
20966
  jobs.shutdown();
20354
20967
  await workspaceFiles.close();
20355
20968
  await metadata.close();
@@ -20506,6 +21119,57 @@ async function createNekoRuntime(options = {}) {
20506
21119
  },
20507
21120
  abort() {
20508
21121
  return target.abort(context);
21122
+ },
21123
+ async withdraw(submissionId) {
21124
+ const id = submissionId;
21125
+ const inbox = await harness.snapshot(InboxDoc2, target.id, context);
21126
+ const item = inbox?.items.find((i) => i.id === id);
21127
+ const input = item ? queuedUserInput(item) : undefined;
21128
+ if (item && !input)
21129
+ return { result: "unknown" };
21130
+ const requestId = (await (await harness.submission(id, context))?.status(context))?.requestId;
21131
+ const outcome = await harness.abortSubmission(id, context, target.id);
21132
+ if (outcome !== "aborted") {
21133
+ return { result: outcome === "already_placed" ? "placed" : outcome === "settled" ? "settled" : "unknown" };
21134
+ }
21135
+ if (requestId !== undefined)
21136
+ resourceSnapshots.discard(target.id, requestId);
21137
+ await metadata.reconcile(target.id);
21138
+ return { result: "withdrawn", ...input ? { text: input.text, images: input.images } : {} };
21139
+ },
21140
+ async steer(submissionId) {
21141
+ const id = submissionId;
21142
+ const inbox = await harness.snapshot(InboxDoc2, target.id, context);
21143
+ const item = inbox?.items.find((i) => i.id === id);
21144
+ const input = item ? queuedUserInput(item) : undefined;
21145
+ if (item && (!input || item.mode === "write"))
21146
+ return { result: "unknown" };
21147
+ if (item && input?.mode === "steer")
21148
+ return { result: "steered", id: submissionId };
21149
+ const requestId = (await (await harness.submission(id, context))?.status(context))?.requestId;
21150
+ const outcome = await harness.abortSubmission(id, context, target.id);
21151
+ if (outcome !== "aborted" || !item || !input) {
21152
+ return { result: outcome === "already_placed" ? "placed" : outcome === "settled" ? "settled" : "unknown" };
21153
+ }
21154
+ const steerRequestId = requestId !== undefined ? `steer:${requestId}` : crypto.randomUUID();
21155
+ try {
21156
+ const next = await target.submit({
21157
+ type: "input",
21158
+ content: item.content,
21159
+ ...item.entry ? { entry: item.entry } : {},
21160
+ requestId: steerRequestId,
21161
+ whenBusy: "steer"
21162
+ }, context);
21163
+ if (requestId !== undefined)
21164
+ resourceSnapshots.move(target.id, requestId, steerRequestId);
21165
+ await metadata.reconcile(target.id);
21166
+ return { result: "steered", id: next.id };
21167
+ } catch {
21168
+ if (requestId !== undefined)
21169
+ resourceSnapshots.discard(target.id, requestId);
21170
+ await metadata.reconcile(target.id);
21171
+ return { result: "withdrawn", text: input.text, images: input.images };
21172
+ }
20509
21173
  }
20510
21174
  });
20511
21175
  const rootSession = makeSession(conversation);
@@ -20716,8 +21380,28 @@ async function createNekoRuntime(options = {}) {
20716
21380
  source: runtimeKeys.has(spec.id) ? "runtime" : savedConfigured ? "saved" : environmentConfigured ? "environment" : spec.keyless ? "keyless" : "none"
20717
21381
  };
20718
21382
  },
20719
- async saveCredential(provider, key) {
20720
- const spec = credentialProvider(provider);
21383
+ async contextSettings() {
21384
+ return contextState;
21385
+ },
21386
+ async setContextSettings(patch) {
21387
+ return persistContext(applyContextPatch(savedContext, parseContextSettingsPatch(patch)));
21388
+ },
21389
+ async resetContextSettings() {
21390
+ return persistContext(emptySavedContextSettings());
21391
+ },
21392
+ async searchSettings() {
21393
+ return { selected: searchProvider };
21394
+ },
21395
+ async setSearchProvider(provider) {
21396
+ if (!isSearchProvider(provider))
21397
+ throw new Error("Unknown search provider.");
21398
+ await harness.commit(async (tx) => {
21399
+ (await tx.doc(SearchSettingsDoc, conversation.id)).provider = provider;
21400
+ }, context);
21401
+ searchProvider = provider;
21402
+ },
21403
+ async saveCredential(provider, key) {
21404
+ const spec = credentialProvider(provider);
20721
21405
  if (typeof key !== "string" || !key.trim() || key.length > 4096 || Array.from(key).some((char) => char.charCodeAt(0) < 32))
20722
21406
  throw new Error("Enter a valid API key.");
20723
21407
  try {
@@ -20789,506 +21473,34 @@ async function createNekoRuntime(options = {}) {
20789
21473
  }
20790
21474
  };
20791
21475
  }
20792
- // packages/gateway/src/identity.ts
20793
- function isServerId(value) {
20794
- return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
20795
- }
20796
-
20797
- // packages/gateway/src/mobile/admin.ts
20798
- import { createHash, timingSafeEqual } from "crypto";
20799
- import { ChordError, method, remote } from "@earendil-works/chord";
21476
+ // packages/gateway/src/contract.ts
21477
+ import { hostNamed, method, remote } from "@earendil-works/chord";
20800
21478
  import { Type as Type5 } from "typebox";
20801
-
20802
- // packages/gateway/src/mobile/store.ts
20803
- import { randomBytes } from "crypto";
20804
- import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, writeFile } from "fs/promises";
20805
- import { join as join6 } from "path";
20806
- var MOBILE_STATE_FORMAT_VERSION = 1;
20807
- var MOBILE_DEFAULT_PORT = 8323;
20808
- var MOBILE_ADMIN_TOKEN_BYTES = 32;
20809
- var POSIX = process.platform !== "win32";
20810
- var ADMIN_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
20811
- var CREDENTIAL_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
20812
- function defaultState() {
20813
- return {
20814
- formatVersion: MOBILE_STATE_FORMAT_VERSION,
20815
- enabled: false,
20816
- address: null,
20817
- port: MOBILE_DEFAULT_PORT,
20818
- devices: [],
20819
- receipts: [],
20820
- lastConnectedAddress: null
20821
- };
20822
- }
20823
- function freezeState(state) {
20824
- for (const device of state.devices)
20825
- Object.freeze(device);
20826
- Object.freeze(state.devices);
20827
- for (const receipt of state.receipts)
20828
- Object.freeze(receipt);
20829
- Object.freeze(state.receipts);
20830
- return Object.freeze(state);
20831
- }
20832
- function isNotFound(error) {
20833
- return error?.code === "ENOENT";
20834
- }
20835
- function invalidState(statePath, detail) {
20836
- return new Error(`Invalid mobile state file ${statePath}: ${detail}`);
20837
- }
20838
- function readString(value, what, statePath) {
20839
- if (typeof value !== "string")
20840
- throw invalidState(statePath, `${what} must be a string`);
20841
- return value;
20842
- }
20843
- function readNonEmptyString(value, what, statePath) {
20844
- const text = readString(value, what, statePath);
20845
- if (text === "")
20846
- throw invalidState(statePath, `${what} must be a non-empty string`);
20847
- return text;
20848
- }
20849
- function readCredentialHash(value, what, statePath) {
20850
- const text = readString(value, what, statePath);
20851
- if (!CREDENTIAL_HASH_PATTERN.test(text))
20852
- throw invalidState(statePath, `${what} must be a 64-character hex string`);
20853
- return text;
20854
- }
20855
- function readRevision(value, what, statePath) {
20856
- if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
20857
- throw invalidState(statePath, `${what} must be a non-negative integer`);
20858
- return value;
20859
- }
20860
- function readTimestamp(value, what, statePath) {
20861
- if (typeof value !== "number" || !Number.isFinite(value))
20862
- throw invalidState(statePath, `${what} must be a finite number`);
20863
- return value;
20864
- }
20865
- function parseDevice(raw, index, statePath) {
20866
- const what = `devices[${index}]`;
20867
- if (typeof raw !== "object" || raw === null)
20868
- throw invalidState(statePath, `${what} must be an object`);
20869
- const record = raw;
20870
- return {
20871
- deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
20872
- name: readString(record.name, `${what}.name`, statePath),
20873
- credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
20874
- grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
20875
- pairedAt: readTimestamp(record.pairedAt, `${what}.pairedAt`, statePath),
20876
- lastSeenAt: readTimestamp(record.lastSeenAt, `${what}.lastSeenAt`, statePath)
20877
- };
20878
- }
20879
- function parseReceipt(raw, index, statePath) {
20880
- const what = `receipts[${index}]`;
20881
- if (typeof raw !== "object" || raw === null)
20882
- throw invalidState(statePath, `${what} must be an object`);
20883
- const record = raw;
20884
- return {
20885
- pairRequestId: readNonEmptyString(record.pairRequestId, `${what}.pairRequestId`, statePath),
20886
- deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
20887
- credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
20888
- grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
20889
- expiresAt: readTimestamp(record.expiresAt, `${what}.expiresAt`, statePath)
20890
- };
20891
- }
20892
- function parseState(raw, statePath) {
20893
- if (typeof raw !== "object" || raw === null)
20894
- throw invalidState(statePath, "root must be an object");
20895
- const record = raw;
20896
- if (record.formatVersion !== MOBILE_STATE_FORMAT_VERSION)
20897
- throw invalidState(statePath, `unsupported formatVersion (expected ${MOBILE_STATE_FORMAT_VERSION})`);
20898
- if (typeof record.enabled !== "boolean")
20899
- throw invalidState(statePath, "enabled must be a boolean");
20900
- if (record.address !== null && typeof record.address !== "string")
20901
- throw invalidState(statePath, "address must be a string or null");
20902
- const port = record.port;
20903
- if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)
20904
- throw invalidState(statePath, "port must be an integer between 1 and 65535");
20905
- if (!Array.isArray(record.devices))
20906
- throw invalidState(statePath, "devices must be an array");
20907
- if (!Array.isArray(record.receipts))
20908
- throw invalidState(statePath, "receipts must be an array");
20909
- const lastConnectedAddress = record.lastConnectedAddress === undefined || record.lastConnectedAddress === null ? null : readString(record.lastConnectedAddress, "lastConnectedAddress", statePath);
20910
- return {
20911
- formatVersion: MOBILE_STATE_FORMAT_VERSION,
20912
- enabled: record.enabled,
20913
- address: record.address,
20914
- port,
20915
- devices: record.devices.map((device, index) => parseDevice(device, index, statePath)),
20916
- receipts: record.receipts.map((receipt, index) => parseReceipt(receipt, index, statePath)),
20917
- lastConnectedAddress
20918
- };
20919
- }
20920
- function assertPort(port) {
20921
- if (!Number.isInteger(port) || port < 1 || port > 65535)
20922
- throw new Error("port must be an integer between 1 and 65535");
20923
- return port;
20924
- }
20925
- var tempCounter = 0;
20926
- async function writeFileAtomic(filePath, contents, mode) {
20927
- const tempPath = `${filePath}.tmp-${process.pid}-${++tempCounter}`;
20928
- try {
20929
- await writeFile(tempPath, contents, { mode });
20930
- if (POSIX)
20931
- await chmod2(tempPath, mode);
20932
- await rename2(tempPath, filePath);
20933
- } finally {
20934
- await rm(tempPath, { force: true });
20935
- }
20936
- }
20937
- async function writeSecretFile(filePath, contents) {
20938
- await writeFileAtomic(filePath, contents, 384);
20939
- }
20940
- async function readSecretFile(filePath) {
20941
- try {
20942
- return await readFile4(filePath, "utf8");
20943
- } catch (error) {
20944
- if (isNotFound(error))
20945
- return;
20946
- throw error;
20947
- }
20948
- }
20949
- async function loadState(statePath) {
20950
- let raw;
20951
- try {
20952
- raw = await readFile4(statePath, "utf8");
20953
- } catch (error) {
20954
- if (isNotFound(error))
20955
- return freezeState(defaultState());
20956
- throw error;
20957
- }
20958
- let parsed;
20959
- try {
20960
- parsed = JSON.parse(raw);
20961
- } catch {
20962
- throw invalidState(statePath, "not valid JSON");
20963
- }
20964
- return freezeState(parseState(parsed, statePath));
20965
- }
20966
-
20967
- class MobileStore {
20968
- mobileDir;
20969
- tlsDir;
20970
- statePath;
20971
- adminTokenPath;
20972
- current;
20973
- queue = Promise.resolve();
20974
- constructor(mobileDir, initial) {
20975
- this.mobileDir = mobileDir;
20976
- this.tlsDir = join6(mobileDir, "tls");
20977
- this.statePath = join6(mobileDir, "state.json");
20978
- this.adminTokenPath = join6(mobileDir, "admin-token");
20979
- this.current = initial;
20980
- }
20981
- static async open(nekoDir) {
20982
- const mobileDir = join6(nekoDir, "mobile");
20983
- const tlsDir = join6(mobileDir, "tls");
20984
- await mkdir2(mobileDir, { recursive: true, mode: 448 });
20985
- await mkdir2(tlsDir, { recursive: true, mode: 448 });
20986
- if (POSIX) {
20987
- await chmod2(mobileDir, 448);
20988
- await chmod2(tlsDir, 448);
20989
- }
20990
- return new MobileStore(mobileDir, await loadState(join6(mobileDir, "state.json")));
20991
- }
20992
- getState() {
20993
- return this.current;
20994
- }
20995
- updateConfig(patch) {
20996
- return this.mutate((state) => ({
20997
- ...state,
20998
- ...patch.enabled !== undefined ? { enabled: patch.enabled } : {},
20999
- ...patch.address !== undefined ? { address: patch.address } : {},
21000
- ...patch.port !== undefined ? { port: assertPort(patch.port) } : {}
21001
- }));
21002
- }
21003
- upsertDevice(device) {
21004
- return this.mutate((state) => ({
21005
- ...state,
21006
- devices: state.devices.some((d) => d.deviceId === device.deviceId) ? state.devices.map((d) => d.deviceId === device.deviceId ? device : d) : [...state.devices, device]
21007
- }));
21008
- }
21009
- removeDevice(deviceId) {
21010
- return this.mutate((state) => ({
21011
- ...state,
21012
- devices: state.devices.filter((d) => d.deviceId !== deviceId)
21013
- }));
21014
- }
21015
- completePairing(input) {
21016
- return this.mutate((state) => {
21017
- const now = Date.now();
21018
- const existing = state.devices.find((d) => d.deviceId === input.deviceId);
21019
- const grantRevision = existing === undefined ? 1 : existing.grantRevision + 1;
21020
- const device = {
21021
- deviceId: input.deviceId,
21022
- name: input.name,
21023
- credentialHash: input.credentialHash,
21024
- grantRevision,
21025
- pairedAt: input.pairedAt,
21026
- lastSeenAt: input.pairedAt
21027
- };
21028
- const receipt = {
21029
- pairRequestId: input.pairRequestId,
21030
- deviceId: input.deviceId,
21031
- credentialHash: input.credentialHash,
21032
- grantRevision,
21033
- expiresAt: input.receiptExpiresAt
21034
- };
21035
- return {
21036
- ...state,
21037
- devices: existing === undefined ? [...state.devices, device] : state.devices.map((d) => d.deviceId === input.deviceId ? device : d),
21038
- receipts: [
21039
- ...state.receipts.filter((r) => r.expiresAt > now && r.pairRequestId !== input.pairRequestId),
21040
- receipt
21041
- ]
21042
- };
21043
- });
21044
- }
21045
- putReceipt(receipt) {
21046
- return this.mutate((state) => ({
21047
- ...state,
21048
- receipts: state.receipts.some((r) => r.pairRequestId === receipt.pairRequestId) ? state.receipts.map((r) => r.pairRequestId === receipt.pairRequestId ? receipt : r) : [...state.receipts, receipt]
21049
- }));
21050
- }
21051
- removeReceipt(pairRequestId) {
21052
- return this.mutate((state) => ({
21053
- ...state,
21054
- receipts: state.receipts.filter((r) => r.pairRequestId !== pairRequestId)
21055
- }));
21056
- }
21057
- touchDevice(deviceId, lastSeenAt) {
21058
- return this.mutate((state) => {
21059
- const existing = state.devices.find((d) => d.deviceId === deviceId);
21060
- if (existing === undefined)
21061
- return state;
21062
- return {
21063
- ...state,
21064
- devices: state.devices.map((d) => d.deviceId === deviceId ? { ...d, lastSeenAt } : d)
21065
- };
21066
- });
21067
- }
21068
- rememberConnectedAddress(address) {
21069
- if (this.current.lastConnectedAddress === address)
21070
- return Promise.resolve(this.current);
21071
- return this.mutate((state) => ({ ...state, lastConnectedAddress: address }));
21072
- }
21073
- clearMobileAuthorizations() {
21074
- return this.mutate((state) => ({ ...state, devices: [], receipts: [] }));
21075
- }
21076
- ensureAdminToken() {
21077
- return this.enqueue(async () => {
21078
- const existing = await readSecretFile(this.adminTokenPath);
21079
- if (existing !== undefined) {
21080
- const token = existing.trim();
21081
- if (ADMIN_TOKEN_PATTERN.test(token))
21082
- return token;
21083
- throw new Error(`Invalid mobile admin token file ${this.adminTokenPath}: expected a base64url token`);
21084
- }
21085
- const token = randomBytes(MOBILE_ADMIN_TOKEN_BYTES).toString("base64url");
21086
- await writeSecretFile(this.adminTokenPath, `${token}
21087
- `);
21088
- return token;
21089
- });
21090
- }
21091
- mutate(recipe) {
21092
- return this.enqueue(async () => {
21093
- const next = freezeState(recipe(this.current));
21094
- await writeFileAtomic(this.statePath, `${JSON.stringify(next, null, "\t")}
21095
- `, 384);
21096
- this.current = next;
21097
- return next;
21098
- });
21099
- }
21100
- enqueue(task) {
21101
- const run = this.queue.then(task);
21102
- this.queue = run.then(() => {
21103
- return;
21104
- }, () => {
21105
- return;
21106
- });
21107
- return run;
21108
- }
21109
- }
21110
-
21111
- // packages/gateway/src/mobile/admin.ts
21112
- function contract(name, def) {
21113
- const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
21114
- key,
21115
- {
21116
- methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method(args, result)]))
21117
- }
21118
- ]));
21119
- return remote(name, { ...def, variants });
21120
- }
21121
- var listenerStateSchema = Type5.Union([
21122
- Type5.Object({ status: Type5.Literal("disabled") }),
21123
- Type5.Object({ status: Type5.Literal("starting") }),
21124
- Type5.Object({
21125
- status: Type5.Literal("listening"),
21126
- address: Type5.String(),
21127
- port: Type5.Integer({ minimum: 1, maximum: 65535 }),
21128
- bindAddress: Type5.Optional(Type5.String()),
21129
- mode: Type5.Optional(Type5.Union([Type5.Literal("direct"), Type5.Literal("wsl-relay")]))
21130
- }),
21131
- Type5.Object({ status: Type5.Literal("error"), error: Type5.String() }),
21132
- Type5.Object({ status: Type5.Literal("stopping") })
21133
- ]);
21134
- var adminDeviceSchema = Type5.Object({
21135
- deviceId: Type5.String(),
21136
- name: Type5.String(),
21137
- pairedAt: Type5.Integer(),
21138
- lastSeenAt: Type5.Integer(),
21139
- online: Type5.Boolean()
21140
- });
21141
- var NekoMobileAdmin = contract("neko.mobile-admin", {
21142
- version: 1,
21143
- input: Type5.Object({ token: Type5.String() }),
21144
- variants: {
21145
- admin: {
21146
- methods: {
21147
- state: [
21148
- Type5.Null(),
21149
- Type5.Object({
21150
- state: listenerStateSchema,
21151
- candidates: Type5.Array(Type5.String()),
21152
- port: Type5.Integer({ minimum: 1, maximum: 65535 }),
21153
- certificate: Type5.Union([
21154
- Type5.Object({ fingerprint: Type5.String(), expiresAt: Type5.Integer() }),
21155
- Type5.Null()
21156
- ]),
21157
- devices: Type5.Array(adminDeviceSchema),
21158
- lastConnectedAddress: Type5.Union([Type5.String(), Type5.Null()]),
21159
- wsl: Type5.Optional(Type5.Union([
21160
- Type5.Object({ interop: Type5.Boolean(), error: Type5.Union([Type5.String(), Type5.Null()]) }),
21161
- Type5.Null()
21162
- ]))
21163
- })
21164
- ],
21165
- enable: [
21166
- Type5.Object({
21167
- address: Type5.String(),
21168
- port: Type5.Optional(Type5.Integer({ minimum: 1, maximum: 65535 }))
21169
- }),
21170
- Type5.Object({
21171
- status: Type5.Literal("listening"),
21172
- address: Type5.String(),
21173
- port: Type5.Integer({ minimum: 1, maximum: 65535 }),
21174
- bindAddress: Type5.Optional(Type5.String()),
21175
- mode: Type5.Optional(Type5.Union([Type5.Literal("direct"), Type5.Literal("wsl-relay")]))
21176
- })
21177
- ],
21178
- disable: [Type5.Null(), listenerStateSchema],
21179
- createPairing: [Type5.Null(), Type5.Object({ pairingText: Type5.String(), expiresAt: Type5.Integer() })],
21180
- cancelPairing: [Type5.Null(), Type5.Null()],
21181
- revoke: [
21182
- Type5.Object({ deviceId: Type5.String({ minLength: 1 }) }),
21183
- Type5.Object({ revoked: Type5.Boolean() })
21184
- ],
21185
- regenerateCertificate: [
21186
- Type5.Null(),
21187
- Type5.Object({ fingerprint: Type5.String(), expiresAt: Type5.Integer() })
21188
- ]
21189
- }
21190
- }
21191
- }
21192
- });
21193
- function isMobilePrincipal(principal) {
21194
- return typeof principal === "object" && principal !== null && principal.kind === "mobile";
21195
- }
21196
- function digest(value) {
21197
- return createHash("sha256").update(value, "utf8").digest();
21198
- }
21199
- async function tokenMatches(token, store) {
21200
- if (store === undefined)
21201
- return false;
21202
- let adminToken;
21203
- try {
21204
- adminToken = await store.ensureAdminToken();
21205
- } catch {
21206
- return false;
21207
- }
21208
- const expected = digest(adminToken);
21209
- const provided = digest(token);
21210
- return expected.length === provided.length && timingSafeEqual(expected, provided);
21211
- }
21212
- function createMobileAdminFactory(deps) {
21213
- return (input, principal) => {
21214
- if (isMobilePrincipal(principal))
21215
- throw new ChordError("forbidden", "mobile principals cannot administer pairing");
21216
- const guard = async () => {
21217
- if (!await tokenMatches(input.token, deps.store))
21218
- throw new ChordError("forbidden", "forbidden");
21219
- };
21220
- return {
21221
- kind: "admin",
21222
- async state() {
21223
- await guard();
21224
- return deps.mobile.adminState();
21225
- },
21226
- async enable(params) {
21227
- await guard();
21228
- const state = await deps.mobile.enable({ address: params.address, port: params.port });
21229
- if (state.status !== "listening")
21230
- throw new ChordError("unavailable", `mobile listener failed: ${state.status}`);
21231
- return state;
21232
- },
21233
- async disable() {
21234
- await guard();
21235
- return deps.mobile.disable();
21236
- },
21237
- async createPairing() {
21238
- await guard();
21239
- return deps.mobile.createPairing();
21240
- },
21241
- async cancelPairing() {
21242
- await guard();
21243
- deps.mobile.cancelPairing();
21244
- return null;
21245
- },
21246
- async revoke(params) {
21247
- await guard();
21248
- return { revoked: await deps.mobile.revoke(params.deviceId) };
21249
- },
21250
- async regenerateCertificate() {
21251
- await guard();
21252
- return deps.mobile.regenerateCertificate();
21253
- }
21254
- };
21255
- };
21256
- }
21257
-
21258
- // packages/gateway/src/mobile/listener.ts
21259
- import { randomUUID as randomUUID2 } from "crypto";
21260
- import { createServer } from "https";
21261
- import { hostname } from "os";
21262
- import { createHost as createHost2, memoryLinkPair } from "@earendil-works/chord";
21263
- import { WebSocketServer } from "ws";
21264
-
21265
- // packages/gateway/src/contract.ts
21266
- import { method as method2, remote as remote2 } from "@earendil-works/chord";
21267
- import { Type as Type6 } from "typebox";
21268
- var rpc = (args, result) => [args, result];
21269
- function contract2(name, def) {
21270
- const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
21271
- key,
21272
- {
21273
- ...value,
21274
- methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method2(args, result)]))
21479
+ var GATEWAY_HOST = "neko";
21480
+ var rpc = (args, result) => [args, result];
21481
+ function contract(name, def) {
21482
+ const variants = Object.fromEntries(Object.entries(def.variants).map(([key, value]) => [
21483
+ key,
21484
+ {
21485
+ ...value,
21486
+ methods: Object.fromEntries(Object.entries(value.methods ?? {}).map(([member, [args, result]]) => [member, method(args, result)]))
21275
21487
  }
21276
21488
  ]));
21277
- return remote2(name, { ...def, variants });
21489
+ return remote(name, { ...def, host: hostNamed(GATEWAY_HOST), variants });
21278
21490
  }
21279
- var str = Type6.String();
21280
- var num = Type6.Number();
21281
- var bool = Type6.Boolean();
21282
- var nil = Type6.Null();
21283
- var integer = Type6.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
21284
- var nullableString = Type6.Union([str, nil]);
21285
- var phase = Type6.Enum(["pending", "running", "done"]);
21286
- var level = Type6.Enum(["project", "user", "builtin"]);
21287
- var thinking = Type6.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
21288
- var model = Type6.Object({ provider: str, modelId: str });
21289
- var change = Type6.Object({ model: Type6.Optional(model), thinkingLevel: Type6.Optional(thinking) }, { additionalProperties: false });
21290
- var usage = Type6.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
21291
- var image = Type6.Object({
21491
+ var str = Type5.String();
21492
+ var num = Type5.Number();
21493
+ var bool = Type5.Boolean();
21494
+ var nil = Type5.Null();
21495
+ var integer = Type5.Integer({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER });
21496
+ var nullableString = Type5.Union([str, nil]);
21497
+ var phase = Type5.Enum(["pending", "running", "done"]);
21498
+ var level = Type5.Enum(["project", "user", "builtin"]);
21499
+ var thinking = Type5.Enum(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
21500
+ var model = Type5.Object({ provider: str, modelId: str });
21501
+ var change = Type5.Object({ model: Type5.Optional(model), thinkingLevel: Type5.Optional(thinking) }, { additionalProperties: false });
21502
+ var usage = Type5.Object({ input: num, output: num, cacheRead: num, cacheWrite: num, cost: num });
21503
+ var image = Type5.Object({
21292
21504
  name: str,
21293
21505
  mimeType: str,
21294
21506
  width: integer,
@@ -21297,8 +21509,8 @@ var image = Type6.Object({
21297
21509
  entryId: integer,
21298
21510
  contentIndex: integer
21299
21511
  });
21300
- var skill = Type6.Object({ name: str, location: str, body: str, args: str });
21301
- var call = Type6.Object({
21512
+ var skill = Type5.Object({ name: str, location: str, body: str, args: str });
21513
+ var call = Type5.Object({
21302
21514
  callId: str,
21303
21515
  name: str,
21304
21516
  summary: str,
@@ -21306,116 +21518,135 @@ var call = Type6.Object({
21306
21518
  phase,
21307
21519
  output: nullableString,
21308
21520
  diff: nullableString,
21309
- diffAdded: Type6.Union([num, nil]),
21310
- diffRemoved: Type6.Union([num, nil]),
21311
- diffTruncated: Type6.Optional(bool)
21521
+ diffAdded: Type5.Union([num, nil]),
21522
+ diffRemoved: Type5.Union([num, nil]),
21523
+ diffTruncated: Type5.Optional(bool)
21312
21524
  });
21313
- var DelegationSchema = Type6.Object({
21525
+ var DelegationSchema = Type5.Object({
21314
21526
  parentId: integer,
21315
- agentId: Type6.Optional(str),
21316
- round: Type6.Optional(integer),
21317
- runnerId: Type6.Optional(integer),
21318
- inputEntryId: Type6.Optional(integer),
21319
- answerEntryId: Type6.Optional(integer),
21320
- roundUsage: Type6.Optional(Type6.Union([usage, nil])),
21527
+ agentId: Type5.Optional(str),
21528
+ round: Type5.Optional(integer),
21529
+ runnerId: Type5.Optional(integer),
21530
+ inputEntryId: Type5.Optional(integer),
21531
+ answerEntryId: Type5.Optional(integer),
21532
+ roundUsage: Type5.Optional(Type5.Union([usage, nil])),
21321
21533
  taskId: integer,
21322
21534
  callId: str,
21323
21535
  childId: integer,
21324
- role: Type6.Enum(["explore", "plan", "coder"]),
21536
+ role: Type5.Enum(["explore", "plan", "coder"]),
21325
21537
  title: str,
21326
21538
  createdAt: num,
21327
- startedAt: Type6.Union([num, nil]),
21328
- endedAt: Type6.Union([num, nil]),
21329
- status: Type6.Enum(["queued", "running", "completed", "failed", "cancelled"]),
21539
+ startedAt: Type5.Union([num, nil]),
21540
+ endedAt: Type5.Union([num, nil]),
21541
+ status: Type5.Enum(["queued", "running", "completed", "failed", "cancelled"]),
21330
21542
  report: nullableString,
21331
- approvalCount: Type6.Optional(integer),
21332
- historyThrough: Type6.Optional(integer),
21333
- historyUsage: Type6.Optional(usage)
21543
+ approvalCount: Type5.Optional(integer),
21544
+ historyThrough: Type5.Optional(integer),
21545
+ historyUsage: Type5.Optional(usage)
21334
21546
  });
21335
- var SubagentSummarySchema = Type6.Object({
21547
+ var SubagentSummarySchema = Type5.Object({
21336
21548
  agentId: str,
21337
21549
  parentId: integer,
21338
21550
  childId: integer,
21339
- role: Type6.Enum(["explore", "plan", "coder"]),
21551
+ role: Type5.Enum(["explore", "plan", "coder"]),
21340
21552
  title: str,
21341
- status: Type6.Enum(["idle", "queued", "running"]),
21342
- activeRoundId: Type6.Union([integer, nil]),
21553
+ status: Type5.Enum(["idle", "queued", "running"]),
21554
+ activeRoundId: Type5.Union([integer, nil]),
21343
21555
  lastRoundId: integer,
21344
- lastReport: Type6.Optional(nullableString),
21556
+ lastReport: Type5.Optional(nullableString),
21345
21557
  roundCount: integer,
21346
21558
  taskTitle: str,
21347
21559
  approvalCount: integer,
21348
21560
  createdAt: num,
21349
- historyThrough: Type6.Optional(integer),
21350
- historyUsage: Type6.Optional(usage)
21561
+ historyThrough: Type5.Optional(integer),
21562
+ historyUsage: Type5.Optional(usage)
21351
21563
  });
21352
- var jobStatus = Type6.Enum(["running", "exited", "failed", "stopped", "lost"]);
21353
- var JobSummarySchema = Type6.Object({
21564
+ var jobStatus = Type5.Enum(["running", "exited", "failed", "stopped", "lost"]);
21565
+ var JobSummarySchema = Type5.Object({
21354
21566
  jobId: str,
21355
21567
  title: str,
21356
21568
  command: str,
21357
21569
  cwd: str,
21358
21570
  status: jobStatus,
21359
21571
  startedAt: num,
21360
- endedAt: Type6.Union([num, nil]),
21361
- exitCode: Type6.Union([num, nil]),
21362
- reason: Type6.Optional(Type6.Enum(["timeout", "spawn-error", "restart", "aborted"])),
21363
- timeoutSeconds: Type6.Optional(num),
21572
+ endedAt: Type5.Union([num, nil]),
21573
+ exitCode: Type5.Union([num, nil]),
21574
+ reason: Type5.Optional(Type5.Enum(["timeout", "spawn-error", "restart", "aborted"])),
21575
+ timeoutSeconds: Type5.Optional(num),
21364
21576
  outputBytes: integer
21365
21577
  });
21366
- var JobOutputSchema = Type6.Object({
21578
+ var JobOutputSchema = Type5.Object({
21367
21579
  job: JobSummarySchema,
21368
21580
  text: str,
21369
21581
  offset: integer,
21370
21582
  truncated: bool,
21371
21583
  dropped: integer
21372
21584
  });
21373
- var ChatViewSchema = Type6.Object({
21374
- historyBefore: Type6.Optional(Type6.Union([integer, nil])),
21375
- agents: Type6.Optional(Type6.Array(SubagentSummarySchema)),
21376
- jobs: Type6.Optional(Type6.Array(JobSummarySchema)),
21377
- rounds: Type6.Optional(Type6.Array(DelegationSchema)),
21378
- subagents: Type6.Optional(Type6.Array(DelegationSchema)),
21379
- taskUsage: Type6.Optional(usage),
21585
+ var QueuedInputSchema = Type5.Object({
21586
+ id: integer,
21587
+ mode: Type5.Enum(["steer", "followUp"]),
21588
+ preview: str,
21589
+ images: integer,
21590
+ skill: Type5.Optional(Type5.Object({ name: str, args: str }))
21591
+ });
21592
+ var WithdrawResultSchema = Type5.Object({
21593
+ result: Type5.Enum(["withdrawn", "placed", "settled", "unknown"]),
21594
+ text: Type5.Optional(str),
21595
+ images: Type5.Optional(integer)
21596
+ });
21597
+ var SteerResultSchema = Type5.Object({
21598
+ result: Type5.Enum(["steered", "withdrawn", "placed", "settled", "unknown"]),
21599
+ id: Type5.Optional(integer),
21600
+ text: Type5.Optional(str),
21601
+ images: Type5.Optional(integer)
21602
+ });
21603
+ var ChatViewSchema = Type5.Object({
21604
+ historyBefore: Type5.Optional(Type5.Union([integer, nil])),
21605
+ agents: Type5.Optional(Type5.Array(SubagentSummarySchema)),
21606
+ jobs: Type5.Optional(Type5.Array(JobSummarySchema)),
21607
+ queued: Type5.Optional(Type5.Array(QueuedInputSchema)),
21608
+ rounds: Type5.Optional(Type5.Array(DelegationSchema)),
21609
+ subagents: Type5.Optional(Type5.Array(DelegationSchema)),
21610
+ taskUsage: Type5.Optional(usage),
21380
21611
  model: str,
21381
- agent: Type6.Object({ model: Type6.Union([model, nil]), thinkingLevel: Type6.Union([thinking, nil]) }),
21612
+ agent: Type5.Object({ model: Type5.Union([model, nil]), thinkingLevel: Type5.Union([thinking, nil]) }),
21382
21613
  cwd: str,
21383
21614
  busy: bool,
21384
21615
  status: nullableString,
21385
21616
  usage,
21386
- contextUsage: Type6.Optional(Type6.Object({ tokens: Type6.Union([num, nil]), window: num, percent: Type6.Union([num, nil]) })),
21387
- compaction: Type6.Optional(Type6.Object({
21388
- failure: Type6.Optional(Type6.Object({ taskId: integer, reason: Type6.Enum(["manual", "threshold", "overflow"]), error: str })),
21389
- active: Type6.Array(Type6.Object({
21617
+ contextUsage: Type5.Optional(Type5.Object({ tokens: Type5.Union([num, nil]), window: num, percent: Type5.Union([num, nil]) })),
21618
+ compaction: Type5.Optional(Type5.Object({
21619
+ failure: Type5.Optional(Type5.Object({ taskId: integer, reason: Type5.Enum(["manual", "threshold", "overflow"]), error: str })),
21620
+ active: Type5.Array(Type5.Object({
21390
21621
  taskId: integer,
21391
- reason: Type6.Enum(["manual", "threshold", "overflow"]),
21622
+ reason: Type5.Enum(["manual", "threshold", "overflow"]),
21392
21623
  blocking: bool,
21393
21624
  attempt: integer,
21394
- retry: Type6.Optional(Type6.Object({ at: num, error: str }))
21625
+ retry: Type5.Optional(Type5.Object({ at: num, error: str }))
21395
21626
  })),
21396
- latest: Type6.Union([
21627
+ latest: Type5.Union([
21397
21628
  nil,
21398
- Type6.Object({
21629
+ Type5.Object({
21399
21630
  taskId: integer,
21400
- phase: Type6.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
21631
+ phase: Type5.Enum(["summarizing", "waiting-to-apply", "applied", "nothing", "skipped", "cancelled", "failed"]),
21401
21632
  at: num
21402
21633
  })
21403
21634
  ])
21404
21635
  })),
21405
- items: Type6.Array(Type6.Union([
21406
- Type6.Object({
21407
- type: Type6.Literal("user"),
21408
- order: Type6.Optional(num),
21636
+ items: Type5.Array(Type5.Union([
21637
+ Type5.Object({
21638
+ type: Type5.Literal("user"),
21639
+ order: Type5.Optional(num),
21409
21640
  key: str,
21410
21641
  id: integer,
21411
21642
  text: str,
21412
- images: Type6.Optional(Type6.Array(image)),
21413
- skill: Type6.Optional(skill),
21414
- defaultImagePrompt: Type6.Optional(Type6.Object({ version: Type6.Integer({ minimum: 1 }), text: str }))
21643
+ images: Type5.Optional(Type5.Array(image)),
21644
+ skill: Type5.Optional(skill),
21645
+ defaultImagePrompt: Type5.Optional(Type5.Object({ version: Type5.Integer({ minimum: 1 }), text: str }))
21415
21646
  }),
21416
- Type6.Object({
21417
- type: Type6.Literal("assistant"),
21418
- order: Type6.Optional(num),
21647
+ Type5.Object({
21648
+ type: Type5.Literal("assistant"),
21649
+ order: Type5.Optional(num),
21419
21650
  key: str,
21420
21651
  id: integer,
21421
21652
  thinking: str,
@@ -21424,11 +21655,11 @@ var ChatViewSchema = Type6.Object({
21424
21655
  errorMessage: nullableString,
21425
21656
  note: nullableString
21426
21657
  }),
21427
- Type6.Object({
21428
- type: Type6.Literal("tool"),
21429
- order: Type6.Optional(num),
21658
+ Type5.Object({
21659
+ type: Type5.Literal("tool"),
21660
+ order: Type5.Optional(num),
21430
21661
  key: str,
21431
- images: Type6.Optional(Type6.Array(image)),
21662
+ images: Type5.Optional(Type5.Array(image)),
21432
21663
  callId: str,
21433
21664
  name: str,
21434
21665
  summary: str,
@@ -21437,61 +21668,61 @@ var ChatViewSchema = Type6.Object({
21437
21668
  isError: bool,
21438
21669
  output: nullableString,
21439
21670
  result: nullableString,
21440
- durationMs: Type6.Union([num, nil]),
21671
+ durationMs: Type5.Union([num, nil]),
21441
21672
  note: nullableString,
21442
21673
  diff: nullableString,
21443
- diffAdded: Type6.Union([num, nil]),
21444
- diffRemoved: Type6.Union([num, nil]),
21445
- diffTruncated: Type6.Optional(bool)
21674
+ diffAdded: Type5.Union([num, nil]),
21675
+ diffRemoved: Type5.Union([num, nil]),
21676
+ diffTruncated: Type5.Optional(bool)
21446
21677
  }),
21447
- Type6.Object({
21448
- type: Type6.Literal("system"),
21449
- order: Type6.Optional(num),
21678
+ Type5.Object({
21679
+ type: Type5.Literal("system"),
21680
+ order: Type5.Optional(num),
21450
21681
  key: str,
21451
21682
  id: integer,
21452
21683
  text: str,
21453
- jobId: Type6.Optional(str)
21684
+ jobId: Type5.Optional(str)
21454
21685
  })
21455
21686
  ])),
21456
- streaming: Type6.Union([nil, Type6.Object({ thinking: str, text: str, toolCalls: Type6.Array(call) })])
21687
+ streaming: Type5.Union([nil, Type5.Object({ thinking: str, text: str, toolCalls: Type5.Array(call) })])
21457
21688
  });
21458
- var SessionSummarySchema = Type6.Object({
21689
+ var SessionSummarySchema = Type5.Object({
21459
21690
  workspaceRoot: str,
21460
21691
  lastUsedAt: num,
21461
21692
  title: str,
21462
21693
  firstPromptPreview: str,
21463
21694
  createdAt: num,
21464
- lastMessageAt: Type6.Union([num, nil]),
21465
- model: Type6.Union([model, nil]),
21466
- status: Type6.Enum(["idle", "running", "error"]),
21695
+ lastMessageAt: Type5.Union([num, nil]),
21696
+ model: Type5.Union([model, nil]),
21697
+ status: Type5.Enum(["idle", "running", "error"]),
21467
21698
  id: str
21468
21699
  });
21469
- var DirectoryStateSchema = Type6.Object({ revision: integer, sessions: Type6.Array(SessionSummarySchema) });
21470
- var WorkspaceSummarySchema = Type6.Object({ path: str, createdAt: num });
21471
- var WorkspaceStateSchema = Type6.Object({ revision: integer, workspaces: Type6.Array(WorkspaceSummarySchema) });
21472
- var credentialStatus = Type6.Object({
21700
+ var DirectoryStateSchema = Type5.Object({ revision: integer, sessions: Type5.Array(SessionSummarySchema) });
21701
+ var WorkspaceSummarySchema = Type5.Object({ path: str, createdAt: num });
21702
+ var WorkspaceStateSchema = Type5.Object({ revision: integer, workspaces: Type5.Array(WorkspaceSummarySchema) });
21703
+ var credentialStatus = Type5.Object({
21473
21704
  savedConfigured: bool,
21474
21705
  environmentConfigured: bool,
21475
- source: Type6.Enum(["saved", "environment", "runtime", "keyless", "none"])
21706
+ source: Type5.Enum(["saved", "environment", "runtime", "keyless", "none"])
21476
21707
  });
21477
- var CredentialStateSchema = Type6.Object({
21708
+ var CredentialStateSchema = Type5.Object({
21478
21709
  revision: integer,
21479
- statuses: Type6.Object({ deepseek: credentialStatus, tavily: credentialStatus })
21710
+ statuses: Type5.Object({ deepseek: credentialStatus, tavily: credentialStatus })
21480
21711
  });
21481
- var AttachmentSchema = Type6.Object({ sessionId: str, attachmentId: str });
21482
- var chatInput = Type6.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
21483
- var NekoSessionDirectory = contract2("neko.session-directory", {
21712
+ var AttachmentSchema = Type5.Object({ sessionId: str, attachmentId: str });
21713
+ var chatInput = Type5.Object({ sessionId: str, attachmentId: str }, { additionalProperties: false });
21714
+ var NekoSessionDirectory = contract("neko.session-directory", {
21484
21715
  version: 1,
21485
- input: Type6.Object({}, { additionalProperties: false }),
21716
+ input: Type5.Object({}, { additionalProperties: false }),
21486
21717
  variants: { ready: { state: { state: DirectoryStateSchema } } }
21487
21718
  });
21488
- var NekoSessionManagement = contract2("neko.session-management", {
21719
+ var NekoSessionManagement = contract("neko.session-management", {
21489
21720
  version: 1,
21490
- input: Type6.Object({}, { additionalProperties: false }),
21721
+ input: Type5.Object({}, { additionalProperties: false }),
21491
21722
  variants: {
21492
21723
  ready: {
21493
21724
  methods: {
21494
- create: rpc(Type6.Object({ ...change.properties, cwd: Type6.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
21725
+ create: rpc(Type5.Object({ ...change.properties, cwd: Type5.Optional(str) }, { additionalProperties: false }), SessionSummarySchema),
21495
21726
  archive: rpc(str, nil),
21496
21727
  attach: rpc(str, AttachmentSchema),
21497
21728
  detach: rpc(nil, nil),
@@ -21500,30 +21731,30 @@ var NekoSessionManagement = contract2("neko.session-management", {
21500
21731
  }
21501
21732
  }
21502
21733
  });
21503
- var resize = Type6.Object({
21504
- maxWidth: Type6.Optional(integer),
21505
- maxHeight: Type6.Optional(integer),
21506
- maxBytes: Type6.Optional(integer),
21507
- jpegQuality: Type6.Optional(integer)
21734
+ var resize = Type5.Object({
21735
+ maxWidth: Type5.Optional(integer),
21736
+ maxHeight: Type5.Optional(integer),
21737
+ maxBytes: Type5.Optional(integer),
21738
+ jpegQuality: Type5.Optional(integer)
21508
21739
  });
21509
- var inputLimits = Type6.Object({
21510
- maxRequestBytes: Type6.Optional(integer),
21511
- images: Type6.Optional(Type6.Object({
21512
- resize: Type6.Optional(resize),
21513
- maxPerMessage: Type6.Optional(integer),
21514
- maxPerRequest: Type6.Optional(integer)
21740
+ var inputLimits = Type5.Object({
21741
+ maxRequestBytes: Type5.Optional(integer),
21742
+ images: Type5.Optional(Type5.Object({
21743
+ resize: Type5.Optional(resize),
21744
+ maxPerMessage: Type5.Optional(integer),
21745
+ maxPerRequest: Type5.Optional(integer)
21515
21746
  }))
21516
21747
  });
21517
- var modelInfo = Type6.Object({
21748
+ var modelInfo = Type5.Object({
21518
21749
  provider: str,
21519
21750
  modelId: str,
21520
21751
  name: str,
21521
21752
  contextWindow: num,
21522
- input: Type6.Array(str),
21523
- inputLimits: Type6.Optional(inputLimits),
21753
+ input: Type5.Array(str),
21754
+ inputLimits: Type5.Optional(inputLimits),
21524
21755
  reasoning: bool,
21525
- supportedThinkingLevels: Type6.Array(thinking),
21526
- thinkingLevelAdjustments: Type6.Object({
21756
+ supportedThinkingLevels: Type5.Array(thinking),
21757
+ thinkingLevelAdjustments: Type5.Object({
21527
21758
  off: thinking,
21528
21759
  minimal: thinking,
21529
21760
  low: thinking,
@@ -21533,168 +21764,714 @@ var modelInfo = Type6.Object({
21533
21764
  max: thinking
21534
21765
  })
21535
21766
  });
21536
- var NekoModels = contract2("neko.models", {
21767
+ var NekoModels = contract("neko.models", {
21537
21768
  version: 1,
21538
- input: Type6.Object({}, { additionalProperties: false }),
21539
- variants: { ready: { methods: { list: rpc(nil, Type6.Array(modelInfo)) } } }
21769
+ input: Type5.Object({}, { additionalProperties: false }),
21770
+ variants: { ready: { methods: { list: rpc(nil, Type5.Array(modelInfo)) } } }
21540
21771
  });
21541
- var imageChunk = Type6.Object({ data: str, total: integer, mimeType: str });
21542
- var NekoSubagent = contract2("neko.subagent", {
21772
+ var imageChunk = Type5.Object({ data: str, total: integer, mimeType: str });
21773
+ var NekoSubagent = contract("neko.subagent", {
21543
21774
  version: 1,
21544
- input: Type6.Object({
21775
+ input: Type5.Object({
21545
21776
  sessionId: str,
21546
21777
  attachmentId: str,
21547
- taskId: Type6.Optional(integer),
21548
- agentId: Type6.Optional(str)
21778
+ taskId: Type5.Optional(integer),
21779
+ agentId: Type5.Optional(str)
21549
21780
  }),
21550
21781
  variants: {
21551
21782
  ready: {
21552
21783
  state: { revision: integer },
21553
21784
  methods: {
21554
- chunk: rpc(Type6.Object({
21555
- kind: Type6.Enum(["view", "history"]),
21556
- before: Type6.Optional(integer),
21557
- token: Type6.Optional(integer),
21785
+ chunk: rpc(Type5.Object({
21786
+ kind: Type5.Enum(["view", "history"]),
21787
+ before: Type5.Optional(integer),
21788
+ token: Type5.Optional(integer),
21558
21789
  offset: integer
21559
- }), Type6.Object({ token: integer, data: str, next: Type6.Union([integer, nil]) })),
21560
- getImage: rpc(Type6.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
21561
- decideApproval: rpc(Type6.Object({ id: integer, version: str, decision: Type6.Enum(["allowed", "denied"]) }), Type6.Enum(["pending", "allowed", "denied", "cancelled"])),
21562
- stop: rpc(Type6.Union([nil, Type6.Object({ taskId: integer })]), nil)
21790
+ }), Type5.Object({ token: integer, data: str, next: Type5.Union([integer, nil]) })),
21791
+ getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }), imageChunk),
21792
+ decideApproval: rpc(Type5.Object({ id: integer, version: str, decision: Type5.Enum(["allowed", "denied"]) }), Type5.Enum(["pending", "allowed", "denied", "cancelled"])),
21793
+ stop: rpc(Type5.Union([nil, Type5.Object({ taskId: integer })]), nil)
21563
21794
  }
21564
21795
  }
21565
21796
  }
21566
21797
  });
21567
- var NekoChat = contract2("neko.chat", {
21798
+ var NekoChat = contract("neko.chat", {
21568
21799
  version: 1,
21569
21800
  input: chatInput,
21570
21801
  variants: {
21571
21802
  ready: {
21572
21803
  state: { view: ChatViewSchema },
21573
21804
  methods: {
21574
- submit: rpc(Type6.Object({
21805
+ submit: rpc(Type5.Object({
21575
21806
  content: str,
21576
- images: Type6.Optional(Type6.Array(str, { maxItems: 4 })),
21577
- requestId: Type6.Optional(str),
21578
- whenBusy: Type6.Optional(Type6.Enum(["steer", "followUp"]))
21807
+ images: Type5.Optional(Type5.Array(str, { maxItems: 4 })),
21808
+ requestId: Type5.Optional(str),
21809
+ whenBusy: Type5.Optional(Type5.Enum(["steer", "followUp"]))
21579
21810
  }, { additionalProperties: false }), nil),
21580
21811
  abort: rpc(nil, nil),
21812
+ withdraw: rpc(Type5.Object({ id: integer }, { additionalProperties: false }), WithdrawResultSchema),
21813
+ steer: rpc(Type5.Object({ id: integer }, { additionalProperties: false }), SteerResultSchema),
21581
21814
  usage: rpc(nil, usage),
21582
- skills: rpc(nil, Type6.Array(Type6.Object({ name: str, description: str, location: str, level }))),
21583
- jobOutput: rpc(Type6.Object({ jobId: str, since: Type6.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
21584
- jobStop: rpc(Type6.Object({ jobId: str }, { additionalProperties: false }), JobSummarySchema),
21815
+ skills: rpc(nil, Type5.Array(Type5.Object({ name: str, description: str, location: str, level }))),
21816
+ jobOutput: rpc(Type5.Object({ jobId: str, since: Type5.Optional(integer) }, { additionalProperties: false }), JobOutputSchema),
21817
+ jobStop: rpc(Type5.Object({ jobId: str }, { additionalProperties: false }), JobSummarySchema),
21585
21818
  configure: rpc(change, nil),
21586
- compact: rpc(Type6.Object({ instructions: Type6.Optional(str) }, { additionalProperties: false }), nil),
21587
- importImage: rpc(str, Type6.Object({ id: str, name: str })),
21588
- draftImage: rpc(Type6.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
21589
- beginImage: rpc(Type6.Object({ name: str, mimeType: str, size: Type6.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
21590
- appendImage: rpc(Type6.Object({ id: str, offset: integer, data: Type6.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
21819
+ compact: rpc(Type5.Object({ instructions: Type5.Optional(str) }, { additionalProperties: false }), nil),
21820
+ importImage: rpc(str, Type5.Object({ id: str, name: str })),
21821
+ draftImage: rpc(Type5.Object({ id: str, offset: integer }, { additionalProperties: false }), imageChunk),
21822
+ beginImage: rpc(Type5.Object({ name: str, mimeType: str, size: Type5.Integer({ minimum: 1, maximum: 20971520 }) }, { additionalProperties: false }), str),
21823
+ appendImage: rpc(Type5.Object({ id: str, offset: integer, data: Type5.String({ maxLength: 350000 }) }, { additionalProperties: false }), nil),
21591
21824
  finishImage: rpc(str, nil),
21592
21825
  releaseImage: rpc(str, nil),
21593
- getImage: rpc(Type6.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
21826
+ getImage: rpc(Type5.Object({ entryId: integer, contentIndex: integer, offset: integer }, { additionalProperties: false }), imageChunk)
21594
21827
  }
21595
21828
  }
21596
21829
  }
21597
21830
  });
21598
- var browse = Type6.Object({ path: str, query: Type6.Optional(str), showHidden: Type6.Optional(bool), cursor: Type6.Optional(str) }, { additionalProperties: false });
21599
- var page = Type6.Object({
21831
+ var browse = Type5.Object({ path: str, query: Type5.Optional(str), showHidden: Type5.Optional(bool), cursor: Type5.Optional(str) }, { additionalProperties: false });
21832
+ var page = Type5.Object({
21600
21833
  path: str,
21601
21834
  parent: nullableString,
21602
- entries: Type6.Array(Type6.Object({ name: str, path: str, error: Type6.Optional(str) })),
21835
+ entries: Type5.Array(Type5.Object({ name: str, path: str, error: Type5.Optional(str) })),
21603
21836
  next: nullableString
21604
21837
  });
21605
- var NekoWorkspaces = contract2("neko.workspaces", {
21838
+ var NekoWorkspaces = contract("neko.workspaces", {
21606
21839
  version: 1,
21607
- input: Type6.Object({}, { additionalProperties: false }),
21840
+ input: Type5.Object({}, { additionalProperties: false }),
21608
21841
  variants: {
21609
21842
  ready: {
21610
21843
  state: { state: WorkspaceStateSchema },
21611
21844
  methods: {
21612
- add: rpc(Type6.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
21845
+ add: rpc(Type5.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
21613
21846
  browse: rpc(browse, page)
21614
21847
  }
21615
21848
  }
21616
21849
  }
21617
21850
  });
21618
- var provider = Type6.Enum(["deepseek", "tavily"]);
21619
- var NekoCredentials = contract2("neko.credentials", {
21851
+ var provider = Type5.Enum(["deepseek", "tavily"]);
21852
+ var NekoCredentials = contract("neko.credentials", {
21620
21853
  version: 1,
21621
- input: Type6.Object({}, { additionalProperties: false }),
21854
+ input: Type5.Object({}, { additionalProperties: false }),
21622
21855
  variants: {
21623
21856
  ready: {
21624
21857
  state: { state: CredentialStateSchema },
21625
21858
  methods: {
21626
- save: rpc(Type6.Object({ provider, key: Type6.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
21627
- remove: rpc(Type6.Object({ provider }, { additionalProperties: false }), nil)
21859
+ save: rpc(Type5.Object({ provider, key: Type5.String({ minLength: 1, maxLength: 8192 }) }, { additionalProperties: false }), nil),
21860
+ remove: rpc(Type5.Object({ provider }, { additionalProperties: false }), nil)
21861
+ }
21862
+ }
21863
+ }
21864
+ });
21865
+ var SearchSettingsStateSchema = Type5.Object({
21866
+ revision: integer,
21867
+ selected: Type5.Enum(["tavily", "deepseek"])
21868
+ });
21869
+ var NekoSearchSettings = contract("neko.search-settings", {
21870
+ version: 1,
21871
+ input: Type5.Object({}, { additionalProperties: false }),
21872
+ variants: {
21873
+ ready: {
21874
+ state: { state: SearchSettingsStateSchema },
21875
+ methods: {
21876
+ set: rpc(Type5.Object({ provider: Type5.Enum(["tavily", "deepseek"]) }, { additionalProperties: false }), nil)
21628
21877
  }
21629
21878
  }
21630
21879
  }
21631
21880
  });
21632
- var FileCandidatesSchema = Type6.Object({
21633
- entries: Type6.Array(Type6.Object({ name: str, path: str, kind: Type6.Enum(["file", "directory"]) }), { maxItems: 100 }),
21881
+ var contextSource = Type5.Enum(["default", "config", "saved"]);
21882
+ var ContextSettingsStateSchema = Type5.Object({
21883
+ revision: integer,
21884
+ effective: Type5.Object({
21885
+ compaction: Type5.Object({
21886
+ enabled: bool,
21887
+ triggerPercent: Type5.Union([integer, nil]),
21888
+ keepRecentTokens: integer
21889
+ }),
21890
+ pruning: Type5.Object({
21891
+ enabled: bool,
21892
+ triggerPercent: integer,
21893
+ keepRecent: integer,
21894
+ minChars: integer,
21895
+ minSavePercent: integer
21896
+ })
21897
+ }),
21898
+ sources: Type5.Object({
21899
+ compaction: Type5.Object({ enabled: contextSource, triggerPercent: contextSource, keepRecentTokens: contextSource }),
21900
+ pruning: Type5.Object({
21901
+ enabled: contextSource,
21902
+ triggerPercent: contextSource,
21903
+ keepRecent: contextSource,
21904
+ minChars: contextSource,
21905
+ minSavePercent: contextSource
21906
+ })
21907
+ })
21908
+ });
21909
+ var nullable = (schema) => Type5.Union([schema, nil]);
21910
+ var ContextSettingsPatchSchema = Type5.Object({
21911
+ compaction: Type5.Optional(Type5.Object({
21912
+ enabled: Type5.Optional(nullable(bool)),
21913
+ triggerPercent: Type5.Optional(nullable(integer)),
21914
+ keepRecentTokens: Type5.Optional(nullable(integer))
21915
+ }, { additionalProperties: false })),
21916
+ pruning: Type5.Optional(Type5.Object({
21917
+ enabled: Type5.Optional(nullable(bool)),
21918
+ triggerPercent: Type5.Optional(nullable(integer)),
21919
+ keepRecent: Type5.Optional(nullable(integer)),
21920
+ minChars: Type5.Optional(nullable(integer)),
21921
+ minSavePercent: Type5.Optional(nullable(integer))
21922
+ }, { additionalProperties: false }))
21923
+ }, { additionalProperties: false });
21924
+ var NekoContextSettings = contract("neko.context-settings", {
21925
+ version: 1,
21926
+ input: Type5.Object({}, { additionalProperties: false }),
21927
+ variants: {
21928
+ ready: {
21929
+ state: { state: ContextSettingsStateSchema },
21930
+ methods: {
21931
+ set: rpc(ContextSettingsPatchSchema, nil),
21932
+ reset: rpc(Type5.Object({}, { additionalProperties: false }), nil)
21933
+ }
21934
+ }
21935
+ }
21936
+ });
21937
+ var FileCandidatesSchema = Type5.Object({
21938
+ entries: Type5.Array(Type5.Object({ name: str, path: str, kind: Type5.Enum(["file", "directory"]) }), { maxItems: 100 }),
21634
21939
  limited: bool,
21635
21940
  incomplete: bool
21636
21941
  });
21637
- var NekoWorkspaceFiles = contract2("neko.workspace-files", {
21942
+ var NekoWorkspaceFiles = contract("neko.workspace-files", {
21943
+ version: 1,
21944
+ input: Type5.Object({}, { additionalProperties: false }),
21945
+ variants: {
21946
+ ready: {
21947
+ methods: {
21948
+ search: rpc(Type5.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
21949
+ }
21950
+ }
21951
+ }
21952
+ });
21953
+ var marketSkill = Type5.Object({
21954
+ name: str,
21955
+ description: str,
21956
+ category: str,
21957
+ source: str,
21958
+ license: str,
21959
+ version: str,
21960
+ sha256: str,
21961
+ sizeBytes: integer,
21962
+ uploadedAt: str,
21963
+ installed: Type5.Object({ project: bool, user: bool, builtin: bool }),
21964
+ activeLevel: Type5.Union([level, nil])
21965
+ });
21966
+ var preview = Type5.Object({ body: str, files: Type5.Array(str) });
21967
+ var scope = Type5.Enum(["user", "project"]);
21968
+ var installResult = Type5.Object({ name: str, scope, path: str, shadowedBy: Type5.Optional(scope) });
21969
+ var NekoSkillMarket = contract("neko.skill-market", {
21638
21970
  version: 1,
21639
- input: Type6.Object({}, { additionalProperties: false }),
21971
+ input: Type5.Object({}, { additionalProperties: false }),
21640
21972
  variants: {
21641
21973
  ready: {
21642
21974
  methods: {
21643
- search: rpc(Type6.Object({ workspaceRoot: str, query: str }, { additionalProperties: false }), FileCandidatesSchema)
21975
+ list: rpc(Type5.Object({ query: Type5.Optional(str), workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), Type5.Array(marketSkill)),
21976
+ detail: rpc(Type5.Object({ name: str, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), marketSkill),
21977
+ preview: rpc(Type5.Object({ name: str }, { additionalProperties: false }), preview),
21978
+ install: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str), overwrite: Type5.Optional(bool) }, { additionalProperties: false }), installResult),
21979
+ uninstall: rpc(Type5.Object({ name: str, scope, workspaceRoot: Type5.Optional(str) }, { additionalProperties: false }), nil)
21644
21980
  }
21645
21981
  }
21646
21982
  }
21647
21983
  });
21648
- var marketSkill = Type6.Object({
21649
- name: str,
21650
- description: str,
21651
- category: str,
21652
- source: str,
21653
- license: str,
21654
- version: str,
21655
- sha256: str,
21656
- sizeBytes: integer,
21657
- uploadedAt: str,
21658
- installed: Type6.Object({ project: bool, user: bool, builtin: bool }),
21659
- activeLevel: Type6.Union([level, nil])
21660
- });
21661
- var preview = Type6.Object({ body: str, files: Type6.Array(str) });
21662
- var scope = Type6.Enum(["user", "project"]);
21663
- var installResult = Type6.Object({ name: str, scope, path: str, shadowedBy: Type6.Optional(scope) });
21664
- var NekoSkillMarket = contract2("neko.skill-market", {
21984
+ var ApplicationInfoSchema = Type5.Object({
21985
+ runningVersion: str,
21986
+ installedVersion: nullableString,
21987
+ protocolVersion: Type5.Integer({ minimum: 1 }),
21988
+ latestVersion: nullableString,
21989
+ checkedAt: Type5.Union([integer, nil]),
21990
+ checkState: Type5.Enum(["unknown", "checking", "ok", "failed", "development"]),
21991
+ serverId: str
21992
+ });
21993
+ var NekoApplicationInfo = contract("neko.application-info", {
21994
+ version: 1,
21995
+ input: Type5.Object({}),
21996
+ variants: {
21997
+ ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } }
21998
+ }
21999
+ });
22000
+
22001
+ // packages/gateway/src/identity.ts
22002
+ function isServerId(value) {
22003
+ return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(value);
22004
+ }
22005
+
22006
+ // packages/gateway/src/mobile/admin.ts
22007
+ import { createHash, timingSafeEqual } from "crypto";
22008
+ import { ChordError } from "@earendil-works/chord";
22009
+ import { Type as Type6 } from "typebox";
22010
+
22011
+ // packages/gateway/src/principal.ts
22012
+ var HOST_PRINCIPAL = { kind: "host" };
22013
+ function gatewayPrincipalKey(principal) {
22014
+ switch (principal.kind) {
22015
+ case "mobile":
22016
+ return `mobile:${principal.deviceId}:${principal.grantRevision}:${principal.connectionId}`;
22017
+ case "local":
22018
+ return `local:${principal.connectionId}`;
22019
+ case "host":
22020
+ return "host";
22021
+ }
22022
+ }
22023
+ function isMobilePrincipal(principal) {
22024
+ return typeof principal === "object" && principal !== null && principal.kind === "mobile";
22025
+ }
22026
+
22027
+ // packages/gateway/src/mobile/store.ts
22028
+ import { randomBytes } from "crypto";
22029
+ import { chmod as chmod2, mkdir as mkdir2, readFile as readFile4, rename as rename2, rm, writeFile } from "fs/promises";
22030
+ import { join as join6 } from "path";
22031
+ var MOBILE_STATE_FORMAT_VERSION = 1;
22032
+ var MOBILE_DEFAULT_PORT = 8323;
22033
+ var MOBILE_ADMIN_TOKEN_BYTES = 32;
22034
+ var POSIX = process.platform !== "win32";
22035
+ var ADMIN_TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
22036
+ var CREDENTIAL_HASH_PATTERN = /^[0-9a-fA-F]{64}$/;
22037
+ function defaultState() {
22038
+ return {
22039
+ formatVersion: MOBILE_STATE_FORMAT_VERSION,
22040
+ enabled: false,
22041
+ address: null,
22042
+ port: MOBILE_DEFAULT_PORT,
22043
+ devices: [],
22044
+ receipts: [],
22045
+ lastConnectedAddress: null
22046
+ };
22047
+ }
22048
+ function freezeState(state) {
22049
+ for (const device of state.devices)
22050
+ Object.freeze(device);
22051
+ Object.freeze(state.devices);
22052
+ for (const receipt of state.receipts)
22053
+ Object.freeze(receipt);
22054
+ Object.freeze(state.receipts);
22055
+ return Object.freeze(state);
22056
+ }
22057
+ function isNotFound(error) {
22058
+ return error?.code === "ENOENT";
22059
+ }
22060
+ function invalidState(statePath, detail) {
22061
+ return new Error(`Invalid mobile state file ${statePath}: ${detail}`);
22062
+ }
22063
+ function readString(value, what, statePath) {
22064
+ if (typeof value !== "string")
22065
+ throw invalidState(statePath, `${what} must be a string`);
22066
+ return value;
22067
+ }
22068
+ function readNonEmptyString(value, what, statePath) {
22069
+ const text = readString(value, what, statePath);
22070
+ if (text === "")
22071
+ throw invalidState(statePath, `${what} must be a non-empty string`);
22072
+ return text;
22073
+ }
22074
+ function readCredentialHash(value, what, statePath) {
22075
+ const text = readString(value, what, statePath);
22076
+ if (!CREDENTIAL_HASH_PATTERN.test(text))
22077
+ throw invalidState(statePath, `${what} must be a 64-character hex string`);
22078
+ return text;
22079
+ }
22080
+ function readRevision(value, what, statePath) {
22081
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0)
22082
+ throw invalidState(statePath, `${what} must be a non-negative integer`);
22083
+ return value;
22084
+ }
22085
+ function readTimestamp(value, what, statePath) {
22086
+ if (typeof value !== "number" || !Number.isFinite(value))
22087
+ throw invalidState(statePath, `${what} must be a finite number`);
22088
+ return value;
22089
+ }
22090
+ function parseDevice(raw, index, statePath) {
22091
+ const what = `devices[${index}]`;
22092
+ if (typeof raw !== "object" || raw === null)
22093
+ throw invalidState(statePath, `${what} must be an object`);
22094
+ const record = raw;
22095
+ return {
22096
+ deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
22097
+ name: readString(record.name, `${what}.name`, statePath),
22098
+ credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
22099
+ grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
22100
+ pairedAt: readTimestamp(record.pairedAt, `${what}.pairedAt`, statePath),
22101
+ lastSeenAt: readTimestamp(record.lastSeenAt, `${what}.lastSeenAt`, statePath)
22102
+ };
22103
+ }
22104
+ function parseReceipt(raw, index, statePath) {
22105
+ const what = `receipts[${index}]`;
22106
+ if (typeof raw !== "object" || raw === null)
22107
+ throw invalidState(statePath, `${what} must be an object`);
22108
+ const record = raw;
22109
+ return {
22110
+ pairRequestId: readNonEmptyString(record.pairRequestId, `${what}.pairRequestId`, statePath),
22111
+ deviceId: readNonEmptyString(record.deviceId, `${what}.deviceId`, statePath),
22112
+ credentialHash: readCredentialHash(record.credentialHash, `${what}.credentialHash`, statePath),
22113
+ grantRevision: readRevision(record.grantRevision, `${what}.grantRevision`, statePath),
22114
+ expiresAt: readTimestamp(record.expiresAt, `${what}.expiresAt`, statePath)
22115
+ };
22116
+ }
22117
+ function parseState(raw, statePath) {
22118
+ if (typeof raw !== "object" || raw === null)
22119
+ throw invalidState(statePath, "root must be an object");
22120
+ const record = raw;
22121
+ if (record.formatVersion !== MOBILE_STATE_FORMAT_VERSION)
22122
+ throw invalidState(statePath, `unsupported formatVersion (expected ${MOBILE_STATE_FORMAT_VERSION})`);
22123
+ if (typeof record.enabled !== "boolean")
22124
+ throw invalidState(statePath, "enabled must be a boolean");
22125
+ if (record.address !== null && typeof record.address !== "string")
22126
+ throw invalidState(statePath, "address must be a string or null");
22127
+ const port = record.port;
22128
+ if (typeof port !== "number" || !Number.isInteger(port) || port < 1 || port > 65535)
22129
+ throw invalidState(statePath, "port must be an integer between 1 and 65535");
22130
+ if (!Array.isArray(record.devices))
22131
+ throw invalidState(statePath, "devices must be an array");
22132
+ if (!Array.isArray(record.receipts))
22133
+ throw invalidState(statePath, "receipts must be an array");
22134
+ const lastConnectedAddress = record.lastConnectedAddress === undefined || record.lastConnectedAddress === null ? null : readString(record.lastConnectedAddress, "lastConnectedAddress", statePath);
22135
+ return {
22136
+ formatVersion: MOBILE_STATE_FORMAT_VERSION,
22137
+ enabled: record.enabled,
22138
+ address: record.address,
22139
+ port,
22140
+ devices: record.devices.map((device, index) => parseDevice(device, index, statePath)),
22141
+ receipts: record.receipts.map((receipt, index) => parseReceipt(receipt, index, statePath)),
22142
+ lastConnectedAddress
22143
+ };
22144
+ }
22145
+ function assertPort(port) {
22146
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
22147
+ throw new Error("port must be an integer between 1 and 65535");
22148
+ return port;
22149
+ }
22150
+ var tempCounter = 0;
22151
+ async function writeFileAtomic(filePath, contents, mode) {
22152
+ const tempPath = `${filePath}.tmp-${process.pid}-${++tempCounter}`;
22153
+ try {
22154
+ await writeFile(tempPath, contents, { mode });
22155
+ if (POSIX)
22156
+ await chmod2(tempPath, mode);
22157
+ await rename2(tempPath, filePath);
22158
+ } finally {
22159
+ await rm(tempPath, { force: true });
22160
+ }
22161
+ }
22162
+ async function writeSecretFile(filePath, contents) {
22163
+ await writeFileAtomic(filePath, contents, 384);
22164
+ }
22165
+ async function readSecretFile(filePath) {
22166
+ try {
22167
+ return await readFile4(filePath, "utf8");
22168
+ } catch (error) {
22169
+ if (isNotFound(error))
22170
+ return;
22171
+ throw error;
22172
+ }
22173
+ }
22174
+ async function loadState(statePath) {
22175
+ let raw;
22176
+ try {
22177
+ raw = await readFile4(statePath, "utf8");
22178
+ } catch (error) {
22179
+ if (isNotFound(error))
22180
+ return freezeState(defaultState());
22181
+ throw error;
22182
+ }
22183
+ let parsed;
22184
+ try {
22185
+ parsed = JSON.parse(raw);
22186
+ } catch {
22187
+ throw invalidState(statePath, "not valid JSON");
22188
+ }
22189
+ return freezeState(parseState(parsed, statePath));
22190
+ }
22191
+
22192
+ class MobileStore {
22193
+ mobileDir;
22194
+ tlsDir;
22195
+ statePath;
22196
+ adminTokenPath;
22197
+ current;
22198
+ queue = Promise.resolve();
22199
+ constructor(mobileDir, initial) {
22200
+ this.mobileDir = mobileDir;
22201
+ this.tlsDir = join6(mobileDir, "tls");
22202
+ this.statePath = join6(mobileDir, "state.json");
22203
+ this.adminTokenPath = join6(mobileDir, "admin-token");
22204
+ this.current = initial;
22205
+ }
22206
+ static async open(nekoDir) {
22207
+ const mobileDir = join6(nekoDir, "mobile");
22208
+ const tlsDir = join6(mobileDir, "tls");
22209
+ await mkdir2(mobileDir, { recursive: true, mode: 448 });
22210
+ await mkdir2(tlsDir, { recursive: true, mode: 448 });
22211
+ if (POSIX) {
22212
+ await chmod2(mobileDir, 448);
22213
+ await chmod2(tlsDir, 448);
22214
+ }
22215
+ return new MobileStore(mobileDir, await loadState(join6(mobileDir, "state.json")));
22216
+ }
22217
+ getState() {
22218
+ return this.current;
22219
+ }
22220
+ updateConfig(patch) {
22221
+ return this.mutate((state) => ({
22222
+ ...state,
22223
+ ...patch.enabled !== undefined ? { enabled: patch.enabled } : {},
22224
+ ...patch.address !== undefined ? { address: patch.address } : {},
22225
+ ...patch.port !== undefined ? { port: assertPort(patch.port) } : {}
22226
+ }));
22227
+ }
22228
+ upsertDevice(device) {
22229
+ return this.mutate((state) => ({
22230
+ ...state,
22231
+ devices: state.devices.some((d) => d.deviceId === device.deviceId) ? state.devices.map((d) => d.deviceId === device.deviceId ? device : d) : [...state.devices, device]
22232
+ }));
22233
+ }
22234
+ removeDevice(deviceId) {
22235
+ return this.mutate((state) => ({
22236
+ ...state,
22237
+ devices: state.devices.filter((d) => d.deviceId !== deviceId)
22238
+ }));
22239
+ }
22240
+ completePairing(input) {
22241
+ return this.mutate((state) => {
22242
+ const now = Date.now();
22243
+ const existing = state.devices.find((d) => d.deviceId === input.deviceId);
22244
+ const grantRevision = existing === undefined ? 1 : existing.grantRevision + 1;
22245
+ const device = {
22246
+ deviceId: input.deviceId,
22247
+ name: input.name,
22248
+ credentialHash: input.credentialHash,
22249
+ grantRevision,
22250
+ pairedAt: input.pairedAt,
22251
+ lastSeenAt: input.pairedAt
22252
+ };
22253
+ const receipt = {
22254
+ pairRequestId: input.pairRequestId,
22255
+ deviceId: input.deviceId,
22256
+ credentialHash: input.credentialHash,
22257
+ grantRevision,
22258
+ expiresAt: input.receiptExpiresAt
22259
+ };
22260
+ return {
22261
+ ...state,
22262
+ devices: existing === undefined ? [...state.devices, device] : state.devices.map((d) => d.deviceId === input.deviceId ? device : d),
22263
+ receipts: [
22264
+ ...state.receipts.filter((r) => r.expiresAt > now && r.pairRequestId !== input.pairRequestId),
22265
+ receipt
22266
+ ]
22267
+ };
22268
+ });
22269
+ }
22270
+ putReceipt(receipt) {
22271
+ return this.mutate((state) => ({
22272
+ ...state,
22273
+ receipts: state.receipts.some((r) => r.pairRequestId === receipt.pairRequestId) ? state.receipts.map((r) => r.pairRequestId === receipt.pairRequestId ? receipt : r) : [...state.receipts, receipt]
22274
+ }));
22275
+ }
22276
+ removeReceipt(pairRequestId) {
22277
+ return this.mutate((state) => ({
22278
+ ...state,
22279
+ receipts: state.receipts.filter((r) => r.pairRequestId !== pairRequestId)
22280
+ }));
22281
+ }
22282
+ touchDevice(deviceId, lastSeenAt) {
22283
+ return this.mutate((state) => {
22284
+ const existing = state.devices.find((d) => d.deviceId === deviceId);
22285
+ if (existing === undefined)
22286
+ return state;
22287
+ return {
22288
+ ...state,
22289
+ devices: state.devices.map((d) => d.deviceId === deviceId ? { ...d, lastSeenAt } : d)
22290
+ };
22291
+ });
22292
+ }
22293
+ rememberConnectedAddress(address) {
22294
+ if (this.current.lastConnectedAddress === address)
22295
+ return Promise.resolve(this.current);
22296
+ return this.mutate((state) => ({ ...state, lastConnectedAddress: address }));
22297
+ }
22298
+ clearMobileAuthorizations() {
22299
+ return this.mutate((state) => ({ ...state, devices: [], receipts: [] }));
22300
+ }
22301
+ ensureAdminToken() {
22302
+ return this.enqueue(async () => {
22303
+ const existing = await readSecretFile(this.adminTokenPath);
22304
+ if (existing !== undefined) {
22305
+ const token = existing.trim();
22306
+ if (ADMIN_TOKEN_PATTERN.test(token))
22307
+ return token;
22308
+ throw new Error(`Invalid mobile admin token file ${this.adminTokenPath}: expected a base64url token`);
22309
+ }
22310
+ const token = randomBytes(MOBILE_ADMIN_TOKEN_BYTES).toString("base64url");
22311
+ await writeSecretFile(this.adminTokenPath, `${token}
22312
+ `);
22313
+ return token;
22314
+ });
22315
+ }
22316
+ mutate(recipe) {
22317
+ return this.enqueue(async () => {
22318
+ const next = freezeState(recipe(this.current));
22319
+ await writeFileAtomic(this.statePath, `${JSON.stringify(next, null, "\t")}
22320
+ `, 384);
22321
+ this.current = next;
22322
+ return next;
22323
+ });
22324
+ }
22325
+ enqueue(task) {
22326
+ const run = this.queue.then(task);
22327
+ this.queue = run.then(() => {
22328
+ return;
22329
+ }, () => {
22330
+ return;
22331
+ });
22332
+ return run;
22333
+ }
22334
+ }
22335
+
22336
+ // packages/gateway/src/mobile/admin.ts
22337
+ var listenerStateSchema = Type6.Union([
22338
+ Type6.Object({ status: Type6.Literal("disabled") }),
22339
+ Type6.Object({ status: Type6.Literal("starting") }),
22340
+ Type6.Object({
22341
+ status: Type6.Literal("listening"),
22342
+ address: Type6.String(),
22343
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
22344
+ bindAddress: Type6.Optional(Type6.String()),
22345
+ mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
22346
+ }),
22347
+ Type6.Object({ status: Type6.Literal("error"), error: Type6.String() }),
22348
+ Type6.Object({ status: Type6.Literal("stopping") })
22349
+ ]);
22350
+ var adminDeviceSchema = Type6.Object({
22351
+ deviceId: Type6.String(),
22352
+ name: Type6.String(),
22353
+ pairedAt: Type6.Integer(),
22354
+ lastSeenAt: Type6.Integer(),
22355
+ online: Type6.Boolean()
22356
+ });
22357
+ var NekoMobileAdmin = contract("neko.mobile-admin", {
21665
22358
  version: 1,
21666
- input: Type6.Object({}, { additionalProperties: false }),
22359
+ input: Type6.Object({ token: Type6.String() }),
21667
22360
  variants: {
21668
- ready: {
22361
+ admin: {
21669
22362
  methods: {
21670
- list: rpc(Type6.Object({ query: Type6.Optional(str), workspaceRoot: Type6.Optional(str) }, { additionalProperties: false }), Type6.Array(marketSkill)),
21671
- detail: rpc(Type6.Object({ name: str, workspaceRoot: Type6.Optional(str) }, { additionalProperties: false }), marketSkill),
21672
- preview: rpc(Type6.Object({ name: str }, { additionalProperties: false }), preview),
21673
- install: rpc(Type6.Object({ name: str, scope, workspaceRoot: Type6.Optional(str), overwrite: Type6.Optional(bool) }, { additionalProperties: false }), installResult),
21674
- uninstall: rpc(Type6.Object({ name: str, scope, workspaceRoot: Type6.Optional(str) }, { additionalProperties: false }), nil)
22363
+ state: rpc(Type6.Null(), Type6.Object({
22364
+ state: listenerStateSchema,
22365
+ candidates: Type6.Array(Type6.String()),
22366
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
22367
+ certificate: Type6.Union([
22368
+ Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }),
22369
+ Type6.Null()
22370
+ ]),
22371
+ devices: Type6.Array(adminDeviceSchema),
22372
+ lastConnectedAddress: Type6.Union([Type6.String(), Type6.Null()]),
22373
+ wsl: Type6.Optional(Type6.Union([
22374
+ Type6.Object({ interop: Type6.Boolean(), error: Type6.Union([Type6.String(), Type6.Null()]) }),
22375
+ Type6.Null()
22376
+ ]))
22377
+ })),
22378
+ enable: rpc(Type6.Object({
22379
+ address: Type6.String(),
22380
+ port: Type6.Optional(Type6.Integer({ minimum: 1, maximum: 65535 }))
22381
+ }), Type6.Object({
22382
+ status: Type6.Literal("listening"),
22383
+ address: Type6.String(),
22384
+ port: Type6.Integer({ minimum: 1, maximum: 65535 }),
22385
+ bindAddress: Type6.Optional(Type6.String()),
22386
+ mode: Type6.Optional(Type6.Union([Type6.Literal("direct"), Type6.Literal("wsl-relay")]))
22387
+ })),
22388
+ disable: rpc(Type6.Null(), listenerStateSchema),
22389
+ createPairing: rpc(Type6.Null(), Type6.Object({ pairingText: Type6.String(), expiresAt: Type6.Integer() })),
22390
+ cancelPairing: rpc(Type6.Null(), Type6.Null()),
22391
+ revoke: rpc(Type6.Object({ deviceId: Type6.String({ minLength: 1 }) }), Type6.Object({ revoked: Type6.Boolean() })),
22392
+ regenerateCertificate: rpc(Type6.Null(), Type6.Object({ fingerprint: Type6.String(), expiresAt: Type6.Integer() }))
21675
22393
  }
21676
22394
  }
21677
22395
  }
21678
22396
  });
21679
- var ApplicationInfoSchema = Type6.Object({
21680
- runningVersion: str,
21681
- installedVersion: nullableString,
21682
- protocolVersion: Type6.Integer({ minimum: 1 }),
21683
- latestVersion: nullableString,
21684
- checkedAt: Type6.Union([integer, nil]),
21685
- checkState: Type6.Enum(["unknown", "checking", "ok", "failed", "development"])
21686
- });
21687
- var NekoApplicationInfo = contract2("neko.application-info", {
21688
- version: 1,
21689
- input: Type6.Object({}),
21690
- variants: { ready: { state: { info: ApplicationInfoSchema }, methods: { check: rpc(nil, ApplicationInfoSchema) } } }
21691
- });
22397
+ function digest(value) {
22398
+ return createHash("sha256").update(value, "utf8").digest();
22399
+ }
22400
+ async function tokenMatches(token, store) {
22401
+ if (store === undefined)
22402
+ return false;
22403
+ let adminToken;
22404
+ try {
22405
+ adminToken = await store.ensureAdminToken();
22406
+ } catch {
22407
+ return false;
22408
+ }
22409
+ const expected = digest(adminToken);
22410
+ const provided = digest(token);
22411
+ return expected.length === provided.length && timingSafeEqual(expected, provided);
22412
+ }
22413
+ function createMobileAdminFactory(deps) {
22414
+ return (input, principal) => {
22415
+ if (isMobilePrincipal(principal))
22416
+ throw new ChordError("forbidden", "mobile principals cannot administer pairing");
22417
+ const guard = async () => {
22418
+ if (!await tokenMatches(input.token, deps.store))
22419
+ throw new ChordError("forbidden", "forbidden");
22420
+ };
22421
+ return {
22422
+ variant: "admin",
22423
+ async state() {
22424
+ await guard();
22425
+ return deps.mobile.adminState();
22426
+ },
22427
+ async enable(params) {
22428
+ await guard();
22429
+ const state = await deps.mobile.enable({ address: params.address, port: params.port });
22430
+ if (state.status !== "listening")
22431
+ throw new ChordError("unavailable", `mobile listener failed: ${state.status}`);
22432
+ return state;
22433
+ },
22434
+ async disable() {
22435
+ await guard();
22436
+ return deps.mobile.disable();
22437
+ },
22438
+ async createPairing() {
22439
+ await guard();
22440
+ return deps.mobile.createPairing();
22441
+ },
22442
+ async cancelPairing() {
22443
+ await guard();
22444
+ deps.mobile.cancelPairing();
22445
+ return null;
22446
+ },
22447
+ async revoke(params) {
22448
+ await guard();
22449
+ return { revoked: await deps.mobile.revoke(params.deviceId) };
22450
+ },
22451
+ async regenerateCertificate() {
22452
+ await guard();
22453
+ return deps.mobile.regenerateCertificate();
22454
+ }
22455
+ };
22456
+ };
22457
+ }
22458
+
22459
+ // packages/gateway/src/mobile/listener.ts
22460
+ import { randomUUID as randomUUID2 } from "crypto";
22461
+ import { createServer } from "https";
22462
+ import { hostname } from "os";
22463
+ import { memoryLinkPair } from "@earendil-works/chord";
22464
+ import { WebSocketServer } from "ws";
21692
22465
 
21693
22466
  // packages/gateway/src/link.ts
21694
- import { webSocketLink } from "@earendil-works/chord";
22467
+ import {
22468
+ createHost,
22469
+ webSocketLink
22470
+ } from "@earendil-works/chord";
21695
22471
  var MAX_LINK_BYTES = 16 * 1024 * 1024;
21696
- var size = (text) => new TextEncoder().encode(text).byteLength;
21697
- function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
22472
+ var asBytes = (data) => data instanceof Uint8Array ? data : new Uint8Array(data);
22473
+ var size = (data) => typeof data === "string" ? new TextEncoder().encode(data).byteLength : data.byteLength;
22474
+ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES, options) {
21698
22475
  let queued = 0, incoming = 0, receiving = false;
21699
22476
  let native;
21700
22477
  const bridge = {
@@ -21710,18 +22487,22 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
21710
22487
  },
21711
22488
  close: (code, reason) => socket.close(code, reason),
21712
22489
  addEventListener(type, handler) {
21713
- if (type === "message")
21714
- socket.addEventListener("message", (event) => {
21715
- const bytes = size(String(event.data));
22490
+ if (type === "message") {
22491
+ const deliver = (data) => {
22492
+ const bytes = size(data);
21716
22493
  if (!receiving)
21717
22494
  incoming += bytes;
21718
22495
  if (bytes > max || incoming > max) {
21719
22496
  native.close("frame limit exceeded");
21720
22497
  return;
21721
22498
  }
21722
- handler(event);
21723
- });
21724
- else
22499
+ handler({ data });
22500
+ };
22501
+ if (socket.on)
22502
+ socket.on("message", (data, isBinary) => deliver(isBinary ? asBytes(data) : new TextDecoder().decode(asBytes(data))));
22503
+ else
22504
+ socket.addEventListener("message", (event) => deliver(typeof event.data === "string" || event.data instanceof Uint8Array ? event.data : String(event.data)));
22505
+ } else
21725
22506
  socket.addEventListener(type, () => {
21726
22507
  if (type === "open")
21727
22508
  queued = 0;
@@ -21733,7 +22514,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
21733
22514
  });
21734
22515
  }
21735
22516
  };
21736
- native = webSocketLink(bridge);
22517
+ native = webSocketLink(bridge, options);
21737
22518
  socket.addEventListener("error", () => native.close("socket error"));
21738
22519
  return {
21739
22520
  get closed() {
@@ -21753,7 +22534,7 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
21753
22534
  }
21754
22535
  if (socket.readyState === 0)
21755
22536
  queued += bytes;
21756
- native.send(frame);
22537
+ return native.send(frame);
21757
22538
  },
21758
22539
  receive(handler) {
21759
22540
  receiving = true;
@@ -21764,47 +22545,17 @@ function boundedWebSocketLink(socket, max = MAX_LINK_BYTES) {
21764
22545
  close: (reason) => native.close(reason)
21765
22546
  };
21766
22547
  }
21767
- function identifiedLink(link, expectedName) {
21768
- let receiver;
21769
- let seen = false, pendingBytes = 0;
21770
- const pending = [];
21771
- link.receive((frame) => {
21772
- if (!seen) {
21773
- if (frame.t !== "hello" || frame.name !== expectedName) {
21774
- link.close("serverId mismatch");
21775
- return;
21776
- }
21777
- seen = true;
21778
- }
21779
- if (receiver)
21780
- receiver(frame);
21781
- else {
21782
- pendingBytes += size(JSON.stringify(frame));
21783
- if (pendingBytes > MAX_LINK_BYTES) {
21784
- link.close("receive queue limit exceeded");
21785
- pending.length = 0;
21786
- return;
21787
- }
21788
- pending.push(frame);
21789
- }
21790
- });
21791
- return {
21792
- get closed() {
21793
- return link.closed;
21794
- },
21795
- send: (frame) => link.send(frame),
21796
- receive(handler) {
21797
- receiver = handler;
21798
- pendingBytes = 0;
21799
- for (const frame of pending.splice(0)) {
21800
- if (link.closed)
21801
- break;
21802
- handler(frame);
21803
- }
22548
+ function createClientHost(link, options = {}) {
22549
+ return createHost({
22550
+ kind: "client",
22551
+ self: { kind: "client" },
22552
+ upstream: {
22553
+ link,
22554
+ principal: { kind: "server" },
22555
+ ...options.onProtocolMismatch ? { onProtocolMismatch: options.onProtocolMismatch } : {}
21804
22556
  },
21805
- onClose: (handler) => link.onClose(handler),
21806
- close: (reason) => link.close(reason)
21807
- };
22557
+ onError: () => {}
22558
+ });
21808
22559
  }
21809
22560
 
21810
22561
  // packages/gateway/src/mobile/adapter.ts
@@ -21817,16 +22568,6 @@ var byMessage = (a, b) => messageTime(b) - messageTime(a) || Number(b.id) - Numb
21817
22568
  // packages/gateway/src/mobile/adapter.ts
21818
22569
  import { Check as Check2 } from "typebox/value";
21819
22570
 
21820
- // packages/gateway/src/client-session.ts
21821
- import { createHost } from "@earendil-works/chord";
21822
-
21823
- // packages/core/src/application-version.ts
21824
- var RUNNING_VERSION = "0.4.2";
21825
- var APPLICATION_PROTOCOL = 4;
21826
- function stableVersion(value) {
21827
- 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)));
21828
- }
21829
-
21830
22571
  // packages/gateway/src/subagent-client.ts
21831
22572
  function subagentClient(connect) {
21832
22573
  let value;
@@ -21967,40 +22708,16 @@ function subagentClient(connect) {
21967
22708
  };
21968
22709
  }
21969
22710
 
21970
- // packages/gateway/src/client-session.ts
21971
- function waitForValue(state, signal) {
21972
- return new Promise((resolve, reject) => {
21973
- let stop = () => {};
21974
- let settled = false;
21975
- const finish = (error, value) => {
21976
- if (settled)
21977
- return;
21978
- settled = true;
21979
- clearTimeout(timer);
21980
- stop();
21981
- signal?.removeEventListener("abort", abort);
21982
- if (error)
21983
- reject(error);
21984
- else
21985
- resolve(value);
21986
- };
21987
- const abort = () => finish(signal?.reason ?? new Error("Cancelled"));
21988
- const timer = setTimeout(() => finish(new Error("Timed out waiting for first snapshot")), 5000);
21989
- stop = state.subscribe((value) => {
21990
- if (value !== undefined)
21991
- finish(undefined, value);
21992
- });
21993
- if (settled)
21994
- stop();
21995
- signal?.addEventListener("abort", abort, { once: true });
21996
- if (signal?.aborted)
21997
- abort();
21998
- });
21999
- }
22000
-
22001
22711
  // packages/gateway/src/mobile/projection.ts
22002
22712
  import { createHash as createHash2 } from "crypto";
22003
22713
 
22714
+ // packages/core/src/application-version.ts
22715
+ var RUNNING_VERSION = "0.4.4";
22716
+ var APPLICATION_PROTOCOL = 6;
22717
+ function stableVersion(value) {
22718
+ 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)));
22719
+ }
22720
+
22004
22721
  // packages/gateway/src/mobile/protocol.ts
22005
22722
  import { Type as Type7 } from "typebox";
22006
22723
  import { Check } from "typebox/value";
@@ -22072,7 +22789,7 @@ function buildHelloResult(serverId, serverName, apiVersion) {
22072
22789
  var opt = Type7.Optional;
22073
22790
  var str2 = Type7.String();
22074
22791
  var int = Type7.Integer({ minimum: 0 });
22075
- var nullable = (schema) => Type7.Union([schema, Type7.Null()]);
22792
+ var nullable2 = (schema) => Type7.Union([schema, Type7.Null()]);
22076
22793
  var MobileContentRefSchema = Type7.Object({
22077
22794
  sessionId: str2,
22078
22795
  agentId: opt(str2),
@@ -22161,7 +22878,7 @@ var MobileSubagentRoundSchema = Type7.Object({
22161
22878
  approvalCount: int,
22162
22879
  readonly: Type7.Boolean(),
22163
22880
  createdAt: Type7.Number(),
22164
- endedAt: nullable(Type7.Number()),
22881
+ endedAt: nullable2(Type7.Number()),
22165
22882
  usage: opt(MobileUsageSchema)
22166
22883
  });
22167
22884
  var MobileAgentSchema = Type7.Object({
@@ -22169,18 +22886,25 @@ var MobileAgentSchema = Type7.Object({
22169
22886
  role: str2,
22170
22887
  title: str2,
22171
22888
  status: Type7.Union([Type7.Literal("idle"), Type7.Literal("queued"), Type7.Literal("running")]),
22172
- activeRoundId: nullable(int),
22889
+ activeRoundId: nullable2(int),
22173
22890
  lastRoundId: int,
22174
22891
  roundCount: int,
22175
22892
  approvalCount: int
22176
22893
  });
22894
+ var MobileQueuedSchema = Type7.Object({
22895
+ id: int,
22896
+ mode: Type7.Union([Type7.Literal("steer"), Type7.Literal("followUp")]),
22897
+ preview: Type7.String(),
22898
+ images: int
22899
+ });
22177
22900
  var MobileChatStatusSchema = Type7.Object({
22178
22901
  busy: Type7.Boolean(),
22179
- status: nullable(str2),
22902
+ queued: opt(Type7.Array(MobileQueuedSchema)),
22903
+ status: nullable2(str2),
22180
22904
  usage: MobileUsageSchema,
22181
22905
  taskUsage: opt(MobileUsageSchema),
22182
- contextUsage: opt(Type7.Object({ tokens: nullable(Type7.Number()), window: Type7.Number(), percent: nullable(Type7.Number()) })),
22183
- agent: Type7.Object({ model: nullable(str2), thinkingLevel: nullable(str2) }),
22906
+ contextUsage: opt(Type7.Object({ tokens: nullable2(Type7.Number()), window: Type7.Number(), percent: nullable2(Type7.Number()) })),
22907
+ agent: Type7.Object({ model: nullable2(str2), thinkingLevel: nullable2(str2) }),
22184
22908
  subagents: Type7.Array(MobileSubagentRoundSchema),
22185
22909
  agents: Type7.Array(MobileAgentSchema),
22186
22910
  approvalCount: int,
@@ -22190,8 +22914,8 @@ var MobileChatStatusSchema = Type7.Object({
22190
22914
  var MobileChatSnapshotSchema = Type7.Object({
22191
22915
  ...MobileChatStatusSchema.properties,
22192
22916
  items: Type7.Array(MobileChatItemSchema),
22193
- historyBefore: nullable(str2),
22194
- streaming: nullable(MobileStreamingSchema)
22917
+ historyBefore: nullable2(str2),
22918
+ streaming: nullable2(MobileStreamingSchema)
22195
22919
  });
22196
22920
  var MobileWorkspaceSchema = Type7.Object({ root: str2, name: str2 });
22197
22921
  var MobileSessionSummarySchema = Type7.Object({
@@ -22224,7 +22948,7 @@ var sessionListResult = Type7.Object({
22224
22948
  nextCursor: opt(str2)
22225
22949
  });
22226
22950
  var subscription = { subscriptionId: Type7.String({ minLength: 1 }) };
22227
- var pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable(str2) });
22951
+ var pageResult = Type7.Object({ items: Type7.Array(MobileChatItemSchema), historyBefore: nullable2(str2) });
22228
22952
  var pageParams = Type7.Object({
22229
22953
  ...subscription,
22230
22954
  before: Type7.String({ minLength: 1 }),
@@ -22259,6 +22983,34 @@ var MOBILE_API_V1_METHODS = {
22259
22983
  params: Type7.Object({ ...subscription, requestId: Type7.String({ minLength: 1, maxLength: 128 }) }),
22260
22984
  result: Type7.Object({ status: Type7.Union([Type7.Literal("accepted"), Type7.Literal("unknown")]) })
22261
22985
  },
22986
+ "chat.steer": {
22987
+ params: Type7.Object({ ...subscription, id: int }),
22988
+ result: Type7.Object({
22989
+ result: Type7.Union([
22990
+ Type7.Literal("steered"),
22991
+ Type7.Literal("withdrawn"),
22992
+ Type7.Literal("placed"),
22993
+ Type7.Literal("settled"),
22994
+ Type7.Literal("unknown")
22995
+ ]),
22996
+ id: opt(int),
22997
+ text: opt(Type7.String()),
22998
+ images: opt(int)
22999
+ })
23000
+ },
23001
+ "chat.withdraw": {
23002
+ params: Type7.Object({ ...subscription, id: int }),
23003
+ result: Type7.Object({
23004
+ result: Type7.Union([
23005
+ Type7.Literal("withdrawn"),
23006
+ Type7.Literal("placed"),
23007
+ Type7.Literal("settled"),
23008
+ Type7.Literal("unknown")
23009
+ ]),
23010
+ text: opt(Type7.String()),
23011
+ images: opt(int)
23012
+ })
23013
+ },
22262
23014
  "chat.abort": { params: Type7.Object(subscription), result: Type7.Object({ accepted: Type7.Boolean() }) },
22263
23015
  "subagents.open": {
22264
23016
  params: Type7.Object({ ...subscription, agentId: opt(Type7.String({ minLength: 1 })), taskId: opt(int) }),
@@ -22338,7 +23090,7 @@ var patchBody = {
22338
23090
  seq: Type7.Integer({ minimum: 1 }),
22339
23091
  upserts: Type7.Array(MobileChatItemSchema),
22340
23092
  status: opt(MobileChatStatusSchema),
22341
- streaming: opt(nullable(MobileStreamingSchema)),
23093
+ streaming: opt(nullable2(MobileStreamingSchema)),
22342
23094
  streamingAppend: opt(Type7.Object({ text: opt(str2), thinking: opt(str2) }))
22343
23095
  };
22344
23096
  var MOBILE_API_V1_NOTIFICATIONS = {
@@ -22546,6 +23298,9 @@ function mapStatus(view, options = {}) {
22546
23298
  const latest = view.compaction?.latest;
22547
23299
  return {
22548
23300
  busy: view.busy,
23301
+ ...view.queued === undefined ? {} : {
23302
+ queued: view.queued.map(({ id, mode, preview, images }) => ({ id, mode, preview, images }))
23303
+ },
22549
23304
  status: view.status,
22550
23305
  usage: mapUsage(view.usage),
22551
23306
  ...view.taskUsage === undefined ? {} : { taskUsage: mapUsage(view.taskUsage) },
@@ -22850,7 +23605,7 @@ function chordToMobileCode(error) {
22850
23605
  return "internal";
22851
23606
  }
22852
23607
  }
22853
- async function remote3(work, invalidAs = "invalid_params") {
23608
+ async function remote2(work, invalidAs = "invalid_params") {
22854
23609
  try {
22855
23610
  return await work();
22856
23611
  } catch (error) {
@@ -23032,6 +23787,10 @@ class MobileAdapter {
23032
23787
  return this.chatSubmit(params);
23033
23788
  case "chat.submissionStatus":
23034
23789
  return this.chatSubmissionStatus(params);
23790
+ case "chat.steer":
23791
+ return this.chatSteer(params);
23792
+ case "chat.withdraw":
23793
+ return this.chatWithdraw(params);
23035
23794
  case "chat.abort":
23036
23795
  return this.chatAbort(params);
23037
23796
  case "subagents.open":
@@ -23064,14 +23823,17 @@ class MobileAdapter {
23064
23823
  }
23065
23824
  }
23066
23825
  async workspacesList() {
23067
- const state = await waitForValue(this.services().workspaces.state);
23826
+ const { workspaces } = this.services();
23827
+ await workspaces.ready;
23828
+ const state = workspaces.state.value;
23068
23829
  return {
23069
23830
  workspaces: state.workspaces.map((workspace) => ({ root: workspace.path, name: baseName(workspace.path) }))
23070
23831
  };
23071
23832
  }
23072
23833
  async registeredWorkspaceRoots() {
23073
- const state = await waitForValue(this.services().workspaces.state);
23074
- return state.workspaces.map((workspace) => workspace.path);
23834
+ const { workspaces } = this.services();
23835
+ await workspaces.ready;
23836
+ return workspaces.state.value.workspaces.map((workspace) => workspace.path);
23075
23837
  }
23076
23838
  async sessionsList(params) {
23077
23839
  const roots = await this.registeredWorkspaceRoots();
@@ -23080,7 +23842,9 @@ class MobileAdapter {
23080
23842
  const offset = params.cursor === undefined ? 0 : Number.parseInt(params.cursor, 10);
23081
23843
  if (!Number.isInteger(offset) || offset < 0)
23082
23844
  throw new MobileMethodError("invalid_params", "invalid cursor");
23083
- const directory = await waitForValue(this.services().directory.state);
23845
+ const { directory: directoryRef } = this.services();
23846
+ await directoryRef.ready;
23847
+ const directory = directoryRef.state.value;
23084
23848
  let sessions = directory.sessions.filter((session) => session.workspaceRoot === params.workspace);
23085
23849
  if (params.query !== undefined) {
23086
23850
  const query = params.query.toLowerCase();
@@ -23106,7 +23870,7 @@ class MobileAdapter {
23106
23870
  if (!roots.includes(params.workspace))
23107
23871
  throw new MobileMethodError("invalid_params", `unknown workspace: ${params.workspace}`);
23108
23872
  this.assertAuthorized();
23109
- const created = await remote3(() => this.services().management.create({ cwd: params.workspace }));
23873
+ const created = await remote2(() => this.services().management.create({ cwd: params.workspace }));
23110
23874
  return {
23111
23875
  id: created.id,
23112
23876
  workspace: created.workspaceRoot,
@@ -23143,16 +23907,13 @@ class MobileAdapter {
23143
23907
  const generation = ++this.attachGeneration;
23144
23908
  this.closeSubagent();
23145
23909
  this.releaseChat();
23146
- const attachment = await remote3(() => services.management.attach(params.sessionId));
23910
+ const attachment = await remote2(() => services.management.attach(params.sessionId));
23147
23911
  if (generation !== this.attachGeneration || this.disposed)
23148
23912
  throw new MobileMethodError("gone", "superseded by a newer attach");
23149
23913
  const chat = services.openChat(attachment);
23150
23914
  const stopView = chat.view.subscribe(() => {});
23151
23915
  try {
23152
- await remote3(async () => {
23153
- await chat.ready;
23154
- await waitForValue(chat.view);
23155
- });
23916
+ await remote2(() => chat.ready);
23156
23917
  } catch (error) {
23157
23918
  stopView();
23158
23919
  chat.release();
@@ -23214,7 +23975,7 @@ class MobileAdapter {
23214
23975
  this.attachGeneration += 1;
23215
23976
  this.closeSubagent();
23216
23977
  this.releaseChat();
23217
- await remote3(() => this.services().management.detach(null)).catch(() => {
23978
+ await remote2(() => this.services().management.detach(null)).catch(() => {
23218
23979
  return;
23219
23980
  });
23220
23981
  return null;
@@ -23245,7 +24006,7 @@ class MobileAdapter {
23245
24006
  const state = this.requireChat(params);
23246
24007
  const busy = state.chat.view.value?.busy ?? false;
23247
24008
  this.assertAuthorized();
23248
- await remote3(() => state.chat.submit({
24009
+ await remote2(() => state.chat.submit({
23249
24010
  content: params.text,
23250
24011
  requestId: params.requestId,
23251
24012
  ...params.mode === "steer" ? { whenBusy: "steer" } : {}
@@ -23259,10 +24020,20 @@ class MobileAdapter {
23259
24020
  const accepted = await this.runtime.submissionAccepted(state.sessionId, params.requestId);
23260
24021
  return { status: accepted ? "accepted" : "unknown" };
23261
24022
  }
24023
+ async chatSteer(params) {
24024
+ const state = this.requireChat(params);
24025
+ this.assertAuthorized();
24026
+ return remote2(() => state.chat.steer({ id: params.id }), "rejected");
24027
+ }
24028
+ async chatWithdraw(params) {
24029
+ const state = this.requireChat(params);
24030
+ this.assertAuthorized();
24031
+ return remote2(() => state.chat.withdraw({ id: params.id }), "rejected");
24032
+ }
23262
24033
  async chatAbort(params) {
23263
24034
  const state = this.requireChat(params);
23264
24035
  this.assertAuthorized();
23265
- await remote3(() => state.chat.abort(null));
24036
+ await remote2(() => state.chat.abort(null));
23266
24037
  return { accepted: true };
23267
24038
  }
23268
24039
  async approvalRecords(state) {
@@ -23341,7 +24112,7 @@ class MobileAdapter {
23341
24112
  this.assertAuthorized();
23342
24113
  const handle = openSubagent({ ...state.attachment, taskId: params.taskId });
23343
24114
  try {
23344
- const status = await remote3(() => handle.decideApproval({
24115
+ const status = await remote2(() => handle.decideApproval({
23345
24116
  id: params.id,
23346
24117
  version: params.version,
23347
24118
  decision: params.decision === "allow" ? "allowed" : "denied"
@@ -23421,7 +24192,7 @@ class MobileAdapter {
23421
24192
  return () => listeners.delete(listener);
23422
24193
  },
23423
24194
  older: async (before) => {
23424
- const page = await remote3(() => client.history({ before }));
24195
+ const page = await remote2(() => client.history({ before }));
23425
24196
  return { items: page.items, next: page.next };
23426
24197
  }
23427
24198
  };
@@ -23465,7 +24236,7 @@ class MobileAdapter {
23465
24236
  this.assertAuthorized();
23466
24237
  const handle = openSubagent({ ...chat.attachment, taskId: params.taskId });
23467
24238
  try {
23468
- await remote3(() => handle.stop({ taskId: params.taskId }));
24239
+ await remote2(() => handle.stop({ taskId: params.taskId }));
23469
24240
  } finally {
23470
24241
  handle.release();
23471
24242
  }
@@ -23479,9 +24250,9 @@ class MobileAdapter {
23479
24250
  const state = this.subagentState;
23480
24251
  if (state === undefined || state.agentId !== params.agentId || state.chatSubscriptionId !== chat.subscriptionId)
23481
24252
  throw new MobileMethodError("gone", "subagent view is no longer open");
23482
- chunk = await remote3(() => state.client.getImage(input));
24253
+ chunk = await remote2(() => state.client.getImage(input));
23483
24254
  } else {
23484
- chunk = await remote3(() => chat.chat.getImage(input));
24255
+ chunk = await remote2(() => chat.chat.getImage(input));
23485
24256
  }
23486
24257
  const bytes = Buffer.from(chunk.data, "base64").subarray(0, MOBILE_LIMITS.chunkBytes);
23487
24258
  const nextOffset = params.offset + bytes.length;
@@ -24412,9 +25183,6 @@ function describeRelayError(error, address, port, bindAddress) {
24412
25183
  return `Windows \u4E2D\u7EE7\u542F\u52A8\u5931\u8D25\uFF1A${error.message}`;
24413
25184
  }
24414
25185
  }
24415
- function isMobilePrincipal2(principal) {
24416
- return typeof principal === "object" && principal !== null && principal.kind === "mobile";
24417
- }
24418
25186
  function toMessage(error) {
24419
25187
  if (error instanceof Error) {
24420
25188
  const code = error.code;
@@ -24593,7 +25361,7 @@ class MobileGateway {
24593
25361
  await store.clearMobileAuthorizations();
24594
25362
  this.certificate = certificate;
24595
25363
  this.pairingValue?.invalidate();
24596
- await this.chord.invalidatePrincipal(isMobilePrincipal2);
25364
+ await this.chord.invalidatePrincipal(isMobilePrincipal);
24597
25365
  for (const connection of [...this.connections.values()])
24598
25366
  connection.close(MOBILE_CLOSE_REVOKED, "revoked");
24599
25367
  const current = this.stateValue;
@@ -24669,11 +25437,7 @@ class MobileGateway {
24669
25437
  store.rememberConnectedAddress(learned).catch((error) => this.onError?.(toError(error)));
24670
25438
  }
24671
25439
  }
24672
- const clientHost = createHost2({
24673
- kind: "client",
24674
- upstream: identifiedLink(clientEnd, this.serverId),
24675
- onError: () => {}
24676
- });
25440
+ const clientHost = createClientHost(clientEnd);
24677
25441
  let servicesValue;
24678
25442
  const adapter = new MobileAdapter({
24679
25443
  socket,
@@ -24685,11 +25449,11 @@ class MobileGateway {
24685
25449
  services: () => {
24686
25450
  if (servicesValue === undefined) {
24687
25451
  servicesValue = {
24688
- directory: clientHost.get(NekoSessionDirectory, {}, { at: this.serverId }),
24689
- management: clientHost.get(NekoSessionManagement, {}, { at: this.serverId }),
24690
- workspaces: clientHost.get(NekoWorkspaces, {}, { at: this.serverId }),
24691
- openChat: (attachment) => clientHost.get(NekoChat, attachment, { at: this.serverId }),
24692
- openSubagent: (input) => clientHost.get(NekoSubagent, input, { at: this.serverId })
25452
+ directory: clientHost.hold(NekoSessionDirectory, {}),
25453
+ management: clientHost.hold(NekoSessionManagement, {}),
25454
+ workspaces: clientHost.hold(NekoWorkspaces, {}),
25455
+ openChat: (attachment) => clientHost.hold(NekoChat, attachment),
25456
+ openSubagent: (input) => clientHost.hold(NekoSubagent, input)
24693
25457
  };
24694
25458
  }
24695
25459
  return servicesValue;
@@ -24713,10 +25477,13 @@ class MobileGateway {
24713
25477
  if (this.notificationCleanup !== undefined)
24714
25478
  return;
24715
25479
  const [serverEnd, clientEnd] = memoryLinkPair();
24716
- this.chord.accept(serverEnd, { principal: { connectionId: "mobile-notification-hub" }, kind: "client" });
24717
- const host = createHost2({ kind: "client", upstream: identifiedLink(clientEnd, this.serverId), onError: () => {} });
24718
- const directory = host.get(NekoSessionDirectory, {}, { at: this.serverId });
24719
- const workspaces = host.get(NekoWorkspaces, {}, { at: this.serverId });
25480
+ this.chord.accept(serverEnd, {
25481
+ principal: { kind: "local", connectionId: "mobile-notification-hub" },
25482
+ kind: "client"
25483
+ });
25484
+ const host = createClientHost(clientEnd);
25485
+ const directory = host.hold(NekoSessionDirectory, {});
25486
+ const workspaces = host.hold(NekoWorkspaces, {});
24720
25487
  const last = { directory: -1, workspaces: -1 };
24721
25488
  const unsubscribe = [
24722
25489
  directory.state.subscribe(() => {
@@ -25013,14 +25780,7 @@ async function removeGatewayPortFile(nekoDir, serverId) {
25013
25780
 
25014
25781
  // packages/gateway/src/server-services.ts
25015
25782
  import { randomUUID as randomUUID4 } from "crypto";
25016
- import {
25017
- ChordError as ChordError5,
25018
- cell as cell3,
25019
- hostPrincipal as hostPrincipal3,
25020
- listener as listener3,
25021
- local as local3,
25022
- method as method3
25023
- } from "@earendil-works/chord";
25783
+ import { ChordError as ChordError5, local as local3, method as method2 } from "@earendil-works/chord";
25024
25784
 
25025
25785
  // packages/core/src/application-updates.ts
25026
25786
  import { mkdir as mkdir3, readFile as readFile6, rename as rename3, rm as rm3, writeFile as writeFile3 } from "fs/promises";
@@ -25177,12 +25937,8 @@ import { open as open2 } from "fs/promises";
25177
25937
  import { basename as basename3, extname } from "path";
25178
25938
  import {
25179
25939
  ChordError as ChordError4,
25180
- cell as cell2,
25181
- hostPrincipal as hostPrincipal2,
25182
- listener as listener2,
25183
25940
  local as local2,
25184
- opaque as opaque2,
25185
- resource as resource2
25941
+ opaque as opaque2
25186
25942
  } from "@earendil-works/chord";
25187
25943
  import { withAbortSignal as withAbortSignal4 } from "@earendil-works/chord/context";
25188
25944
  import { UsageDoc as UsageDoc4 } from "@earendil-works/pi-durable";
@@ -25283,15 +26039,7 @@ class ImageUploads {
25283
26039
  }
25284
26040
 
25285
26041
  // packages/gateway/src/subagent-host.ts
25286
- import {
25287
- ChordError as ChordError3,
25288
- cell,
25289
- hostPrincipal,
25290
- listener,
25291
- local,
25292
- opaque,
25293
- resource
25294
- } from "@earendil-works/chord";
26042
+ import { ChordError as ChordError3, local, opaque } from "@earendil-works/chord";
25295
26043
  import { UsageDoc as UsageDoc3 } from "@earendil-works/pi-durable";
25296
26044
  import { Type as Type8 } from "typebox";
25297
26045
  async function openReader(runtime, childId, parentId, context, through, historyUsage) {
@@ -25426,31 +26174,27 @@ var ChildProjection = local("neko.subagent-projection", {
25426
26174
  });
25427
26175
  function installSubagentHost(host, runtime, context) {
25428
26176
  host.serve(ChildProjection, (input, _principal, self2) => {
25429
- const { revision } = self2.state({ revision: cell(Type8.Integer(), 0) });
25430
- const { reader } = self2.resources({
25431
- reader: resource({
25432
- rev: 1,
25433
- async open() {
25434
- const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => input.agentId.startsWith("round-") ? d.taskId === Number(input.agentId.slice(6)) : (d.agentId ?? agentIdFor(d.childId)) === input.agentId);
25435
- if (!entry)
25436
- throw new ChordError3("gone", "Subagent unavailable");
25437
- const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
25438
- try {
25439
- const initial = await r.refresh();
25440
- await host.root((ctx) => revision.replace(ctx, initial));
25441
- return r;
25442
- } catch (error) {
25443
- r.close();
25444
- throw error;
25445
- }
25446
- },
25447
- close: (r) => r.close()
25448
- })
26177
+ const revision = self2.state("revision", Type8.Integer(), 0);
26178
+ const reader = self2.resource("reader", {
26179
+ version: 1,
26180
+ async open() {
26181
+ const entry = (await listDelegations(runtime.harness, input.parentId, context)).find((d) => input.agentId.startsWith("round-") ? d.taskId === Number(input.agentId.slice(6)) : (d.agentId ?? agentIdFor(d.childId)) === input.agentId);
26182
+ if (!entry)
26183
+ throw new ChordError3("gone", "Subagent unavailable");
26184
+ const r = await openReader(runtime, entry.childId, input.parentId, context, entry.historyThrough, entry.historyUsage);
26185
+ try {
26186
+ const initial = await r.refresh();
26187
+ await host.root((ctx) => revision.replace(ctx, initial));
26188
+ return r;
26189
+ } catch (error) {
26190
+ r.close();
26191
+ throw error;
26192
+ }
26193
+ },
26194
+ close: (r) => r.close()
25449
26195
  });
25450
- self2.listen({
25451
- view: listener(reader, (r) => r, async (_event, ctx) => {
25452
- revision.replace(ctx, await reader.value.refresh());
25453
- })
26196
+ self2.on("view", reader, (r) => r, async (_event, ctx) => {
26197
+ revision.replace(ctx, await reader.value.refresh());
25454
26198
  });
25455
26199
  return { revision, reader: self2.expose(reader) };
25456
26200
  });
@@ -25466,23 +26210,24 @@ function installSubagentHost(host, runtime, context) {
25466
26210
  throw new ChordError3("forbidden", "Subagent does not belong to this session");
25467
26211
  return entry;
25468
26212
  };
25469
- const { target } = self2.resources({ target: resource({ rev: 1, deps: [owner], open: check }) });
25470
- const shared = self2.get(ChildProjection, { parentId: Number(input.sessionId), agentId: input.agentId ?? `round-${input.taskId}` }, { principal: hostPrincipal });
25471
- const { authorizedRevision } = self2.state({ authorizedRevision: cell(Type8.Integer(), 0) });
25472
- self2.listen({
25473
- authorizedView: listener(shared.revision, async (value, ctx) => {
25474
- try {
25475
- target.value;
25476
- await check();
25477
- if (value)
25478
- authorizedRevision.replace(ctx, value);
25479
- } catch {
25480
- authorizedRevision.replace(ctx, 0);
25481
- }
25482
- })
26213
+ const target = self2.resource("target", { version: 1, deps: [owner], open: check });
26214
+ const shared = self2.get(ChildProjection, {
26215
+ parentId: Number(input.sessionId),
26216
+ agentId: input.agentId ?? `round-${input.taskId}`
26217
+ });
26218
+ const authorizedRevision = self2.state("authorizedRevision", Type8.Integer(), 0);
26219
+ self2.on("authorizedView", shared.revision, async (value, ctx) => {
26220
+ try {
26221
+ target.value;
26222
+ await check();
26223
+ if (value)
26224
+ authorizedRevision.replace(ctx, value);
26225
+ } catch {
26226
+ authorizedRevision.replace(ctx, 0);
26227
+ }
25483
26228
  });
25484
26229
  return {
25485
- kind: "ready",
26230
+ variant: "ready",
25486
26231
  revision: authorizedRevision,
25487
26232
  chunk: async (request) => {
25488
26233
  await check();
@@ -25542,6 +26287,7 @@ var emptyView = {
25542
26287
  agent: { model: null, thinkingLevel: null },
25543
26288
  cwd: "",
25544
26289
  busy: false,
26290
+ queued: [],
25545
26291
  status: null,
25546
26292
  usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 },
25547
26293
  items: [],
@@ -25638,65 +26384,61 @@ async function openProjection(runtime, session, context) {
25638
26384
  function serveSessions(host, runtime, context) {
25639
26385
  installSubagentHost(host, runtime, context);
25640
26386
  host.serve(Owner, (_input, _principal, self2) => {
25641
- const { owner } = self2.resources({
25642
- owner: resource2({
25643
- rev: 1,
25644
- open: () => ({ active: null }),
25645
- close: (value) => {
25646
- value.active = null;
25647
- }
25648
- })
26387
+ const owner = self2.resource("owner", {
26388
+ version: 1,
26389
+ open: () => ({ active: null }),
26390
+ close: (value) => {
26391
+ value.active = null;
26392
+ }
25649
26393
  });
25650
26394
  return { owner: self2.expose(owner) };
25651
26395
  });
25652
26396
  host.serve(Projection, (input, _principal, self2) => {
25653
- const { view } = self2.state({ view: cell2(ChatViewSchema, emptyView) });
25654
- const { projection } = self2.resources({
25655
- projection: resource2({
25656
- rev: 1,
25657
- async open({ signal }) {
25658
- const session = await runtime.getConversation(Number(input.id));
25659
- if (!session)
25660
- throw new ChordError4("gone", "Session unavailable");
25661
- const opened = await openProjection(runtime, session, context);
25662
- try {
25663
- const initial = await opened.project();
25664
- await host.root((ctx) => view.replace(ctx, initial), { signal });
25665
- return opened;
25666
- } catch (error) {
25667
- opened.close();
25668
- throw error;
25669
- }
25670
- },
25671
- close: (value) => value.close()
25672
- })
26397
+ const view = self2.state("view", ChatViewSchema, emptyView);
26398
+ const projection = self2.resource("projection", {
26399
+ version: 1,
26400
+ async open({ signal }) {
26401
+ const session = await runtime.getConversation(Number(input.id));
26402
+ if (!session)
26403
+ throw new ChordError4("gone", "Session unavailable");
26404
+ const opened = await openProjection(runtime, session, context);
26405
+ try {
26406
+ const initial = await opened.project();
26407
+ await host.root((ctx) => view.replace(ctx, initial), { signal });
26408
+ return opened;
26409
+ } catch (error) {
26410
+ opened.close();
26411
+ throw error;
26412
+ }
26413
+ },
26414
+ close: (value) => value.close()
25673
26415
  });
25674
- self2.listen({
25675
- view: listener2(projection, (value) => value, async (_event, ctx) => {
25676
- view.replace(ctx, await projection.value.project());
25677
- })
26416
+ self2.on("view", projection, (value) => value, async (_event, ctx) => {
26417
+ view.replace(ctx, await projection.value.project());
25678
26418
  });
25679
26419
  return { view, reader: self2.expose(projection) };
25680
26420
  });
25681
26421
  host.serve(NekoChat, (input, principal, self2) => {
25682
26422
  const owner = self2.get(Owner, {}, { principal });
25683
- const shared = self2.get(Projection, { id: input.sessionId }, { principal: hostPrincipal2 });
25684
- const { uploads, projection } = self2.resources({
25685
- uploads: resource2({ rev: 1, open: () => new ImageUploads, close: (value) => value.close() }),
25686
- projection: resource2({
25687
- rev: 1,
25688
- deps: [owner],
25689
- async open() {
25690
- if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
25691
- throw new ChordError4("forbidden", "Attachment does not belong to this connection");
25692
- const session = await runtime.getConversation(Number(input.sessionId));
25693
- if (!session)
25694
- throw new ChordError4("gone", "Session unavailable");
25695
- return {
25696
- session
25697
- };
25698
- }
25699
- })
26423
+ const shared = self2.get(Projection, { id: input.sessionId });
26424
+ const uploads = self2.resource("uploads", {
26425
+ version: 1,
26426
+ open: () => new ImageUploads,
26427
+ close: (value) => value.close()
26428
+ });
26429
+ const projection = self2.resource("projection", {
26430
+ version: 1,
26431
+ deps: [owner],
26432
+ async open() {
26433
+ if (owner.owner?.active?.attachmentId !== input.attachmentId || owner.owner.active.sessionId !== input.sessionId)
26434
+ throw new ChordError4("forbidden", "Attachment does not belong to this connection");
26435
+ const session = await runtime.getConversation(Number(input.sessionId));
26436
+ if (!session)
26437
+ throw new ChordError4("gone", "Session unavailable");
26438
+ return {
26439
+ session
26440
+ };
26441
+ }
25700
26442
  });
25701
26443
  const safeUpload = (action) => {
25702
26444
  try {
@@ -25706,7 +26448,7 @@ function serveSessions(host, runtime, context) {
25706
26448
  }
25707
26449
  };
25708
26450
  const behavior = {
25709
- kind: "ready",
26451
+ variant: "ready",
25710
26452
  view: shared.view,
25711
26453
  draftImage: async ({ id, offset }) => safeUpload(() => uploads.value.read(id, offset)),
25712
26454
  importImage: async (path, ctx) => {
@@ -25813,6 +26555,8 @@ function serveSessions(host, runtime, context) {
25813
26555
  await projection.value.session.abort();
25814
26556
  return null;
25815
26557
  },
26558
+ withdraw: ({ id }) => projection.value.session.withdraw(id),
26559
+ steer: ({ id }) => projection.value.session.steer(id),
25816
26560
  skills: () => runtime.listSkills(projection.value.session.id),
25817
26561
  jobOutput: async ({ jobId, since }) => {
25818
26562
  try {
@@ -26568,7 +27312,11 @@ var ServerState = local3("neko.server-state", {
26568
27312
  directory: DirectoryStateSchema,
26569
27313
  workspaces: WorkspaceStateSchema,
26570
27314
  credentials: CredentialStateSchema,
26571
- refreshCredentials: method3(Type10.Null(), Type10.Null())
27315
+ searchSettings: SearchSettingsStateSchema,
27316
+ contextSettings: ContextSettingsStateSchema,
27317
+ refreshCredentials: method2(Type10.Null(), Type10.Null()),
27318
+ refreshSearchSettings: method2(Type10.Null(), Type10.Null()),
27319
+ refreshContextSettings: method2(Type10.Null(), Type10.Null())
26572
27320
  }
26573
27321
  });
26574
27322
  async function createNekoServerServices(options) {
@@ -26576,6 +27324,7 @@ async function createNekoServerServices(options) {
26576
27324
  const updates = new UpdateChecker(options.updates);
26577
27325
  await updates.local();
26578
27326
  const updateListeners = new Set;
27327
+ const info = (value) => ({ ...value, serverId: options.serverId });
26579
27328
  let published = JSON.stringify(updates.state), closed = false;
26580
27329
  const publishUpdates = () => {
26581
27330
  if (closed)
@@ -26588,22 +27337,20 @@ async function createNekoServerServices(options) {
26588
27337
  listener(updates.state);
26589
27338
  };
26590
27339
  host.serve(NekoApplicationInfo, (_input, _principal, self2) => {
26591
- const state = self2.state({ info: cell3(ApplicationInfoSchema, updates.state) });
26592
- self2.listen({
26593
- updates: listener3({
26594
- subscribe: (fn) => {
26595
- updateListeners.add(fn);
26596
- return () => updateListeners.delete(fn);
26597
- }
26598
- }, (value, ctx) => state.info.replace(ctx, value))
26599
- });
27340
+ const current = self2.state("info", ApplicationInfoSchema, info(updates.state));
27341
+ self2.on("updates", {
27342
+ subscribe: (fn) => {
27343
+ updateListeners.add(fn);
27344
+ return () => updateListeners.delete(fn);
27345
+ }
27346
+ }, (value, ctx) => current.replace(ctx, info(value)));
26600
27347
  return {
26601
- kind: "ready",
26602
- info: state.info,
27348
+ variant: "ready",
27349
+ info: current,
26603
27350
  check: async () => {
26604
27351
  await updates.check(true);
26605
27352
  publishUpdates();
26606
- return updates.state;
27353
+ return info(updates.state);
26607
27354
  }
26608
27355
  };
26609
27356
  });
@@ -26647,7 +27394,13 @@ async function createNekoServerServices(options) {
26647
27394
  status: info.status
26648
27395
  }));
26649
27396
  const readStatuses = async () => Object.fromEntries(await Promise.all(CREDENTIAL_PROVIDERS.map(async (spec) => [spec.id, await runtime.credentialStatus(spec.id)])));
26650
- const [sessions, workspaces, statuses] = await Promise.all([list(), runtime.listWorkspaces(), readStatuses()]);
27397
+ const [sessions, workspaces, statuses, search, contextSettings0] = await Promise.all([
27398
+ list(),
27399
+ runtime.listWorkspaces(),
27400
+ readStatuses(),
27401
+ runtime.searchSettings(),
27402
+ runtime.contextSettings()
27403
+ ]);
26651
27404
  let mutationTail = Promise.resolve();
26652
27405
  const serialize = (fn) => {
26653
27406
  const result = mutationTail.catch(() => {}).then(fn);
@@ -26655,43 +27408,64 @@ async function createNekoServerServices(options) {
26655
27408
  return result;
26656
27409
  };
26657
27410
  host.serve(ServerState, (_input, _principal, self2) => {
26658
- const state = self2.state({
26659
- directory: cell3(DirectoryStateSchema, { revision: 1, sessions }),
26660
- workspaces: cell3(WorkspaceStateSchema, { revision: 1, workspaces }),
26661
- credentials: cell3(CredentialStateSchema, { revision: 1, statuses })
27411
+ const directory = self2.state("directory", DirectoryStateSchema, { revision: 1, sessions });
27412
+ const workspaceState = self2.state("workspaces", WorkspaceStateSchema, { revision: 1, workspaces });
27413
+ const credentials = self2.state("credentials", CredentialStateSchema, { revision: 1, statuses });
27414
+ const searchSettings = self2.state("searchSettings", SearchSettingsStateSchema, { revision: 1, ...search });
27415
+ const contextSettings = self2.state("contextSettings", ContextSettingsStateSchema, {
27416
+ revision: 1,
27417
+ ...contextSettings0
26662
27418
  });
26663
- self2.listen({
26664
- directory: listener3({ subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
26665
- if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
26666
- "app.neko.session-metadata",
26667
- "app.neko.activity",
26668
- "app.neko.conversation",
26669
- "app.neko.workspaces",
26670
- "pi.agent"
26671
- ].includes(change.record.kind)))
26672
- return;
26673
- await serialize(async () => {
26674
- const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
26675
- state.directory.replace(ctx, { revision: state.directory.value.revision + 1, sessions: items });
26676
- state.workspaces.replace(ctx, { revision: state.workspaces.value.revision + 1, workspaces: roots });
26677
- });
26678
- })
27419
+ const refreshContextSettings = async (_input, ctx) => {
27420
+ const next = await runtime.contextSettings();
27421
+ const { revision, ...current } = contextSettings.value;
27422
+ if (JSON.stringify(next) !== JSON.stringify(current))
27423
+ contextSettings.replace(ctx, { revision: revision + 1, ...next });
27424
+ return null;
27425
+ };
27426
+ const refreshSearchSettings = async (_input, ctx) => {
27427
+ const next = await runtime.searchSettings();
27428
+ const current = searchSettings.value;
27429
+ if (next.selected !== current.selected)
27430
+ searchSettings.replace(ctx, { revision: current.revision + 1, ...next });
27431
+ return null;
27432
+ };
27433
+ self2.on("directory", { subscribe: runtime.harness.subscribeCommits.bind(runtime.harness) }, async (event, ctx) => {
27434
+ if (!event.changes.some((change) => change.type === "conversation" || change.type === "document" && [
27435
+ "app.neko.session-metadata",
27436
+ "app.neko.activity",
27437
+ "app.neko.conversation",
27438
+ "app.neko.workspaces",
27439
+ "pi.agent"
27440
+ ].includes(change.record.kind)))
27441
+ return;
27442
+ await serialize(async () => {
27443
+ const [items, roots] = await Promise.all([list(), runtime.listWorkspaces()]);
27444
+ directory.replace(ctx, { revision: directory.value.revision + 1, sessions: items });
27445
+ workspaceState.replace(ctx, { revision: workspaceState.value.revision + 1, workspaces: roots });
27446
+ });
26679
27447
  });
26680
27448
  return {
26681
- ...state,
27449
+ directory,
27450
+ workspaces: workspaceState,
27451
+ credentials,
27452
+ searchSettings,
27453
+ contextSettings,
26682
27454
  async refreshCredentials(_input, ctx) {
26683
- state.credentials.replace(ctx, {
26684
- revision: state.credentials.value.revision + 1,
27455
+ credentials.replace(ctx, {
27456
+ revision: credentials.value.revision + 1,
26685
27457
  statuses: await readStatuses()
26686
27458
  });
26687
27459
  return null;
26688
- }
27460
+ },
27461
+ refreshSearchSettings,
27462
+ refreshContextSettings
26689
27463
  };
26690
- }, { eager: { input: {} } });
26691
- const shared = (self2) => self2.get(ServerState, {}, { principal: hostPrincipal3 });
26692
- host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ kind: "ready", state: shared(self2).directory }));
27464
+ }, { lifetime: "host" });
27465
+ const shared = (self2) => self2.get(ServerState, {});
27466
+ host.serve(NekoSessionDirectory, (_input, _principal, self2) => ({ variant: "ready", state: shared(self2).directory }));
26693
27467
  host.serve(NekoWorkspaces, (_input, _principal, self2) => ({
26694
- kind: "ready",
27468
+ variant: "ready",
26695
27469
  state: shared(self2).workspaces,
26696
27470
  browse: (input) => browseWorkspaces(input),
26697
27471
  add: (input) => serialize(() => runtime.addWorkspace(input.path))
@@ -26699,7 +27473,7 @@ async function createNekoServerServices(options) {
26699
27473
  host.serve(NekoCredentials, (_input, _principal, self2) => {
26700
27474
  const hub = shared(self2);
26701
27475
  return {
26702
- kind: "ready",
27476
+ variant: "ready",
26703
27477
  state: hub.credentials,
26704
27478
  save: async (input, ctx) => {
26705
27479
  await runtime.saveCredential(input.provider, input.key);
@@ -26711,8 +27485,34 @@ async function createNekoServerServices(options) {
26711
27485
  }
26712
27486
  };
26713
27487
  });
27488
+ host.serve(NekoSearchSettings, (_input, _principal, self2) => {
27489
+ const hub = shared(self2);
27490
+ return {
27491
+ variant: "ready",
27492
+ state: hub.searchSettings,
27493
+ set: (input, ctx) => serialize(async () => {
27494
+ await runtime.setSearchProvider(input.provider);
27495
+ return hub.refreshSearchSettings(null, ctx);
27496
+ })
27497
+ };
27498
+ });
27499
+ host.serve(NekoContextSettings, (_input, _principal, self2) => {
27500
+ const hub = shared(self2);
27501
+ return {
27502
+ variant: "ready",
27503
+ state: hub.contextSettings,
27504
+ set: (input, ctx) => serialize(async () => {
27505
+ await runtime.setContextSettings(input);
27506
+ return hub.refreshContextSettings(null, ctx);
27507
+ }),
27508
+ reset: (_input, ctx) => serialize(async () => {
27509
+ await runtime.resetContextSettings();
27510
+ return hub.refreshContextSettings(null, ctx);
27511
+ })
27512
+ };
27513
+ });
26714
27514
  host.serve(NekoModels, () => ({
26715
- kind: "ready",
27515
+ variant: "ready",
26716
27516
  list: async () => [...runtime.listModels()].map((value) => ({
26717
27517
  ...value,
26718
27518
  input: [...value.input],
@@ -26720,7 +27520,7 @@ async function createNekoServerServices(options) {
26720
27520
  }))
26721
27521
  }));
26722
27522
  host.serve(NekoWorkspaceFiles, () => ({
26723
- kind: "ready",
27523
+ variant: "ready",
26724
27524
  search: async (input, ctx) => {
26725
27525
  const roots = await runtime.listWorkspaces();
26726
27526
  if (!roots.some((root) => root.path === input.workspaceRoot))
@@ -26738,7 +27538,7 @@ async function createNekoServerServices(options) {
26738
27538
  return found;
26739
27539
  };
26740
27540
  return {
26741
- kind: "ready",
27541
+ variant: "ready",
26742
27542
  create: (input) => serialize(async () => summary((await runtime.createConversation(input)).id)),
26743
27543
  fork: (id) => serialize(async () => summary((await runtime.forkConversation(toConversationId(id))).id)),
26744
27544
  archive: async (id) => {
@@ -26761,7 +27561,7 @@ async function createNekoServerServices(options) {
26761
27561
  };
26762
27562
  });
26763
27563
  host.serve(NekoSkillMarket, () => ({
26764
- kind: "ready",
27564
+ variant: "ready",
26765
27565
  list: (input) => market.list(input.query).then((skills) => Promise.all(skills.map((skill) => withInstallState(skill, input.workspaceRoot)))).catch(toRemote),
26766
27566
  detail: (input) => market.detail(input.name).then((skill) => withInstallState(skill, input.workspaceRoot)).catch(toRemote),
26767
27567
  preview: (input) => market.preview(input.name).then((value) => ({ ...value, files: [...value.files] })).catch(toRemote),
@@ -26876,7 +27676,7 @@ class UnixLink {
26876
27676
  }
26877
27677
  send(frame) {
26878
27678
  try {
26879
- this.#native.send(frame);
27679
+ return this.#native.send(frame);
26880
27680
  } catch (error) {
26881
27681
  this.#native.close("pending byte limit exceeded");
26882
27682
  throw error;
@@ -26985,7 +27785,7 @@ function createWsListener(options) {
26985
27785
  server.on("error", (error) => options.onError?.(error));
26986
27786
  server.on("connection", (socket) => {
26987
27787
  socket.on("error", (error) => options.onError?.(error));
26988
- accept(boundedWebSocketLink(socket, options.maxPendingBytes));
27788
+ accept(boundedWebSocketLink(socket, options.maxPendingBytes, { acceptPacked: false }));
26989
27789
  });
26990
27790
  },
26991
27791
  async close() {
@@ -27011,6 +27811,7 @@ async function startGateway(options = {}) {
27011
27811
  const storagePath = options.storagePath ?? fileConfig.storagePath ?? DEFAULT_STORAGE_PATH;
27012
27812
  const host = options.host ?? fileConfig.gateway?.host ?? DEFAULT_HOST;
27013
27813
  const port = options.port !== undefined ? options.port : fileConfig.gateway?.port ?? (process.platform === "win32" ? 0 : DEFAULT_PORT);
27814
+ const searchProvider = options.searchProvider ?? fileConfig.webSearch?.provider;
27014
27815
  const marketUrl = options.marketUrl ?? fileConfig.skills?.marketUrl ?? DEFAULT_SKILL_MARKET_URL;
27015
27816
  const nekoDir = dirname9(storagePath);
27016
27817
  const serverId = options.serverId ?? await loadOrCreateServerId(nekoDir);
@@ -27022,6 +27823,8 @@ async function startGateway(options = {}) {
27022
27823
  ...options.storage !== "memory" ? { storagePath } : {},
27023
27824
  ...options.credentialsPath !== undefined ? { credentialsPath: options.credentialsPath } : {},
27024
27825
  ...options.apiKey !== undefined ? { apiKey: options.apiKey } : {},
27826
+ ...searchProvider !== undefined ? { searchProvider } : {},
27827
+ ...fileConfig.context !== undefined ? { contextSettings: fileConfig.context } : {},
27025
27828
  model,
27026
27829
  thinkingLevel: options.thinkingLevel ?? fileConfig.thinkingLevel ?? "high",
27027
27830
  ...options.cwd !== undefined ? { cwd: options.cwd } : {},
@@ -27052,8 +27855,9 @@ async function startGateway(options = {}) {
27052
27855
  }
27053
27856
  async function bindGateway(runtime, options) {
27054
27857
  const { context, host, port, socketPath, serverId, nekoDir } = options;
27055
- const chord = createHost3({
27056
- name: serverId,
27858
+ const chord = createHost2({
27859
+ name: GATEWAY_HOST,
27860
+ self: HOST_PRINCIPAL,
27057
27861
  dev: true,
27058
27862
  onError: (error) => options.onError?.(error instanceof Error ? error : new Error(String(error))),
27059
27863
  principalKey: gatewayPrincipalKey
@@ -27062,6 +27866,7 @@ async function bindGateway(runtime, options) {
27062
27866
  host: chord,
27063
27867
  runtime,
27064
27868
  context,
27869
+ serverId,
27065
27870
  marketUrl: options.marketUrl,
27066
27871
  ...options.updates ? { updates: options.updates } : {},
27067
27872
  ...options.updateIntervalMs ? { updateIntervalMs: options.updateIntervalMs } : {},
@@ -27077,7 +27882,10 @@ async function bindGateway(runtime, options) {
27077
27882
  }
27078
27883
  try {
27079
27884
  for (const listener of listeners)
27080
- await listener.start((link) => chord.accept(link, { principal: { connectionId: randomUUID5() } }));
27885
+ await listener.start((link) => chord.accept(link, {
27886
+ principal: { kind: "local", connectionId: randomUUID5() },
27887
+ onProtocolMismatch: ({ peer }) => options.onError?.(new Error(`Rejected a client on Chord protocol ${peer ?? "unknown"}; restart or update it.`))
27888
+ }));
27081
27889
  } catch (error) {
27082
27890
  await Promise.allSettled(listeners.map((listener) => listener.close()));
27083
27891
  await chord.close();
@@ -27140,16 +27948,6 @@ async function bindGateway(runtime, options) {
27140
27948
  }).catch((error) => options.onError?.(error instanceof Error ? error : new Error(String(error))));
27141
27949
  return gateway;
27142
27950
  }
27143
- function gatewayPrincipalKey(principal) {
27144
- if (typeof principal === "object" && principal !== null) {
27145
- const record = principal;
27146
- if (record.kind === "mobile" && typeof record.deviceId === "string" && typeof record.grantRevision === "number") {
27147
- const connectionId = typeof record.connectionId === "string" ? record.connectionId : "";
27148
- return `mobile:${record.deviceId}:${record.grantRevision}:${connectionId}`;
27149
- }
27150
- }
27151
- return JSON.stringify(principal) ?? "unknown";
27152
- }
27153
27951
  function toStartupError(error, info) {
27154
27952
  const message = error instanceof Error ? error.message : String(error);
27155
27953
  const existingServerId = info.socketPath === null ? undefined : serverIdFromSocketPath(info.socketPath);