neko-harness 0.4.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,13 +4,47 @@
4
4
  <meta charset="UTF-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <meta name="color-scheme" content="light" />
7
+ <script>
8
+ // 首屏前应用外观偏好(与 src/client/lib/appearance.ts 的键名/属性名对拍,test/appearance.test.ts)
9
+ (() => {
10
+ try {
11
+ const data = JSON.parse(localStorage.getItem("neko.appearance") || "null");
12
+ if (!data || typeof data !== "object") return;
13
+ const attrs = {
14
+ theme: "data-theme",
15
+ font: "data-chat-font",
16
+ size: "data-chat-size",
17
+ weight: "data-chat-weight",
18
+ leading: "data-chat-leading",
19
+ };
20
+ for (const key of Object.keys(attrs)) {
21
+ const value = data[key];
22
+ if (typeof value === "string" && /^[a-z0-9-]{1,16}$/.test(value))
23
+ document.documentElement.setAttribute(attrs[key], value);
24
+ }
25
+ const meta = document.querySelector('meta[name="color-scheme"]');
26
+ if (data.theme === "dark" && meta) meta.setAttribute("content", "dark");
27
+ } catch {}
28
+ })();
29
+ </script>
30
+ <script>
31
+ // 首屏前同步界面语言到 <html lang>(与 src/client/lib/language.ts 的键名对拍,test/language.test.ts)
32
+ (() => {
33
+ try {
34
+ document.documentElement.setAttribute(
35
+ "lang",
36
+ localStorage.getItem("neko.language") === "en" ? "en" : "zh-CN",
37
+ );
38
+ } catch {}
39
+ })();
40
+ </script>
7
41
  <link
8
42
  rel="icon"
9
43
  href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='26' fill='%23ffb7c5'/%3E%3Ctext x='50' y='70' font-size='58' text-anchor='middle' fill='%23ffffff' font-family='sans-serif' font-weight='bold'%3En%3C/text%3E%3C/svg%3E"
10
44
  />
11
45
  <title>neko harness</title>
12
- <script type="module" crossorigin src="/assets/index-vrkP1dHF.js"></script>
13
- <link rel="stylesheet" crossorigin href="/assets/index-wmHY1s4T.css">
46
+ <script type="module" crossorigin src="/assets/index-v4Ct9JMn.js"></script>
47
+ <link rel="stylesheet" crossorigin href="/assets/index-DCZeTvcw.css">
14
48
  </head>
15
49
  <body>
16
50
  <div id="root"></div>
