@jitsusama/agentic-harness.core 0.6.2 → 0.6.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.
@@ -11,8 +11,10 @@
11
11
  * council needs that council still to be there, or nothing can say
12
12
  * what it consolidated.
13
13
  */
14
+ import { randomUUID } from "node:crypto";
14
15
  import { mkdir, readdir, readFile, rename, writeFile } from "node:fs/promises";
15
16
  import { join } from "node:path";
17
+ import { withFileLock } from "../../internal/file-lock.js";
16
18
  import { changeKey } from "../keys.js";
17
19
  /** Runs on disk, one file per change. */
18
20
  export function createRunStore(root) {
@@ -71,15 +73,29 @@ export function createRunStore(root) {
71
73
  // whose whole premise is that the session may not survive it,
72
74
  // so the window stopped being theoretical. A rename within one
73
75
  // directory is atomic: a reader sees the old ledger or the new
74
- // one.
75
- const pending = `${path}.${process.pid}.tmp`;
76
+ // one. Named per write, not per process: two writes in one
77
+ // process sharing a name tore the file, the shorter write landing
78
+ // over the start of the longer.
79
+ const pending = `${path}.${process.pid}.${randomUUID()}.tmp`;
76
80
  await writeFile(pending, JSON.stringify(ledger, null, 2), "utf8");
77
81
  await rename(pending, path);
78
82
  }
83
+ /**
84
+ * Read, change and write one change's ledger as a single step. Every
85
+ * write is a whole ledger built from the one read before it, so two
86
+ * of these at once, from parallel tool calls or two sessions on one
87
+ * change, would each lay its version over the other's and drop a
88
+ * round without an error.
89
+ */
90
+ async function mutate(change, next) {
91
+ await mkdir(root, { recursive: true });
92
+ await withFileLock(join(root, fileFor(change)), async () => {
93
+ await write(change, next(await read(change)));
94
+ });
95
+ }
79
96
  return {
80
97
  async record(change, run) {
81
- const ledger = await read(change);
82
- await write(change, { runs: [...ledger.runs, run] });
98
+ await mutate(change, (ledger) => ({ runs: [...ledger.runs, run] }));
83
99
  },
84
100
  async list(change) {
85
101
  return (await read(change)).runs;
@@ -176,24 +192,26 @@ export function createRunStore(root) {
176
192
  return (await read(change)).runs.find((r) => r.id === runId);
177
193
  },
178
194
  async keep(change, run) {
179
- const ledger = await read(change);
180
- const at = ledger.runs.findIndex((held) => held.id === run.id);
181
- await write(change, {
182
- runs: at === -1
183
- ? [...ledger.runs, run]
184
- : ledger.runs.map((held, index) => (index === at ? run : held)),
195
+ await mutate(change, (ledger) => {
196
+ const at = ledger.runs.findIndex((held) => held.id === run.id);
197
+ return {
198
+ runs: at === -1
199
+ ? [...ledger.runs, run]
200
+ : ledger.runs.map((held, index) => (index === at ? run : held)),
201
+ };
185
202
  });
186
203
  },
187
204
  async replace(change, run) {
188
- const ledger = await read(change);
189
- const at = ledger.runs.findIndex((held) => held.id === run.id);
190
- if (at === -1) {
191
- // Adding it silently would make a retry look like it
192
- // patched something when it invented a round instead.
193
- throw new Error(`No run "${run.id}" is held against this change, so there is nothing to replace. Record it first, or check the id.`);
194
- }
195
- await write(change, {
196
- runs: ledger.runs.map((held, index) => (index === at ? run : held)),
205
+ await mutate(change, (ledger) => {
206
+ const at = ledger.runs.findIndex((held) => held.id === run.id);
207
+ if (at === -1) {
208
+ // Adding it silently would make a retry look like it
209
+ // patched something when it invented a round instead.
210
+ throw new Error(`No run "${run.id}" is held against this change, so there is nothing to replace. Record it first, or check the id.`);
211
+ }
212
+ return {
213
+ runs: ledger.runs.map((held, index) => (index === at ? run : held)),
214
+ };
197
215
  });
198
216
  },
199
217
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jitsusama/agentic-harness.core",
3
- "version": "0.6.2",
3
+ "version": "0.6.4",
4
4
  "description": "Pi-agnostic business logic for agentic-harness: state machines, guardian decisions, quest/TDD domain model.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,6 +45,12 @@
45
45
  "./quest/verbs/queries": "./dist/quest/verbs/queries.js",
46
46
  "./quest/verbs/lifecycle": "./dist/quest/verbs/lifecycle.js",
47
47
  "./quest/verbs/tree-ops": "./dist/quest/verbs/tree-ops.js",
48
+ "./quest/bash-write": "./dist/internal/quest/bash-write.js",
49
+ "./quest/write-classifier": "./dist/internal/quest/write-classifier.js",
50
+ "./quest/record": "./dist/internal/quest/record.js",
51
+ "./quest/workspace": "./dist/internal/quest/workspace.js",
52
+ "./quest/record-gate": "./dist/internal/quest/record-gate.js",
53
+ "./quest/record-audit": "./dist/internal/quest/record-audit.js",
48
54
  "./tree": "./dist/tree/index.js",
49
55
  "./lsp": "./dist/lsp/index.js",
50
56
  "./result": "./dist/result/index.js",