hoi4-agent-tools 3.0.8 → 3.1.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/README.md +4 -0
  3. package/dist/bin/stdio.js +23 -12
  4. package/dist/hoi4_agent_tools/core/analysis-checkpoints.js +82 -0
  5. package/dist/hoi4_agent_tools/core/artifact-resource.js +173 -0
  6. package/dist/hoi4_agent_tools/core/artifacts.js +84 -2
  7. package/dist/hoi4_agent_tools/core/condition-evaluator.js +399 -0
  8. package/dist/hoi4_agent_tools/core/condition-model.js +1 -0
  9. package/dist/hoi4_agent_tools/core/condition-schema.js +42 -0
  10. package/dist/hoi4_agent_tools/core/dependency-pages.js +216 -0
  11. package/dist/hoi4_agent_tools/core/dependency-walk.js +266 -0
  12. package/dist/hoi4_agent_tools/core/domain-validation.js +188 -0
  13. package/dist/hoi4_agent_tools/core/engine.js +48 -5
  14. package/dist/hoi4_agent_tools/core/helper-expansion.js +186 -0
  15. package/dist/hoi4_agent_tools/core/index-segments.js +137 -0
  16. package/dist/hoi4_agent_tools/core/index.js +47 -4
  17. package/dist/hoi4_agent_tools/core/job-controls.js +109 -0
  18. package/dist/hoi4_agent_tools/core/job-executor.js +190 -0
  19. package/dist/hoi4_agent_tools/core/job-service.js +112 -0
  20. package/dist/hoi4_agent_tools/core/job-store.js +641 -0
  21. package/dist/hoi4_agent_tools/core/job-transactions.js +126 -0
  22. package/dist/hoi4_agent_tools/core/job-worker-host.js +200 -0
  23. package/dist/hoi4_agent_tools/core/job-worker.js +62 -0
  24. package/dist/hoi4_agent_tools/core/operation-context.js +31 -0
  25. package/dist/hoi4_agent_tools/core/operation-result.js +412 -0
  26. package/dist/hoi4_agent_tools/core/operation-tasks.js +382 -0
  27. package/dist/hoi4_agent_tools/core/persistent-analysis-cache.js +383 -0
  28. package/dist/hoi4_agent_tools/core/result.js +1 -0
  29. package/dist/hoi4_agent_tools/core/scanner.js +15 -75
  30. package/dist/hoi4_agent_tools/core/scenario-state.js +97 -0
  31. package/dist/hoi4_agent_tools/core/semantic-dependencies.js +233 -0
  32. package/dist/hoi4_agent_tools/core/shared-request-capacity.js +74 -25
  33. package/dist/hoi4_agent_tools/core/source/cache.js +107 -0
  34. package/dist/hoi4_agent_tools/core/source/parser.js +11 -1
  35. package/dist/hoi4_agent_tools/core/transaction-execution.js +134 -0
  36. package/dist/hoi4_agent_tools/core/transactions.js +80 -27
  37. package/dist/hoi4_agent_tools/event/graph.js +214 -290
  38. package/dist/hoi4_agent_tools/event/helper-expansion.js +33 -0
  39. package/dist/hoi4_agent_tools/event/job-operations.js +51 -0
  40. package/dist/hoi4_agent_tools/event/operations.js +179 -0
  41. package/dist/hoi4_agent_tools/event/service.js +67 -15
  42. package/dist/hoi4_agent_tools/event/source-analysis.js +5 -4
  43. package/dist/hoi4_agent_tools/focus/job-operations.js +77 -0
  44. package/dist/hoi4_agent_tools/focus/tool-operations.js +669 -0
  45. package/dist/hoi4_agent_tools/focus/tool-support.js +123 -0
  46. package/dist/hoi4_agent_tools/gui/job-operations.js +81 -0
  47. package/dist/hoi4_agent_tools/gui/scenario-conditions.js +119 -0
  48. package/dist/hoi4_agent_tools/gui/scenario-generator.js +59 -73
  49. package/dist/hoi4_agent_tools/gui/scenario.js +3 -0
  50. package/dist/hoi4_agent_tools/gui/studio.js +5 -2
  51. package/dist/hoi4_agent_tools/gui/tool-operations.js +270 -0
  52. package/dist/hoi4_agent_tools/map/job-operations.js +76 -0
  53. package/dist/hoi4_agent_tools/map/tool-operations.js +493 -0
  54. package/dist/hoi4_agent_tools/mcp/prompts/probability.js +25 -0
  55. package/dist/hoi4_agent_tools/mcp/prompts/register-modern.js +29 -0
  56. package/dist/hoi4_agent_tools/mcp/resources/register-modern.js +34 -0
  57. package/dist/hoi4_agent_tools/mcp/resources/register.js +4 -150
  58. package/dist/hoi4_agent_tools/mcp/server/base-tools.js +3 -219
  59. package/dist/hoi4_agent_tools/mcp/server/create-modern-operations.js +188 -0
  60. package/dist/hoi4_agent_tools/mcp/server/create.js +16 -6
  61. package/dist/hoi4_agent_tools/mcp/server/instructions.js +2 -0
  62. package/dist/hoi4_agent_tools/mcp/server/job-task-store.js +67 -0
  63. package/dist/hoi4_agent_tools/mcp/server/legacy-task-tools.js +20 -0
  64. package/dist/hoi4_agent_tools/mcp/server/modern-tasks.js +168 -0
  65. package/dist/hoi4_agent_tools/mcp/server/progress.js +17 -15
  66. package/dist/hoi4_agent_tools/mcp/server/request-lifecycle.js +43 -0
  67. package/dist/hoi4_agent_tools/mcp/server/result.js +2 -412
  68. package/dist/hoi4_agent_tools/mcp/server/task-request-context.js +9 -0
  69. package/dist/hoi4_agent_tools/mcp/server/task-tool-catalog.js +14 -0
  70. package/dist/hoi4_agent_tools/mcp/server/task-tool-definition.js +1 -0
  71. package/dist/hoi4_agent_tools/mcp/server/transaction-execution.js +1 -123
  72. package/dist/hoi4_agent_tools/mcp/tools/chaosx.js +38 -21
  73. package/dist/hoi4_agent_tools/mcp/tools/event.js +24 -315
  74. package/dist/hoi4_agent_tools/mcp/tools/focus.js +34 -1039
  75. package/dist/hoi4_agent_tools/mcp/tools/gui.js +19 -504
  76. package/dist/hoi4_agent_tools/mcp/tools/job.js +46 -0
  77. package/dist/hoi4_agent_tools/mcp/tools/map.js +21 -600
  78. package/dist/hoi4_agent_tools/mcp/tools/probability.js +39 -463
  79. package/dist/hoi4_agent_tools/mcp/tools/technology.js +40 -304
  80. package/dist/hoi4_agent_tools/mcp/transports/http.js +58 -1
  81. package/dist/hoi4_agent_tools/mcp/transports/modern-task-routing.js +109 -0
  82. package/dist/hoi4_agent_tools/mcp/transports/negotiated-stdio.js +121 -0
  83. package/dist/hoi4_agent_tools/probability/job-operations.js +58 -0
  84. package/dist/hoi4_agent_tools/probability/operations.js +183 -0
  85. package/dist/hoi4_agent_tools/probability/scenario-state.js +2 -95
  86. package/dist/hoi4_agent_tools/probability/service.js +24 -3
  87. package/dist/hoi4_agent_tools/probability/trigger-evaluator.js +1 -287
  88. package/dist/hoi4_agent_tools/schemas/common.js +2 -0
  89. package/dist/hoi4_agent_tools/schemas/event.js +72 -1
  90. package/dist/hoi4_agent_tools/schemas/focus-requests.js +189 -0
  91. package/dist/hoi4_agent_tools/schemas/gui-requests.js +193 -0
  92. package/dist/hoi4_agent_tools/schemas/helper-expansion.js +58 -0
  93. package/dist/hoi4_agent_tools/schemas/map-requests.js +85 -0
  94. package/dist/hoi4_agent_tools/schemas/probability-requests.js +168 -0
  95. package/dist/hoi4_agent_tools/schemas/technology.js +90 -1
  96. package/dist/hoi4_agent_tools/schemas/transaction.js +1 -0
  97. package/dist/hoi4_agent_tools/technology/graph.js +83 -78
  98. package/dist/hoi4_agent_tools/technology/helper-expansion.js +33 -0
  99. package/dist/hoi4_agent_tools/technology/job-operations.js +60 -0
  100. package/dist/hoi4_agent_tools/technology/operations.js +125 -0
  101. package/dist/hoi4_agent_tools/technology/service.js +89 -15
  102. package/dist/hoi4_agent_tools/technology/source-analysis.js +2 -1
  103. package/dist/hoi4_agent_tools/version.js +1 -1
  104. package/docs/README.md +2 -0
  105. package/docs/development.md +2 -0
  106. package/docs/events.md +7 -2
  107. package/docs/gui.md +3 -1
  108. package/docs/helper-expansion.md +78 -0
  109. package/docs/http.md +2 -0
  110. package/docs/jobs.md +85 -0
  111. package/docs/technology.md +6 -2
  112. package/package.json +11 -5
  113. package/schemas/configuration.schema.json +1 -1
  114. package/schemas/continuous-focus-palette.schema.json +1 -1
  115. package/schemas/custom-weighted-pool.schema.json +1 -1
  116. package/schemas/event-feature-manifest.schema.json +1 -1
  117. package/schemas/focus-plan.schema.json +1 -1
  118. package/schemas/focus-planning-sidecar.schema.json +1 -1
  119. package/schemas/gui-animation-source.schema.json +1 -1
  120. package/schemas/gui-helper.schema.json +1 -1
  121. package/schemas/gui-scenario.schema.json +189 -33
  122. package/schemas/helper-expansion-request.schema.json +44 -0
  123. package/schemas/helper-expansion-summary.schema.json +70 -0
  124. package/schemas/map-operation.schema.json +1 -1
  125. package/schemas/operation-result.schema.json +1 -1
  126. package/schemas/probability-analysis-result.schema.json +1 -1
  127. package/schemas/probability-compare-input.schema.json +1 -1
  128. package/schemas/probability-evaluate-input.schema.json +1 -1
  129. package/schemas/probability-inspect-input.schema.json +1 -1
  130. package/schemas/probability-render-input.schema.json +1 -1
  131. package/schemas/probability-scenario-set.schema.json +1 -1
  132. package/schemas/probability-sequence-input.schema.json +1 -1
  133. package/schemas/probability-simulate-input.schema.json +1 -1
  134. package/schemas/probability-sweep-input.schema.json +1 -1
  135. package/server.json +2 -2