package/dist/gateway.js CHANGED
@@ -17022,14 +17022,20 @@ function normalizeEnabledProviders(saved) {
17022
17022
  // packages/core/src/credentials.ts
17023
17023
  import { InMemoryCredentialStore } from "@earendil-works/pi-ai";
17024
17024
  var STALE_LOCK_MS = 30000;
17025
+ var LOCK_ATTEMPTS = 100;
17026
+ var LOCK_RETRY_MS = 25;
17025
17027
  var DEFAULT_CREDENTIALS_PATH = join2(homedir2(), ".neko", "credentials.json");
17026
17028
 
17027
17029
  class FileCredentialStore {
17028
17030
  path;
17029
17031
  #renameDeps;
17030
- constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}) {
17032
+ #lockAttempts;
17033
+ #lockRetryMs;
17034
+ constructor(path = DEFAULT_CREDENTIALS_PATH, renameDeps = {}, lock = {}) {
17031
17035
  this.path = path;
17032
17036
  this.#renameDeps = renameDeps;
17037
+ this.#lockAttempts = lock.lockAttempts ?? LOCK_ATTEMPTS;
17038
+ this.#lockRetryMs = lock.lockRetryMs ?? LOCK_RETRY_MS;
17033
17039
  }
17034
17040
  async#read() {
17035
17041
  try {
@@ -17065,7 +17071,7 @@ class FileCredentialStore {
17065
17071
  lock = await open(lockPath, "wx", 384);
17066
17072
  break;
17067
17073
  } catch (error) {
17068
- if (error.code !== "EEXIST" || attempt >= 100)
17074
+ if (error.code !== "EEXIST" || attempt >= this.#lockAttempts)
17069
17075
  throw new Error("Credential storage is busy or unavailable.");
17070
17076
  if (!reclaimed) {
17071
17077
  const info = await stat(lockPath).catch(() => {
@@ -17077,7 +17083,7 @@ class FileCredentialStore {
17077
17083
  continue;
17078
17084
  }
17079
17085
  }
17080
- await new Promise((resolve) => setTimeout(resolve, 25));
17086
+ await new Promise((resolve) => setTimeout(resolve, this.#lockRetryMs));
17081
17087
  }
17082
17088
  }
17083
17089
  const temporary = `${this.path}.${crypto.randomUUID()}.tmp`;
@@ -17235,6 +17241,9 @@ function accessibleWorkspace(directory) {
17235
17241
  }
17236
17242
  return path;
17237
17243
  }
17244
+ var WORKSPACE_UNKNOWN = "WORKSPACE_UNKNOWN: This workspace is not in the list.";
17245
+ var WORKSPACE_LAST = "WORKSPACE_LAST: Keep at least one workspace.";
17246
+ var WORKSPACE_BUSY = "WORKSPACE_BUSY: Stop or finish the running sessions of this workspace first.";
17238
17247
 
17239
17248
  // packages/core/src/projection.ts
17240
17249
  function userText(content) {
@@ -19605,7 +19614,9 @@ class JobManager {
19605
19614
  #reporter = createReporter();
19606
19615
  #runner = this.#createRunner();
19607
19616
  extension;
19608
- constructor() {
19617
+ #startWindowMs;
19618
+ constructor(options = {}) {
19619
+ this.#startWindowMs = options.startWindowMs ?? JOB_LIMITS.startWindowMs;
19609
19620
  this.extension = this.#createExtension();
19610
19621
  }
19611
19622
  #closing = false;
@@ -19795,7 +19806,7 @@ class JobManager {
19795
19806
  throw error;
19796
19807
  }
19797
19808
  const live = entry;
19798
- await Promise.race([live.exited, delay(JOB_LIMITS.startWindowMs, context.abortSignal)]);
19809
+ await Promise.race([live.exited, delay(this.#startWindowMs, context.abortSignal)]);
19799
19810
  live.windowOpen = false;
19800
19811
  if (live.suppressReport)
19801
19812
  await live.exited;
@@ -20996,6 +21007,8 @@ var SessionMetadataDoc = defineDoc5({
20996
21007
  namingUsage: { models: {}, tools: {} }
20997
21008
  })
20998
21009
  });
21010
+ var NAMING_REQUEST_TIMEOUT_MS = 20000;
21011
+ var NAMING_RETRY_DELAY_MS = 2000;
20999
21012
  function shortText(value, limit = 60) {
21000
21013
  return [
21001
21014
  ...stripVTControlCharacters(value).replace(/\p{Cc}/gu, " ").replace(/\s+/g, " ").trim()
@@ -21033,7 +21046,7 @@ function mergeUsage(base, extra) {
21033
21046
  }
21034
21047
  return result;
21035
21048
  }
21036
- function namingTask(canUse) {
21049
+ function namingTask(canUse, { requestTimeoutMs, retryDelayMs }) {
21037
21050
  let active = 0;
21038
21051
  const waits = new Set;
21039
21052
  return defineTask3({
@@ -21084,7 +21097,7 @@ function namingTask(canUse) {
21084
21097
  return { status: "running", checkpoint: { phase: "generate", attempt: d.attempts } };
21085
21098
  }, context);
21086
21099
  let title = "", usage;
21087
- const signal = AbortSignal.any([runtime.signal, AbortSignal.timeout(20000)]);
21100
+ const signal = AbortSignal.any([runtime.signal, AbortSignal.timeout(requestTimeoutMs)]);
21088
21101
  try {
21089
21102
  const budget = Math.max(32, Math.min(12000, Math.floor((model.contextWindow - 512) / 4)));
21090
21103
  const response = await Promise.race([
@@ -21123,7 +21136,7 @@ function namingTask(canUse) {
21123
21136
  d.answer = "";
21124
21137
  } else
21125
21138
  d.naming = d.attempts >= 2 ? "fallback" : "pending";
21126
- return title || d.attempts >= 2 ? { status: "terminal", outcome: { status: "completed", result: null } } : { status: "running", checkpoint: { phase: "wait", until: runtime.now() + 2000 } };
21139
+ return title || d.attempts >= 2 ? { status: "terminal", outcome: { status: "completed", result: null } } : { status: "running", checkpoint: { phase: "wait", until: runtime.now() + retryDelayMs } };
21127
21140
  }, context);
21128
21141
  } finally {
21129
21142
  active--;
@@ -21155,8 +21168,11 @@ class SessionMetadataManager {
21155
21168
  storage;
21156
21169
  context;
21157
21170
  report;
21158
- constructor(harness, storage, context, registry, report = console.error, canUseModel = async () => true) {
21159
- this.task = namingTask(canUseModel);
21171
+ constructor(harness, storage, context, registry, report = console.error, canUseModel = async () => true, timings = {}) {
21172
+ this.task = namingTask(canUseModel, {
21173
+ requestTimeoutMs: timings.requestTimeoutMs ?? NAMING_REQUEST_TIMEOUT_MS,
21174
+ retryDelayMs: timings.retryDelayMs ?? NAMING_RETRY_DELAY_MS
21175
+ });
21160
21176
  this.harness = harness;
21161
21177
  this.storage = storage;
21162
21178
  this.context = context;
@@ -22605,6 +22621,14 @@ var WorkspacesDoc = defineDoc8({
22605
22621
  fork: "initial",
22606
22622
  initial: () => ({ items: [] })
22607
22623
  });
22624
+ var RemovedWorkspacesDoc = defineDoc8({
22625
+ kind: "app.neko.workspaces-removed",
22626
+ version: 1,
22627
+ scope: "conversation",
22628
+ history: "latest",
22629
+ fork: "initial",
22630
+ initial: () => ({ paths: [] })
22631
+ });
22608
22632
  var SearchSettingsDoc = defineDoc8({
22609
22633
  kind: "app.neko.search-settings",
22610
22634
  version: 1,
@@ -22736,7 +22760,7 @@ async function createNekoRuntime(options = {}) {
22736
22760
  registry.install(createSubagentExtension(async (id, requestId, cwd) => {
22737
22761
  resourceSnapshots.stage(id, requestId, cwd, await loadResources(cwd));
22738
22762
  }, resourceSnapshots.discard, (ref) => guardProvider(ref)));
22739
- const jobs = new JobManager;
22763
+ const jobs = new JobManager(options.timings?.jobStartWindowMs === undefined ? {} : { startWindowMs: options.timings.jobStartWindowMs });
22740
22764
  registry.install(jobs.extension);
22741
22765
  registry.install(createTodoExtension());
22742
22766
  let questions;
@@ -22754,6 +22778,7 @@ async function createNekoRuntime(options = {}) {
22754
22778
  models,
22755
22779
  registry,
22756
22780
  settings: {
22781
+ ...options.timings?.retryBaseDelayMs === undefined ? {} : { retry: { baseDelayMs: options.timings.retryBaseDelayMs } },
22757
22782
  get compaction() {
22758
22783
  return compactionPolicy(contextState, allModels().filter((model) => !isModelProvider(model.provider) || enabled.has(model.provider)).map((model) => model.contextWindow));
22759
22784
  }
@@ -22822,20 +22847,57 @@ async function createNekoRuntime(options = {}) {
22822
22847
  contextState = resolveContextSettings(options.contextSettings, savedContext);
22823
22848
  return contextState;
22824
22849
  };
22825
- const registerWorkspace = async (path) => {
22850
+ const removedWorkspaces = async () => new Set((await harness.snapshot(RemovedWorkspacesDoc, conversation.id, context))?.paths ?? []);
22851
+ const registerWorkspace = async (path, explicit) => {
22826
22852
  await harness.commit(async (tx) => {
22827
22853
  const doc = await tx.doc(WorkspacesDoc, conversation.id);
22854
+ const removed = await tx.doc(RemovedWorkspacesDoc, conversation.id);
22855
+ if (removed.paths.includes(path)) {
22856
+ if (!explicit)
22857
+ return;
22858
+ removed.paths = removed.paths.filter((entry) => entry !== path);
22859
+ }
22828
22860
  if (!doc.items.some((item) => item.path === path))
22829
22861
  doc.items.push({ path, createdAt: Date.now() });
22830
22862
  }, context);
22863
+ };
22864
+ const addWorkspace = async (input) => {
22865
+ const path = accessibleWorkspace(input);
22866
+ await registerWorkspace(path, true);
22831
22867
  return (await harness.snapshot(WorkspacesDoc, conversation.id, context)).items.find((item) => item.path === path);
22832
22868
  };
22833
- const addWorkspace = async (path) => registerWorkspace(accessibleWorkspace(path));
22834
22869
  for (const record of [...existing, { id: conversation.id }]) {
22835
22870
  const target = await harness.conversation(record.id, context);
22836
22871
  if (target)
22837
- await registerWorkspace(savedWorkspace((await target.agent(context)).cwd));
22838
- }
22872
+ await registerWorkspace(savedWorkspace((await target.agent(context)).cwd), false);
22873
+ }
22874
+ await registerWorkspace(defaultCwd, true);
22875
+ const removeWorkspace = async (path) => {
22876
+ const items = (await harness.snapshot(WorkspacesDoc, conversation.id, context))?.items ?? [];
22877
+ if (!items.some((item) => item.path === path))
22878
+ throw new Error(WORKSPACE_UNKNOWN);
22879
+ if (items.length <= 1)
22880
+ throw new Error(WORKSPACE_LAST);
22881
+ for (const record of await scanConversations()) {
22882
+ if (record.owner)
22883
+ continue;
22884
+ if ((await harness.snapshot(ArchivedDoc, record.id, context))?.archived)
22885
+ continue;
22886
+ const target = await harness.conversation(record.id, context);
22887
+ if (!target || savedWorkspace((await target.agent(context)).cwd) !== path)
22888
+ continue;
22889
+ const info = await harness.snapshot(SessionMetadataDoc, record.id, context);
22890
+ if (info?.status === "running" || info?.awaiting === true || info?.awaitingPlan === true)
22891
+ throw new Error(WORKSPACE_BUSY);
22892
+ }
22893
+ await harness.commit(async (tx) => {
22894
+ const doc = await tx.doc(WorkspacesDoc, conversation.id);
22895
+ const removed = await tx.doc(RemovedWorkspacesDoc, conversation.id);
22896
+ doc.items = doc.items.filter((item) => item.path !== path);
22897
+ if (!removed.paths.includes(path))
22898
+ removed.paths.push(path);
22899
+ }, context);
22900
+ };
22839
22901
  const jobIo = (id) => ({
22840
22902
  commit: (change) => harness.commit(change, context),
22841
22903
  state: () => harness.snapshot(JobsDoc, id, context)
@@ -22852,7 +22914,10 @@ async function createNekoRuntime(options = {}) {
22852
22914
  };
22853
22915
  if (existing.length === 0)
22854
22916
  await touchConversation(conversation.id);
22855
- const metadata = new SessionMetadataManager(harness, storage, context, registry, options.onReport, (provider, modelId) => guardProvider({ provider, modelId }).then(() => true, () => false));
22917
+ const metadata = new SessionMetadataManager(harness, storage, context, registry, options.onReport, (provider, modelId) => guardProvider({ provider, modelId }).then(() => true, () => false), {
22918
+ ...options.timings?.namingRequestTimeoutMs === undefined ? {} : { requestTimeoutMs: options.timings.namingRequestTimeoutMs },
22919
+ ...options.timings?.namingRetryDelayMs === undefined ? {} : { retryDelayMs: options.timings.namingRetryDelayMs }
22920
+ });
22856
22921
  if (existing.length === 0)
22857
22922
  await metadata.initialize(conversation.id);
22858
22923
  for (const record of existing)
@@ -23212,13 +23277,14 @@ async function createNekoRuntime(options = {}) {
23212
23277
  },
23213
23278
  touchConversation,
23214
23279
  addWorkspace,
23280
+ removeWorkspace,
23215
23281
  async listWorkspaces() {
23216
23282
  return [...(await harness.snapshot(WorkspacesDoc, conversation.id, context))?.items ?? []];
23217
23283
  },
23218
23284
  async forkConversation(id) {
23219
23285
  const source = await sessionFor(id);
23220
23286
  const agent = await source.conversation.agent(context);
23221
- await registerWorkspace(savedWorkspace(agent.cwd));
23287
+ await registerWorkspace(savedWorkspace(agent.cwd), false);
23222
23288
  const newest = await source.conversation.entries({}, 1, undefined, context);
23223
23289
  const at = newest.items[0]?.id;
23224
23290
  const sourceMetadata = at === undefined ? undefined : await harness.snapshot(SessionMetadataDoc, id, context);
@@ -23315,6 +23381,7 @@ async function createNekoRuntime(options = {}) {
23315
23381
  async listConversations() {
23316
23382
  const records = await scanConversations();
23317
23383
  const infos = [];
23384
+ const removed = await removedWorkspaces();
23318
23385
  for (const record of records) {
23319
23386
  if (record.owner)
23320
23387
  continue;
@@ -23323,6 +23390,9 @@ async function createNekoRuntime(options = {}) {
23323
23390
  const target = await harness.conversation(record.id, context);
23324
23391
  if (!target)
23325
23392
  continue;
23393
+ const workspaceRoot = savedWorkspace((await target.agent(context)).cwd);
23394
+ if (removed.has(workspaceRoot))
23395
+ continue;
23326
23396
  const info = await harness.snapshot(SessionMetadataDoc, record.id, context);
23327
23397
  infos.push({
23328
23398
  title: info.title,
@@ -23335,7 +23405,7 @@ async function createNekoRuntime(options = {}) {
23335
23405
  model: (await target.agent(context)).model ?? null,
23336
23406
  id: record.id,
23337
23407
  archived: false,
23338
- workspaceRoot: savedWorkspace((await target.agent(context)).cwd),
23408
+ workspaceRoot,
23339
23409
  lastUsedAt: (await harness.snapshot(ActivityDoc, record.id, context))?.lastUsedAt ?? 0
23340
23410
  });
23341
23411
  }
@@ -24064,13 +24134,14 @@ var page = Type8.Object({
24064
24134
  next: nullableString
24065
24135
  });
24066
24136
  var NekoWorkspaces = contract("neko.workspaces", {
24067
- version: 1,
24137
+ version: 2,
24068
24138
  input: Type8.Object({}, { additionalProperties: false }),
24069
24139
  variants: {
24070
24140
  ready: {
24071
24141
  state: { state: WorkspaceStateSchema },
24072
24142
  methods: {
24073
24143
  add: rpc(Type8.Object({ path: str }, { additionalProperties: false }), WorkspaceSummarySchema),
24144
+ remove: rpc(Type8.Object({ path: str }, { additionalProperties: false }), Type8.Null()),
24074
24145
  browse: rpc(browse, page)
24075
24146
  }
24076
24147
  }
@@ -24969,8 +25040,8 @@ function subagentClient(connect) {
24969
25040
  import { createHash as createHash2 } from "crypto";
24970
25041
 
24971
25042
  // packages/core/src/application-version.ts
24972
- var RUNNING_VERSION = "0.4.8";
24973
- var APPLICATION_PROTOCOL = 8;
25043
+ var RUNNING_VERSION = "0.5.0";
25044
+ var APPLICATION_PROTOCOL = 9;
24974
25045
  function stableVersion(value) {
24975
25046
  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)));
24976
25047
  }
@@ -30067,6 +30138,7 @@ async function createNekoServerServices(options) {
30067
30138
  "app.neko.activity",
30068
30139
  "app.neko.conversation",
30069
30140
  "app.neko.workspaces",
30141
+ "app.neko.workspaces-removed",
30070
30142
  "pi.agent"
30071
30143
  ].includes(change.record.kind)))
30072
30144
  return;
@@ -30101,7 +30173,11 @@ async function createNekoServerServices(options) {
30101
30173
  variant: "ready",
30102
30174
  state: shared(self2).workspaces,
30103
30175
  browse: (input) => browseWorkspaces(input),
30104
- add: (input) => serialize(() => runtime.addWorkspace(input.path))
30176
+ add: (input) => serialize(() => runtime.addWorkspace(input.path)),
30177
+ remove: (input) => serialize(async () => {
30178
+ await runtime.removeWorkspace(input.path);
30179
+ return null;
30180
+ })
30105
30181
  }));
30106
30182
  host.serve(NekoCredentials, (_input, _principal, self2) => {
30107
30183
  const hub = shared(self2);
@@ -30490,7 +30566,8 @@ async function startGateway(options = {}) {
30490
30566
  ...options.extensions !== undefined ? { extensions: options.extensions } : {},
30491
30567
  ...options.promptHome !== undefined ? { promptHome: options.promptHome } : {},
30492
30568
  ...fileConfig.shell?.path !== undefined ? { shellPath: fileConfig.shell.path } : {},
30493
- ...options.shellResolver !== undefined ? { shellResolver: options.shellResolver } : {}
30569
+ ...options.shellResolver !== undefined ? { shellResolver: options.shellResolver } : {},
30570
+ ...options.timings !== undefined ? { timings: options.timings } : {}
30494
30571
  });
30495
30572
  try {
30496
30573
  return await bindGateway(runtime, {