@visual-z/xeno 0.1.2 → 0.1.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/README.md CHANGED
@@ -17,7 +17,7 @@ is ever summarized by a model, and nothing is ever rewritten in place.
17
17
  messages[] frozen system prefix + handoff + this node's events
18
18
  ```
19
19
 
20
- Version **0.1.2**. The on-disk session format will still change; `0.x` is that
20
+ Version **0.1.4**. The on-disk session format will still change; `0.x` is that
21
21
  statement. Published as `@visual-z/xeno`; the license is `UNLICENSED` (readable and installable, no reuse granted).
22
22
 
23
23
  ## Install
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@visual-z/xeno",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "license": "UNLICENSED",
5
5
  "type": "module",
6
6
  "description": "Segmented append-only agent log with pure context and handoff projections",
@@ -27,6 +27,12 @@ function hasUnseenLines(requested: readonly LineRange[], observed: readonly Line
27
27
  });
28
28
  }
29
29
 
30
+ /**
31
+ * A file version's fingerprint in the observation set. Keyed by digest alone:
32
+ * the same bytes under another path are not new work either.
33
+ */
34
+ const versionKey = (_path: string, sha256: string): string => `version:${sha256}`;
35
+
30
36
  export function deriveProgress(
31
37
  previous: readonly ProgressEvidence[], events: readonly LogEvent[], node: number,
32
38
  eligibleChecks: ReadonlySet<string>,
@@ -45,6 +51,8 @@ export function deriveProgress(
45
51
  seen.push(item);
46
52
  reads.set(item.sha256, seen);
47
53
  if (item.observation_sha256 !== undefined) observations.add(item.observation_sha256);
54
+ // A version read before a cut is as known as one read in this segment.
55
+ observations.add(versionKey("", item.sha256));
48
56
  } else if (item.kind === "observation") observations.add(item.sha256);
49
57
  else checks.add(checkKey(item.command, item.gate, item.workspace_sha256));
50
58
  }
@@ -61,6 +69,7 @@ export function deriveProgress(
61
69
  const source = { node, event_id: event.id };
62
70
  const effect = event.effect;
63
71
  const observation = event.observation_sha256 ?? event.content_ref?.sha256 ?? event.content_sha256;
72
+ if (effect?.kind === "read" && effect.sha256 !== "") observations.add(versionKey(effect.path, effect.sha256));
64
73
  if (effect?.kind === "read" && effect.sha256 !== "") {
65
74
  const seen = reads.get(effect.sha256) ?? [];
66
75
  const novel = !seen.some((r) => r.ranges === undefined) &&
@@ -79,6 +88,15 @@ export function deriveProgress(
79
88
  if (observation !== undefined) observations.add(observation);
80
89
  }
81
90
  } else if (effect?.kind === "command" && effect.exit === 0) {
91
+ // D9 revision (2026-09-29, user-approved): output never seen before is a
92
+ // new observation, exactly as for read/grep. A model exploring with
93
+ // `sed -n` or `grep` in the shell is gathering evidence too; the digest
94
+ // excludes wall time, so an unchanged `git status` repeats and stalls.
95
+ if (observation !== undefined && !observations.has(observation)) {
96
+ observations.add(observation);
97
+ evidence.push({ kind: "observation", sha256: observation, source });
98
+ progressed = true;
99
+ }
82
100
  const key = checkKey(effect.cmd, effect.gate, effect.workspace_sha256);
83
101
  if (!checks.has(key)) {
84
102
  checks.add(key);
@@ -89,6 +107,22 @@ export function deriveProgress(
89
107
  // rerun of old evidence into a fresh completion by giving it a new name.
90
108
  if (eligibleChecks.has(event.id)) progressed = true;
91
109
  }
110
+ } else if (effect?.kind === "edit") {
111
+ // D9 revision (2026-09-29, user-approved): a file version no one has
112
+ // seen before is new evidence -- it is how implementation work shows up.
113
+ // Versions already seen anywhere (read, or either side of an edit) do
114
+ // not count, so an A->B->A rewrite loop still stalls.
115
+ // Both sides become durable evidence: the ledger is all a later
116
+ // derivation sees, and a version seen only as "before" must still be
117
+ // recognised when the file is edited back to it.
118
+ for (const [sha, isAfter] of [[effect.sha256_before, false], [effect.sha256_after, true]] as const) {
119
+ if (sha === null) continue;
120
+ const key = versionKey(effect.path, sha);
121
+ if (observations.has(key)) continue;
122
+ observations.add(key);
123
+ evidence.push({ kind: "observation", sha256: key, source });
124
+ if (isAfter) progressed = true;
125
+ }
92
126
  } else if ((name === "grep" || name === "find" || name === "ls" || name === "query") &&
93
127
  observation !== undefined && (event.content_ref?.bytes ?? event.content.length) > 0 &&
94
128
  !observations.has(observation)) {
@@ -17,7 +17,7 @@ import { createServer, type Server } from "node:http";
17
17
  import { pollDeviceCode } from "./oauth-device.ts";
18
18
 
19
19
  /** Matches package.json. Sent as User-Agent so the server can see who we are (D18). */
20
- export const XENO_VERSION = "0.1.2";
20
+ export const XENO_VERSION = "0.1.4";
21
21
 
22
22
  /**
23
23
  * Minutes subtracted from server expiry so a request issued at the edge
@@ -135,7 +135,7 @@ export type ApprovalChoice = "once" | "command" | "tool" | "deny";
135
135
  export const DEFAULT_TOOL_CONTEXT: Omit<ToolContext, "root"> = {
136
136
  // Runner prefixes are resolved in command position so `python3 -m pytest`
137
137
  // works without letting `echo pytest` forge a gate (D11 / M.2).
138
- gates: { pytest: "pytest", tsc: "typecheck", cargo: "cargo", jest: "test", vitest: "test" },
138
+ gates: { pytest: "pytest", tsc: "typecheck", cargo: "cargo", jest: "test", vitest: "test", go: "go" },
139
139
  // Five minutes covers typical compiles and installs; longer work asks for
140
140
  // its own `timeout` up to the hour ceiling (omp uses the same 300 / 3600).
141
141
  // Approved change from 120s (2026-09-28): 120s hard-killed real builds.
@@ -120,6 +120,12 @@ export async function scanWorkspace(
120
120
  if (err) return err;
121
121
  continue;
122
122
  }
123
+ // A symlink is not workspace content: it can point anywhere, and search
124
+ // and the file index already refuse to follow one. It is left out of the
125
+ // file set, so snapshots never read through it and a restore never
126
+ // deletes or replaces it (/back does not restore symlinks). Anything
127
+ // else that is not a regular file still fails the whole scan.
128
+ if (entry.isSymbolicLink()) continue;
123
129
  if (!entry.isFile()) return `cannot snapshot unsupported file type: ${rel}`;
124
130
 
125
131
  const info = await stat(abs);