@@ -0,0 +1,233 @@
1
+ import { canonicalJson, compareCodeUnits, hashCanonical } from './canonical.js';
2
+ import { ReverseSourceDependencies } from './index-segments.js';
3
+ import { PACKAGE_VERSION } from '../version.js';
4
+ const MAX_PROOF_BYTES = 32 * 1024 * 1024;
5
+ const MAX_FRAGMENT_PROOFS = 10_000;
6
+ const MAX_SCOPE_PROOFS = 32;
7
+ /** Source identity includes provenance and interpretation, not just identical source bytes. */
8
+ export function semanticSourceIdentity(file) {
9
+ return hashCanonical({
10
+ version: 'semantic-source.v1',
11
+ toolVersion: PACKAGE_VERSION,
12
+ absolutePath: file.absolutePath,
13
+ path: file.displayPath,
14
+ relativePath: file.relativePath,
15
+ rootKind: file.rootKind,
16
+ loadOrder: file.loadOrder,
17
+ shadowedBy: file.shadowedBy ?? null,
18
+ hash: file.sha256,
19
+ });
20
+ }
21
+ function valueIdentity(value) {
22
+ if (value instanceof Set)
23
+ return [...value].map(valueIdentity);
24
+ if (value instanceof Map)
25
+ return [...value].map(([key, child]) => [
26
+ valueIdentity(key),
27
+ valueIdentity(child),
28
+ ]);
29
+ return value ?? null;
30
+ }
31
+ /** Observe actual typed catalog lookups, including unsuccessful lookups for later additions. */
32
+ export class SemanticCatalog {
33
+ bindings = new Map();
34
+ reads;
35
+ bind(name, kind, values) {
36
+ if (this.bindings.has(name))
37
+ throw new Error('Semantic catalog binding names must be unique');
38
+ this.bindings.set(name, { kind, values });
39
+ const record = (id) => this.reads?.set(canonicalJson([name, id]), { name, kind, id });
40
+ return new Proxy(values, {
41
+ get(target, property) {
42
+ if (property === 'get' || property === 'has')
43
+ return (id) => {
44
+ record(id);
45
+ return property === 'get'
46
+ ? target.get(id)
47
+ : target.has(id);
48
+ };
49
+ if (property === 'size') {
50
+ record(null);
51
+ return target.size;
52
+ }
53
+ if (['set', 'add', 'delete', 'clear'].includes(String(property)))
54
+ return () => {
55
+ throw new Error('Semantic catalogs are read-only');
56
+ };
57
+ if (property === Symbol.iterator ||
58
+ ['entries', 'keys', 'values', 'forEach'].includes(String(property))) {
59
+ record(null);
60
+ const method = Reflect.get(target, property, target);
61
+ return method.bind(target);
62
+ }
63
+ return Reflect.get(target, property, target);
64
+ },
65
+ });
66
+ }
67
+ collect(analyze) {
68
+ const previous = this.reads;
69
+ this.reads = new Map();
70
+ try {
71
+ const value = analyze();
72
+ const reads = [...this.reads.entries()]
73
+ .sort(([a], [b]) => compareCodeUnits(a, b))
74
+ .map(([, read]) => read);
75
+ return { value, reads, signature: this.signature(reads) };
76
+ }
77
+ finally {
78
+ this.reads = previous;
79
+ }
80
+ }
81
+ signature(reads) {
82
+ return hashCanonical(reads.map(({ name, kind, id }) => {
83
+ const binding = this.bindings.get(name);
84
+ if (binding?.kind !== kind)
85
+ return [name, kind, id, 'missing-binding'];
86
+ const values = binding.values;
87
+ return [
88
+ name,
89
+ kind,
90
+ id,
91
+ id === null
92
+ ? valueIdentity(values)
93
+ : [values.has(id), values instanceof Map ? valueIdentity(values.get(id)) : null],
94
+ ];
95
+ }));
96
+ }
97
+ }
98
+ class DependencyProofs {
99
+ entries = new Map();
100
+ scopes = new Map();
101
+ entryBytes = 0;
102
+ scopeBytes = 0;
103
+ put(key, proof) {
104
+ const bytes = canonicalJson(proof).length * 2 + key.length * 2 + 128;
105
+ const previous = this.entries.get(key);
106
+ if (previous !== undefined) {
107
+ this.entries.delete(key);
108
+ this.entryBytes -= previous.bytes;
109
+ }
110
+ if (bytes > MAX_PROOF_BYTES)
111
+ return;
112
+ this.entries.set(key, { proof, bytes });
113
+ this.entryBytes += bytes;
114
+ while (this.entries.size > MAX_FRAGMENT_PROOFS || this.entryBytes > MAX_PROOF_BYTES) {
115
+ const [oldest, entry] = this.entries.entries().next().value;
116
+ this.entries.delete(oldest);
117
+ this.entryBytes -= entry.bytes;
118
+ }
119
+ }
120
+ remember(scope, proof) {
121
+ const previous = this.scopes.get(scope);
122
+ if (previous !== undefined) {
123
+ this.scopes.delete(scope);
124
+ this.scopeBytes -= previous.bytes;
125
+ }
126
+ if (proof.bytes > MAX_PROOF_BYTES)
127
+ return;
128
+ this.scopes.set(scope, proof);
129
+ this.scopeBytes += proof.bytes;
130
+ while (this.scopes.size > MAX_SCOPE_PROOFS || this.scopeBytes > MAX_PROOF_BYTES) {
131
+ const [oldest, entry] = this.scopes.entries().next().value;
132
+ this.scopes.delete(oldest);
133
+ this.scopeBytes -= entry.bytes;
134
+ }
135
+ }
136
+ }
137
+ const proofsByCache = new WeakMap();
138
+ export function clearSemanticDependencies(cache) {
139
+ proofsByCache.delete(cache);
140
+ }
141
+ /** Reconcile both old and new typed definitions so removal, overlay and rename edges survive. */
142
+ export function beginSemanticAnalysis(cache, snapshot, domain, workspaceIdentity) {
143
+ const scope = hashCanonical({
144
+ domain,
145
+ workspaceId: snapshot.workspaceId,
146
+ workspaceIdentity: workspaceIdentity ?? null,
147
+ roots: [
148
+ ...new Set(snapshot.files.map(({ absolutePath, relativePath }) => absolutePath.slice(0, -relativePath.length))),
149
+ ].sort(compareCodeUnits),
150
+ });
151
+ let proofs = cache === undefined ? undefined : proofsByCache.get(cache);
152
+ if (proofs === undefined) {
153
+ proofs = new DependencyProofs();
154
+ if (cache !== undefined)
155
+ proofsByCache.set(cache, proofs);
156
+ }
157
+ const files = new Map(snapshot.files.map((file) => [file.displayPath, semanticSourceIdentity(file)]));
158
+ const symbols = snapshot.index.symbols.map(({ kind, id, path }) => ({ kind, id, path }));
159
+ const previous = proofs.scopes.get(scope);
160
+ const baselineRevision = previous?.baselineRevision ?? snapshot.revision;
161
+ const invalidations = new Map([...(previous?.invalidations ?? [])].filter(([path]) => files.has(path)));
162
+ let affectedFiles = new Set();
163
+ if (previous !== undefined) {
164
+ const changed = [...new Set([...previous.files.keys(), ...files.keys()])].filter((path) => previous.files.get(path) !== files.get(path));
165
+ if (changed.length > 0) {
166
+ const references = [
167
+ ...snapshot.index.references,
168
+ ];
169
+ for (const { proof } of proofs.entries.values())
170
+ if (proof.scope === scope)
171
+ for (const { kind, id } of proof.reads)
172
+ if (id !== null)
173
+ references.push({ path: proof.path, toKind: kind, to: id });
174
+ affectedFiles = new Set(new ReverseSourceDependencies([...previous.symbols, ...symbols], references).affectedFiles(changed));
175
+ for (const path of affectedFiles)
176
+ if (files.has(path))
177
+ invalidations.set(path, snapshot.revision);
178
+ }
179
+ }
180
+ const bytes = [...files].reduce((total, [path, address]) => total + 128 + (path.length + address.length) * 2, 0) +
181
+ symbols.reduce((total, { kind, id, path }) => total + 128 + (kind.length + id.length + path.length) * 2, 0) +
182
+ [...invalidations].reduce((total, [path, revision]) => total + 128 + (path.length + revision.length) * 2, 0);
183
+ proofs.remember(scope, { files, symbols, invalidations, bytes, baselineRevision });
184
+ return {
185
+ scope,
186
+ proofs,
187
+ affectedFiles,
188
+ complete: snapshot.complete,
189
+ invalidations,
190
+ baselineRevision,
191
+ };
192
+ }
193
+ export function semanticFragment(session, cache, file, legacyKey, catalog, analyze) {
194
+ if (cache === undefined)
195
+ return analyze();
196
+ const address = `${session.scope}:${semanticSourceIdentity(file)}`;
197
+ const previous = session.proofs.entries.get(address)?.proof;
198
+ const dependencyRevision = session.invalidations.get(file.displayPath) ?? session.baselineRevision;
199
+ if (!session.affectedFiles.has(file.displayPath)) {
200
+ if (previous?.environment === session.complete &&
201
+ previous.dependencyRevision === dependencyRevision &&
202
+ previous.signature === catalog.signature(previous.reads)) {
203
+ const value = cache.get(previous.key);
204
+ if (value !== undefined)
205
+ return value;
206
+ }
207
+ else if (previous === undefined) {
208
+ // Preserve custom graph-builder cache adapters; internally produced entries use exact read proofs.
209
+ const value = cache.get(legacyKey);
210
+ if (value !== undefined)
211
+ return value;
212
+ }
213
+ }
214
+ const { value, reads, signature } = catalog.collect(analyze);
215
+ const key = hashCanonical({
216
+ version: 'semantic-fragment.v1',
217
+ address,
218
+ signature,
219
+ complete: session.complete,
220
+ dependencyRevision,
221
+ });
222
+ cache.set(key, value, file.bytes.length);
223
+ session.proofs.put(address, {
224
+ scope: session.scope,
225
+ path: file.displayPath,
226
+ reads,
227
+ signature,
228
+ key,
229
+ environment: session.complete,
230
+ dependencyRevision,
231
+ });
232
+ return value;
233
+ }
@@ -1,9 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { mkdir, readdir, rmdir, stat, unlink, writeFile } from 'node:fs/promises';
2
+ import { lstat, mkdir, readdir, rename, rmdir, unlink, writeFile } from 'node:fs/promises';
3
3
  import { hostname } from 'node:os';
4
+ import path from 'node:path';
4
5
  import { setTimeout as delay } from 'node:timers/promises';
5
6
  import { sha256Bytes } from './canonical.js';
6
7
  import { canonicalPath, containedGeneratedPath } from './workspace.js';
8
+ import { ServiceError } from './result.js';
7
9
  function processAlive(pid) {
8
10
  try {
9
11
  process.kill(pid, 0);
@@ -13,7 +15,6 @@ function processAlive(pid) {
13
15
  return error.code !== 'ESRCH';
14
16
  }
15
17
  }
16
- /** Bounds heavy work across task processes sharing the same private server state. */
17
18
  export class SharedRequestCapacity {
18
19
  stateRoot;
19
20
  capacity;
@@ -24,35 +25,48 @@ export class SharedRequestCapacity {
24
25
  async run(signal, action) {
25
26
  signal.throwIfAborted();
26
27
  if (this.stateRoot === undefined)
27
- return action();
28
+ return action({
29
+ handoffToProcess: () => {
30
+ return Promise.reject(new ServiceError('REQUEST_LEASE_STORAGE_REQUIRED', 'Worker ownership transfer requires persistent capacity storage'));
31
+ },
32
+ });
28
33
  const root = await containedGeneratedPath(await canonicalPath(this.stateRoot, signal), 'request-capacity', sha256Bytes(hostname().toLowerCase()).slice(0, 16));
29
34
  await mkdir(root, { recursive: true });
30
35
  const lease = `${process.pid}-${randomUUID()}.lease`;
36
+ const pendingDeletionFailures = new Map();
31
37
  for (;;) {
32
38
  signal.throwIfAborted();
33
39
  for (let index = 0; index < this.capacity; index += 1) {
34
- let slot;
35
- try {
36
- slot = await containedGeneratedPath(root, String(index));
37
- }
38
- catch (error) {
39
- // Another process can finish a slot while canonical containment is checked.
40
- if (error.code === 'ENOENT')
41
- continue;
42
- throw error;
43
- }
40
+ // Only the stable private parent is canonicalized. A slot can disappear during
41
+ // another owner's release; realpath on that ephemeral directory races on Windows.
42
+ // The numeric component cannot escape, and mkdir atomically proves new ownership.
43
+ const slot = path.join(root, String(index));
44
44
  try {
45
45
  await mkdir(slot);
46
+ pendingDeletionFailures.delete(index);
46
47
  }
47
48
  catch (error) {
48
- if (error.code !== 'EEXIST')
49
+ const code = error.code;
50
+ // Windows can retain a delete-pending directory handle after rmdir
51
+ // returns. Recreating that slot reports EPERM/EBUSY, not EEXIST.
52
+ // No work is admitted until a later mkdir actually succeeds.
53
+ if (process.platform === 'win32' && ['EPERM', 'EBUSY'].includes(code ?? '')) {
54
+ const failures = (pendingDeletionFailures.get(index) ?? 0) + 1;
55
+ pendingDeletionFailures.set(index, failures);
56
+ if (failures >= 50)
57
+ throw error;
58
+ await this.reap(slot);
59
+ continue;
60
+ }
61
+ if (code !== 'EEXIST')
49
62
  throw error;
63
+ pendingDeletionFailures.delete(index);
50
64
  await this.reap(slot);
51
65
  continue;
52
66
  }
53
67
  let owner;
54
68
  try {
55
- owner = await containedGeneratedPath(slot, lease);
69
+ owner = path.join(slot, lease);
56
70
  await writeFile(owner, '', { flag: 'wx', mode: 0o600 });
57
71
  }
58
72
  catch (error) {
@@ -63,17 +77,47 @@ export class SharedRequestCapacity {
63
77
  await rmdir(slot).catch(() => undefined);
64
78
  throw error;
65
79
  }
80
+ let transferredPid;
66
81
  try {
67
82
  signal.throwIfAborted();
68
- return await action();
83
+ return await action({
84
+ handoffToProcess: async (pid) => {
85
+ if (transferredPid !== undefined)
86
+ throw new ServiceError('REQUEST_LEASE_ALREADY_TRANSFERRED', 'The capacity lease already belongs to a child process');
87
+ if (!Number.isSafeInteger(pid) ||
88
+ pid <= 0 ||
89
+ pid === process.pid ||
90
+ !processAlive(pid))
91
+ throw new ServiceError('REQUEST_LEASE_OWNER_INVALID', 'Capacity can only transfer to another live process');
92
+ const next = path.join(slot, `${pid}-${randomUUID()}.lease`);
93
+ try {
94
+ await rename(owner, next);
95
+ }
96
+ catch (error) {
97
+ if (error.code === 'ENOENT')
98
+ throw new ServiceError('REQUEST_LEASE_HANDOFF_LOST', 'The capacity lease disappeared before worker dispatch');
99
+ throw error;
100
+ }
101
+ owner = next;
102
+ transferredPid = pid;
103
+ },
104
+ });
69
105
  }
70
106
  finally {
71
- await unlink(owner);
72
- await rmdir(slot).catch((error) => {
73
- const code = error.code;
74
- if (code !== 'ENOENT' && code !== 'ENOTEMPTY')
75
- throw error;
76
- });
107
+ // A launcher can fail while its admitted child still runs. Keep that child's
108
+ // lease until process-exit evidence permits a later reaper to reclaim it.
109
+ if (transferredPid === undefined || !processAlive(transferredPid)) {
110
+ await unlink(owner).catch((error) => {
111
+ if (error.code !== 'ENOENT')
112
+ throw error;
113
+ });
114
+ await rmdir(slot).catch((error) => {
115
+ const code = error.code;
116
+ if (!['ENOENT', 'ENOTEMPTY'].includes(code ?? '') &&
117
+ !(process.platform === 'win32' && ['EPERM', 'EBUSY'].includes(code ?? '')))
118
+ throw error;
119
+ });
120
+ }
77
121
  }
78
122
  }
79
123
  await delay(100, undefined, { signal });
@@ -81,9 +125,12 @@ export class SharedRequestCapacity {
81
125
  }
82
126
  async reap(slot) {
83
127
  try {
128
+ const metadata = await lstat(slot);
129
+ if (metadata.isSymbolicLink() || !metadata.isDirectory())
130
+ throw new ServiceError('PATH_GENERATED_ROOT_ESCAPE', 'Execution capacity slot must be a real directory beneath its private root');
84
131
  const owners = await readdir(slot);
85
132
  if (owners.length === 0) {
86
- if (Date.now() - (await stat(slot)).mtimeMs < 30_000)
133
+ if (Date.now() - metadata.mtimeMs < 30_000)
87
134
  return;
88
135
  }
89
136
  else {
@@ -92,7 +139,7 @@ export class SharedRequestCapacity {
92
139
  if (match === null || processAlive(Number(match[1])))
93
140
  return;
94
141
  // Unique owner names prevent a concurrent reaper from deleting a new lease.
95
- await unlink(await containedGeneratedPath(slot, name)).catch((error) => {
142
+ await unlink(path.join(slot, name)).catch((error) => {
96
143
  if (error.code !== 'ENOENT')
97
144
  throw error;
98
145
  });
@@ -101,7 +148,9 @@ export class SharedRequestCapacity {
101
148
  await rmdir(slot);
102
149
  }
103
150
  catch (error) {
104
- if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(error.code ?? ''))
151
+ const code = error.code ?? '';
152
+ if (!['ENOENT', 'ENOTEMPTY', 'EEXIST'].includes(code) &&
153
+ !(process.platform === 'win32' && ['EPERM', 'EBUSY'].includes(code)))
105
154
  throw error;
106
155
  }
107
156
  }
@@ -0,0 +1,107 @@
1
+ import { deserialize, serialize } from 'node:v8';
2
+ import { sha256Bytes } from '../canonical.js';
3
+ export function sourceDocumentCacheKey(bytes, sourcePath) {
4
+ return `${sha256Bytes(bytes)}:${sourcePath}`;
5
+ }
6
+ function decodedDocument(encoded, expected) {
7
+ const value = deserialize(encoded);
8
+ if (value === null ||
9
+ typeof value !== 'object' ||
10
+ !('path' in value) ||
11
+ typeof value.path !== 'string' ||
12
+ !('sourceRevision' in value) ||
13
+ typeof value.sourceRevision !== 'string' ||
14
+ !('tokens' in value) ||
15
+ !Array.isArray(value.tokens) ||
16
+ !('diagnostics' in value) ||
17
+ !Array.isArray(value.diagnostics) ||
18
+ !('root' in value) ||
19
+ value.root === undefined ||
20
+ !('bytes' in value) ||
21
+ !(value.bytes instanceof Uint8Array))
22
+ throw new Error('Persistent source document is structurally invalid');
23
+ if (expected !== undefined &&
24
+ (value.path !== expected.sourcePath ||
25
+ value.sourceRevision !== sha256Bytes(expected.bytes) ||
26
+ !Buffer.from(value.bytes).equals(Buffer.from(expected.bytes))))
27
+ throw new Error('Persistent source document does not match its source identity');
28
+ return value;
29
+ }
30
+ /** Process-local parsed facts; serialized entries cannot be mutated by their consumers. */
31
+ export class SourceDocumentCache {
32
+ maxBytes;
33
+ #entries = new Map();
34
+ #bytes = 0;
35
+ #hits = 0;
36
+ #misses = 0;
37
+ #evictions = 0;
38
+ constructor(maxBytes = 64 * 1024 * 1024) {
39
+ this.maxBytes = maxBytes;
40
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0)
41
+ throw new RangeError('Source cache size must be a non-negative integer');
42
+ }
43
+ get(key) {
44
+ const encoded = this.#entries.get(key);
45
+ if (encoded === undefined) {
46
+ this.#misses += 1;
47
+ return undefined;
48
+ }
49
+ this.#hits += 1;
50
+ this.#entries.delete(key);
51
+ this.#entries.set(key, encoded);
52
+ return decodedDocument(encoded);
53
+ }
54
+ put(key, document) {
55
+ // Avoid allocating a large serialized copy just to discover that it cannot
56
+ // be retained. This conservative estimate affects caching, never parsing.
57
+ const estimatedBytes = document.bytes.length * 12 + document.tokens.length * 512 + key.length * 2 + 128;
58
+ if (this.maxBytes === 0 || estimatedBytes > this.maxBytes)
59
+ return;
60
+ const encoded = serialize(document);
61
+ this.retain(key, encoded);
62
+ }
63
+ /** Import one authenticated same-runtime entry without counting it as a process-local hit. */
64
+ importEncoded(key, encoded, expected) {
65
+ decodedDocument(encoded, expected);
66
+ this.retain(key, Buffer.from(encoded));
67
+ }
68
+ exportEncoded(key) {
69
+ const encoded = this.#entries.get(key);
70
+ return encoded === undefined ? undefined : Buffer.from(encoded);
71
+ }
72
+ retain(key, encoded) {
73
+ const size = encoded.length + key.length * 2 + 128;
74
+ if (size > this.maxBytes)
75
+ return;
76
+ const previous = this.#entries.get(key);
77
+ if (previous !== undefined)
78
+ this.#bytes -= previous.length + key.length * 2 + 128;
79
+ this.#entries.delete(key);
80
+ while (this.#bytes + size > this.maxBytes) {
81
+ const oldest = this.#entries.entries().next().value;
82
+ if (oldest === undefined)
83
+ break;
84
+ this.#entries.delete(oldest[0]);
85
+ this.#bytes -= oldest[1].length + oldest[0].length * 2 + 128;
86
+ this.#evictions += 1;
87
+ }
88
+ this.#entries.set(key, encoded);
89
+ this.#bytes += size;
90
+ }
91
+ clear() {
92
+ this.#entries.clear();
93
+ this.#bytes = 0;
94
+ }
95
+ statistics() {
96
+ return {
97
+ hits: this.#hits,
98
+ misses: this.#misses,
99
+ evictions: this.#evictions,
100
+ retainedBytes: this.#bytes,
101
+ retainedDocuments: this.#entries.size,
102
+ };
103
+ }
104
+ }
105
+ // All domains call the same parser; cache keys include both bytes and source location.
106
+ // Authorization remains in workspace reads, before any document reaches this cache.
107
+ export const sourceDocuments = new SourceDocumentCache();
@@ -1,6 +1,7 @@
1
1
  import { hasBlockingDiagnostics } from '../diagnostics.js';
2
2
  import { ServiceError } from '../result.js';
3
3
  import { sha256Bytes } from '../canonical.js';
4
+ import { sourceDocumentCacheKey, sourceDocuments } from './cache.js';
4
5
  import { decodeSource, encodeSource } from './encoding.js';
5
6
  import { createSourceLineIndex, isTrivia, lexClausewitz, locationFor, } from './lexer.js';
6
7
  import { SOURCE_LINE_LIMIT, SOURCE_MAX_BYTES, SOURCE_MAX_NESTING, SourceDiagnosticCollector, } from './limits.js';
@@ -224,7 +225,16 @@ class Parser {
224
225
  return locationFor(this.sourcePath, this.lineIndex, token.start, token.end);
225
226
  }
226
227
  }
227
- export function parseClausewitz(bytes, sourcePath) {
228
+ export function parseClausewitz(bytes, sourcePath, cache = sourceDocuments) {
229
+ const key = sourceDocumentCacheKey(bytes, sourcePath);
230
+ const cached = cache.get(key);
231
+ if (cached !== undefined)
232
+ return cached;
233
+ const document = parseClausewitzUncached(bytes, sourcePath);
234
+ cache.put(key, document);
235
+ return document;
236
+ }
237
+ function parseClausewitzUncached(bytes, sourcePath) {
228
238
  const decoded = decodeSource(bytes);
229
239
  const diagnostics = new SourceDiagnosticCollector();
230
240
  if (bytes.byteLength > SOURCE_MAX_BYTES) {
@@ -0,0 +1,134 @@
1
+ import { canonicalJson } from './canonical.js';
2
+ import { boundedSourceHashEvidence, publicArtifactLink } from './artifacts.js';
3
+ import { ServiceError } from './result.js';
4
+ import { PACKAGE_VERSION } from '../version.js';
5
+ import { postValidateTransaction } from './domain-validation.js';
6
+ import { MAX_INLINE_ARTIFACT_LINKS } from './result.js';
7
+ export class AutonomousRewriteError extends ServiceError {
8
+ manifest;
9
+ artifacts;
10
+ constructor(cause, manifest, artifacts = []) {
11
+ const restored = manifest.state === 'rolled_back' && manifest.rollbackStatus === 'applied';
12
+ const code = cause.code.startsWith('TRANSACTION_')
13
+ ? `REWRITE_${cause.code.slice('TRANSACTION_'.length)}`
14
+ : cause.code;
15
+ super(code, restored
16
+ ? 'Rewrite verification failed and the original files were restored'
17
+ : 'Rewrite failed and automatic recovery could not be completed', {
18
+ execution: 'failed',
19
+ automaticRecovery: restored ? 'restored' : 'incomplete',
20
+ });
21
+ this.manifest = manifest;
22
+ this.artifacts = artifacts;
23
+ this.name = 'AutonomousRewriteError';
24
+ }
25
+ }
26
+ async function storeAutonomousValidationEvidence(engine, manifest, postValidation, principal, signal) {
27
+ const diagnostics = [...manifest.diagnostics, ...postValidation.diagnostics];
28
+ const checks = [...manifest.validation.checks, ...postValidation.checks];
29
+ const passed = manifest.validation.passed &&
30
+ checks.every((check) => check.passed) &&
31
+ !diagnostics.some(({ severity }) => severity === 'error' || severity === 'blocker');
32
+ const sourceEvidence = boundedSourceHashEvidence(Object.fromEntries(manifest.files.flatMap(({ relativePath, afterSha256 }) => afterSha256 === null ? [] : [[relativePath, afterSha256]])));
33
+ const artifact = await engine.artifacts.putChunked(engine.resolver.get(manifest.workspaceId, principal), `${manifest.operationKind}.execution-validation.json`, 'application/json', `${canonicalJson({
34
+ schemaVersion: 1,
35
+ execution: passed ? 'post-validation-passed' : 'post-validation-failed',
36
+ operationKind: manifest.operationKind,
37
+ files: manifest.files.map(({ relativePath, afterSha256, afterSize, mediaType }) => ({
38
+ relativePath,
39
+ afterSha256,
40
+ afterSize,
41
+ mediaType,
42
+ })),
43
+ validation: { passed, checks },
44
+ diagnostics,
45
+ resources: postValidation.artifacts,
46
+ })}\n`, {
47
+ kind: 'autonomous-rewrite-execution-validation',
48
+ toolVersion: PACKAGE_VERSION,
49
+ schemaVersion: 'autonomous-rewrite-execution-validation.v1',
50
+ sourceHashes: sourceEvidence.sourceHashes,
51
+ metadata: {
52
+ operationKind: manifest.operationKind,
53
+ fileCount: manifest.files.length,
54
+ checkCount: checks.length,
55
+ diagnosticCount: diagnostics.length,
56
+ sourceHashInventory: sourceEvidence.inventory,
57
+ },
58
+ }, 'Complete rewrite and post-write validation evidence', signal);
59
+ return publicArtifactLink(artifact);
60
+ }
61
+ function uniqueArtifacts(artifacts) {
62
+ const unique = new Map();
63
+ for (const artifact of artifacts)
64
+ unique.set(artifact.uri, artifact);
65
+ return [...unique.values()].slice(0, MAX_INLINE_ARTIFACT_LINKS);
66
+ }
67
+ export function autonomousResultArtifacts(execution) {
68
+ return uniqueArtifacts([
69
+ ...execution.artifacts,
70
+ ...(execution.transaction.executionArtifacts ?? []),
71
+ ...execution.transaction.artifacts,
72
+ ]);
73
+ }
74
+ /**
75
+ * Complete a validated domain rewrite inside one MCP tool call. Hash checks, durable recovery,
76
+ * post-write validation, and automatic failure recovery remain internal guarantees.
77
+ */
78
+ export async function executePlannedTransaction(engine, transaction, principal, signal, hooks = {}) {
79
+ if (!transaction.validation.passed) {
80
+ return { transaction, outcome: 'blocked', artifacts: [] };
81
+ }
82
+ if (transaction.files.length === 0)
83
+ return { transaction, outcome: 'unchanged', artifacts: [] };
84
+ signal?.throwIfAborted();
85
+ await hooks.beforeApply?.(transaction);
86
+ signal?.throwIfAborted();
87
+ let executionArtifacts = [];
88
+ try {
89
+ const applied = await engine.transactions.apply(transaction.workspaceId, transaction.transactionId, transaction.planHash, {
90
+ ...(principal === undefined ? {} : { principal }),
91
+ ...(signal === undefined ? {} : { signal }),
92
+ postValidate: async (manifest, validationSignal) => {
93
+ const validation = await postValidateTransaction(engine, manifest, principal, validationSignal);
94
+ executionArtifacts = [...validation.artifacts];
95
+ executionArtifacts.unshift(await storeAutonomousValidationEvidence(engine, manifest, validation, principal, validationSignal));
96
+ return { ...validation, artifacts: executionArtifacts };
97
+ },
98
+ });
99
+ // Post-validation already invalidates and rebuilds the shared index before apply can succeed.
100
+ return { transaction: applied, outcome: 'applied', artifacts: executionArtifacts };
101
+ }
102
+ catch (error) {
103
+ engine.invalidate(transaction.workspaceId);
104
+ try {
105
+ const manifest = await engine.transactions.status(transaction.workspaceId, transaction.transactionId, principal);
106
+ const cause = error instanceof ServiceError
107
+ ? error
108
+ : new ServiceError('AUTONOMOUS_REWRITE_FAILED', 'Autonomous rewrite failed');
109
+ throw new AutonomousRewriteError(cause, manifest, executionArtifacts);
110
+ }
111
+ catch (statusError) {
112
+ if (statusError instanceof AutonomousRewriteError)
113
+ throw statusError;
114
+ throw error;
115
+ }
116
+ }
117
+ }
118
+ export function autonomousFailureContext(error) {
119
+ if (!(error instanceof AutonomousRewriteError))
120
+ return undefined;
121
+ const manifest = error.manifest;
122
+ return {
123
+ proposedFiles: manifest.files.slice(0, 100).map(({ relativePath }) => relativePath),
124
+ changedFiles: manifest.rollbackStatus === 'failed'
125
+ ? manifest.files.slice(0, 100).map(({ relativePath }) => relativePath)
126
+ : [],
127
+ artifacts: uniqueArtifacts([
128
+ ...error.artifacts,
129
+ ...(manifest.executionArtifacts ?? []),
130
+ ...manifest.artifacts,
131
+ ]),
132
+ validation: manifest.validation,
133
+ };
134
+ }