@voybio/ace-swarm 2.4.3 → 2.4.6

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 (144) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +72 -532
  3. package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
  4. package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
  5. package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
  6. package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
  7. package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
  8. package/assets/agent-state/MODULES/registry.json +1 -4
  9. package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
  10. package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
  11. package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
  12. package/assets/agent-state/QUALITY_GATES.md +25 -9
  13. package/assets/agent-state/TEAL_CONFIG.md +2 -11
  14. package/assets/eval-battery/battery.json +85 -0
  15. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  16. package/assets/scripts/render-mcp-configs.sh +12 -0
  17. package/dist/ace-context.d.ts +18 -0
  18. package/dist/ace-context.js +161 -16
  19. package/dist/ace-server-instructions.js +36 -9
  20. package/dist/astgrep-index.d.ts +7 -1
  21. package/dist/astgrep-index.js +66 -39
  22. package/dist/bridge-trace.d.ts +17 -0
  23. package/dist/bridge-trace.js +23 -0
  24. package/dist/cli.d.ts +9 -1
  25. package/dist/cli.js +619 -10
  26. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  27. package/dist/discovery-runtime-wrappers.js +65 -9
  28. package/dist/eval-harness.d.ts +110 -0
  29. package/dist/eval-harness.js +254 -0
  30. package/dist/experience-reflector.d.ts +13 -0
  31. package/dist/experience-reflector.js +62 -0
  32. package/dist/gate-contract.d.ts +63 -0
  33. package/dist/gate-contract.js +178 -0
  34. package/dist/goal-engine.d.ts +155 -0
  35. package/dist/goal-engine.js +795 -0
  36. package/dist/helpers/store-resolution.js +40 -39
  37. package/dist/hermes/session-manager.d.ts +10 -0
  38. package/dist/hermes/session-manager.js +130 -27
  39. package/dist/job-scheduler.d.ts +58 -0
  40. package/dist/job-scheduler.js +202 -2
  41. package/dist/job-worker.d.ts +87 -0
  42. package/dist/job-worker.js +182 -0
  43. package/dist/local-model-policy.d.ts +8 -2
  44. package/dist/local-model-policy.js +31 -17
  45. package/dist/local-model-runtime.d.ts +6 -0
  46. package/dist/local-model-runtime.js +130 -2
  47. package/dist/measured-classing.d.ts +66 -0
  48. package/dist/measured-classing.js +165 -0
  49. package/dist/model-bridge.d.ts +45 -0
  50. package/dist/model-bridge.js +502 -44
  51. package/dist/orchestrator-supervisor.js +1 -1
  52. package/dist/plan-proposal.d.ts +7 -0
  53. package/dist/plan-proposal.js +74 -4
  54. package/dist/playbook-adaptation.d.ts +37 -0
  55. package/dist/playbook-adaptation.js +159 -0
  56. package/dist/playbook-curator.d.ts +16 -0
  57. package/dist/playbook-curator.js +106 -0
  58. package/dist/playbook-llm-reflector.d.ts +26 -0
  59. package/dist/playbook-llm-reflector.js +157 -0
  60. package/dist/public-surface.d.ts +2 -0
  61. package/dist/public-surface.js +26 -14
  62. package/dist/runtime-executor.js +45 -2
  63. package/dist/runtime-tool-specs.d.ts +8 -1
  64. package/dist/runtime-tool-specs.js +19 -1
  65. package/dist/safe-edit.d.ts +20 -0
  66. package/dist/safe-edit.js +355 -78
  67. package/dist/schemas.js +16 -15
  68. package/dist/server.d.ts +11 -2
  69. package/dist/server.js +55 -5
  70. package/dist/shared.d.ts +12 -0
  71. package/dist/shared.js +13 -1
  72. package/dist/source-mutation-coordinator.d.ts +54 -0
  73. package/dist/source-mutation-coordinator.js +259 -0
  74. package/dist/status-events.d.ts +4 -1
  75. package/dist/status-events.js +41 -6
  76. package/dist/store/ace-packed-store.d.ts +28 -9
  77. package/dist/store/ace-packed-store.js +324 -57
  78. package/dist/store/bgzf.d.ts +4 -0
  79. package/dist/store/bgzf.js +94 -0
  80. package/dist/store/bootstrap-store.d.ts +4 -1
  81. package/dist/store/bootstrap-store.js +87 -9
  82. package/dist/store/cache-workspace.d.ts +8 -0
  83. package/dist/store/cache-workspace.js +67 -1
  84. package/dist/store/crc32.d.ts +2 -0
  85. package/dist/store/crc32.js +16 -0
  86. package/dist/store/fsck.d.ts +16 -0
  87. package/dist/store/fsck.js +240 -0
  88. package/dist/store/gate-contract-migration.d.ts +10 -0
  89. package/dist/store/gate-contract-migration.js +413 -0
  90. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  91. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  92. package/dist/store/materializers/projection-manager.d.ts +7 -0
  93. package/dist/store/materializers/projection-manager.js +37 -0
  94. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  95. package/dist/store/repositories/goal-repository.d.ts +112 -0
  96. package/dist/store/repositories/goal-repository.js +157 -0
  97. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  98. package/dist/store/repositories/playbook-repository.js +314 -0
  99. package/dist/store/store-artifacts.js +44 -2
  100. package/dist/store/store-authority-audit.js +1 -0
  101. package/dist/store/store-cli.d.ts +30 -0
  102. package/dist/store/store-cli.js +242 -0
  103. package/dist/store/store-snapshot.d.ts +12 -0
  104. package/dist/store/store-snapshot.js +74 -11
  105. package/dist/store/types.d.ts +1 -0
  106. package/dist/store/write-coordinator.d.ts +7 -0
  107. package/dist/store/write-coordinator.js +11 -0
  108. package/dist/todo-state.d.ts +21 -0
  109. package/dist/todo-state.js +150 -53
  110. package/dist/tools-agent.js +86 -22
  111. package/dist/tools-discovery.js +5 -3
  112. package/dist/tools-files.js +107 -23
  113. package/dist/tools-framework.js +146 -40
  114. package/dist/tools-goal.d.ts +11 -0
  115. package/dist/tools-goal.js +108 -0
  116. package/dist/tools-handoff.js +7 -11
  117. package/dist/tools-lifecycle.js +2 -0
  118. package/dist/tools-scheduler.js +44 -1
  119. package/dist/tools-skills.js +3 -2
  120. package/dist/tools-todo.js +64 -3
  121. package/dist/tools.js +2 -0
  122. package/dist/tui/chat.js +4 -0
  123. package/dist/tui/index.js +4 -1
  124. package/dist/tui/ollama.d.ts +2 -0
  125. package/dist/tui/openai-compatible.d.ts +14 -0
  126. package/dist/tui/openai-compatible.js +15 -2
  127. package/dist/tui/provider-discovery.d.ts +1 -0
  128. package/dist/tui/provider-discovery.js +2 -2
  129. package/docs/cli-reference.md +125 -0
  130. package/docs/development.md +46 -0
  131. package/docs/evaluation-safety.md +27 -0
  132. package/docs/experience-playbook.md +35 -0
  133. package/docs/hermes.md +129 -0
  134. package/docs/library-api.md +31 -0
  135. package/docs/mcp-tool-reference.md +290 -0
  136. package/docs/provider-security.md +85 -0
  137. package/docs/state-model.md +113 -0
  138. package/docs/troubleshooting.md +95 -0
  139. package/docs/tui-operator-guide.md +75 -0
  140. package/package.json +5 -1
  141. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
  142. package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
  143. package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
  144. package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
@@ -7,18 +7,6 @@ import { isOperationalArtifactPath, operationalArtifactKey, writeOperationalArti
7
7
  import { withStoreWriteCoordinatorSync } from "../store/write-coordinator.js";
8
8
  import { ACE_ROOT_REL, ACE_SCRIPTS_ROOT_REL, ACE_SKILLS_ROOT_REL, ACE_TASKS_ROOT_REL, AGENT_FILES, AGENT_MANIFEST_FILES, DEFAULTS_ROOT, KERNEL_FILES, STORE_AGENT_FILES, STORE_KERNEL_FILES, STORE_TASK_FILES, TASK_FILES, } from "./constants.js";
9
9
  import { currentWorkspaceRoot, isProjectedAceWorkspacePath, mapAceWorkspaceRelativePath, resolveWorkspaceReadPath, resolveWorkspaceWritePath, } from "./path-utils.js";
10
- const STORE_ONLY_AGENT_STATE_FILES = new Set([
11
- "agent-state/AST_GREP_INDEX.json",
12
- "agent-state/AST_GREP_INDEX.md",
13
- "agent-state/EVIDENCE_LOG.md",
14
- "agent-state/STATUS_EVENTS.ndjson",
15
- "agent-state/STATUS_EVENTS-archive.ndjson",
16
- "agent-state/index-fingerprints.json",
17
- "agent-state/index.json",
18
- "agent-state/job-locks.json",
19
- "agent-state/job-queue.json",
20
- "agent-state/scheduler-lease.json",
21
- ]);
22
10
  export function formatStorePath(key) {
23
11
  return toVirtualStorePath(getWorkspaceStorePath(currentWorkspaceRoot()), key);
24
12
  }
@@ -62,6 +50,13 @@ function resolveStoreFallbackKeys(filePath) {
62
50
  if (logicalAgentStatePath && isOperationalArtifactPath(logicalAgentStatePath)) {
63
51
  keys.add(operationalArtifactKey(logicalAgentStatePath));
64
52
  }
53
+ // Root-level tasks/SWARM_HANDOFF.*.json — not under agent-state/ or
54
+ // .agents/ACE/, so it previously had no store fallback key at all and
55
+ // accumulated forever outside `ace cache`'s scan roots.
56
+ const handoffPayloadMatch = normalized.match(/^tasks\/(SWARM_HANDOFF\..+\.json)$/);
57
+ if (handoffPayloadMatch) {
58
+ keys.add(`state/handoffs/payloads/${handoffPayloadMatch[1]}`);
59
+ }
65
60
  if (normalized === "agent-state/ACE_WORKFLOW.md" || canonical === `${ACE_ROOT_REL}/agent-state/ACE_WORKFLOW.md`) {
66
61
  keys.add("knowledge/runtime/ACE_WORKFLOW.md");
67
62
  }
@@ -117,6 +112,9 @@ function resolveStoreFallbackKeys(filePath) {
117
112
  ]);
118
113
  if (logicalAgentStatePath?.startsWith("agent-state/")) {
119
114
  const rel = logicalAgentStatePath.slice("agent-state/".length);
115
+ if (rel === "source-edit-events.ndjson") {
116
+ keys.add("state/source-edit-events.ndjson");
117
+ }
120
118
  if (!rel.startsWith("context-snapshots/")) {
121
119
  keys.add(`state/artifacts/agent-state/${rel}`);
122
120
  }
@@ -176,6 +174,18 @@ function resolveStoreFallbackKeys(filePath) {
176
174
  }
177
175
  return [...keys];
178
176
  }
177
+ function logicalAgentStatePathFor(normalized, canonical) {
178
+ return normalized.startsWith("agent-state/")
179
+ ? normalized
180
+ : canonical.startsWith(`${ACE_ROOT_REL}/agent-state/`)
181
+ ? canonical.slice(`${ACE_ROOT_REL}/`.length)
182
+ : undefined;
183
+ }
184
+ function isAceManagedPath(canonical, logicalAgentStatePath) {
185
+ return Boolean(logicalAgentStatePath ||
186
+ canonical === ACE_ROOT_REL ||
187
+ canonical.startsWith(`${ACE_ROOT_REL}/`));
188
+ }
179
189
  export function resolveStoreFallbackKeysForPath(filePath) {
180
190
  return resolveStoreFallbackKeys(filePath);
181
191
  }
@@ -197,9 +207,22 @@ export function safeRead(filePath) {
197
207
  const direct = readVirtualStorePathSync(filePath);
198
208
  return typeof direct === "string" ? direct : `[FILE NOT FOUND] ${filePath}`;
199
209
  }
210
+ const storeKeys = resolveStoreFallbackKeys(filePath);
211
+ const store = readStoreText(storeKeys);
212
+ const normalized = normalizeRelPath(filePath);
213
+ const canonical = mapAceWorkspaceRelativePath(normalized);
214
+ const storeFirst = process.env.ACE_STORE_FIRST !== "0" && !isProjectedAceWorkspacePath(canonical);
200
215
  const abs = resolveWorkspaceReadPath(filePath);
216
+ if (store && storeFirst) {
217
+ if (existsSync(abs)) {
218
+ const diskText = readFileSyncUtf8(abs);
219
+ if (diskText !== store.text) {
220
+ console.error(`ace safeRead: store/disk copies differ for '${filePath}' (source=store, key=${store.key}; set ACE_STORE_FIRST=0 for legacy disk precedence)`);
221
+ }
222
+ }
223
+ return store.text;
224
+ }
201
225
  if (!existsSync(abs)) {
202
- const store = readStoreText(resolveStoreFallbackKeys(filePath));
203
226
  if (store)
204
227
  return store.text;
205
228
  return `[FILE NOT FOUND] ${abs}`;
@@ -213,24 +236,13 @@ export function safeRead(filePath) {
213
236
  export function safeWrite(filePath, content) {
214
237
  const normalized = normalizeRelPath(filePath);
215
238
  const canonical = mapAceWorkspaceRelativePath(normalized);
216
- const logicalAgentStatePath = normalized.startsWith("agent-state/")
217
- ? normalized
218
- : canonical.startsWith(`${ACE_ROOT_REL}/agent-state/`)
219
- ? canonical.slice(`${ACE_ROOT_REL}/`.length)
220
- : undefined;
239
+ const logicalAgentStatePath = logicalAgentStatePathFor(normalized, canonical);
221
240
  const root = currentWorkspaceRoot();
222
241
  let storeVirtualPath;
223
242
  const storeKeys = resolveStoreFallbackKeys(filePath);
224
243
  if (storeKeys.length > 0 && existsSync(getWorkspaceStorePath(root))) {
225
244
  storeVirtualPath = withStoreWriteCoordinatorSync(getWorkspaceStorePath(root), () => writeStoreBlobSync(root, storeKeys[0], content), { operation_label: "safeWrite" });
226
- const isAcePath = canonical === ACE_ROOT_REL || canonical.startsWith(`${ACE_ROOT_REL}/`);
227
- const isStoreOnlyProjection = canonical === `${ACE_ROOT_REL}/ace-hook-context.json` ||
228
- logicalAgentStatePath?.startsWith("agent-state/context-snapshots/") ||
229
- STORE_ONLY_AGENT_STATE_FILES.has(logicalAgentStatePath ?? "");
230
- if (isAcePath && !isProjectedAceWorkspacePath(canonical)) {
231
- return storeVirtualPath;
232
- }
233
- if (isStoreOnlyProjection) {
245
+ if (isAceManagedPath(canonical, logicalAgentStatePath) && !isProjectedAceWorkspacePath(canonical)) {
234
246
  return storeVirtualPath;
235
247
  }
236
248
  }
@@ -246,11 +258,7 @@ export function safeWrite(filePath, content) {
246
258
  export async function safeWriteAsync(filePath, content) {
247
259
  const normalized = normalizeRelPath(filePath);
248
260
  const canonical = mapAceWorkspaceRelativePath(normalized);
249
- const logicalAgentStatePath = normalized.startsWith("agent-state/")
250
- ? normalized
251
- : canonical.startsWith(`${ACE_ROOT_REL}/agent-state/`)
252
- ? canonical.slice(`${ACE_ROOT_REL}/`.length)
253
- : undefined;
261
+ const logicalAgentStatePath = logicalAgentStatePathFor(normalized, canonical);
254
262
  const root = currentWorkspaceRoot();
255
263
  let storeVirtualPath;
256
264
  const storeKeys = resolveStoreFallbackKeys(filePath);
@@ -262,14 +270,7 @@ export async function safeWriteAsync(filePath, content) {
262
270
  else {
263
271
  storeVirtualPath = await writeStoreBlobQueued(root, storeKey, content, { operation_label: "safeWriteAsync" });
264
272
  }
265
- const isAcePath = canonical === ACE_ROOT_REL || canonical.startsWith(`${ACE_ROOT_REL}/`);
266
- const isStoreOnlyProjection = canonical === `${ACE_ROOT_REL}/ace-hook-context.json` ||
267
- logicalAgentStatePath?.startsWith("agent-state/context-snapshots/") ||
268
- STORE_ONLY_AGENT_STATE_FILES.has(logicalAgentStatePath ?? "");
269
- if (isAcePath && !isProjectedAceWorkspacePath(canonical)) {
270
- return storeVirtualPath;
271
- }
272
- if (isStoreOnlyProjection) {
273
+ if (isAceManagedPath(canonical, logicalAgentStatePath) && !isProjectedAceWorkspacePath(canonical)) {
273
274
  return storeVirtualPath;
274
275
  }
275
276
  }
@@ -10,6 +10,9 @@ export interface HermesLocalTurnOptions {
10
10
  policy: LocalModelExecutionPolicy;
11
11
  toolScope?: string[];
12
12
  maxTurns: number;
13
+ /** Probed context window for the model; falls back to a conservative default. */
14
+ contextLength?: number;
15
+ timeoutMs?: number;
13
16
  }
14
17
  export interface HermesLocalTurnResult {
15
18
  result: BridgeResult;
@@ -21,10 +24,12 @@ export interface HermesLocalTurnResult {
21
24
  provider: string;
22
25
  model: string;
23
26
  events: HermesBridgeEvent[];
27
+ touched_paths: string[];
24
28
  };
25
29
  }
26
30
  export interface HermesLocalExecutor {
27
31
  runTurn(options: HermesLocalTurnOptions): Promise<HermesLocalTurnResult>;
32
+ interrupt?(): void;
28
33
  }
29
34
  export interface HermesSubprocessExecutorOptions {
30
35
  hermesRoot?: string;
@@ -33,10 +38,15 @@ export interface HermesSubprocessExecutorOptions {
33
38
  command?: readonly string[];
34
39
  launchProfile?: HermesLaunchProfile;
35
40
  keepTemp?: boolean;
41
+ timeoutMs?: number;
42
+ maxOutputBytes?: number;
36
43
  }
37
44
  export declare class HermesSubprocessExecutor implements HermesLocalExecutor {
38
45
  private options;
46
+ private readonly activeChildren;
39
47
  constructor(options?: HermesSubprocessExecutorOptions);
48
+ interrupt(): void;
49
+ private interruptChild;
40
50
  runTurn(options: HermesLocalTurnOptions): Promise<HermesLocalTurnResult>;
41
51
  }
42
52
  //# sourceMappingURL=session-manager.d.ts.map
@@ -10,7 +10,38 @@ const __filename = fileURLToPath(import.meta.url);
10
10
  const __dirname = dirname(__filename);
11
11
  const DIST_ROOT = resolve(__dirname, "..");
12
12
  const PACKAGE_ROOT = resolve(DIST_ROOT, "..");
13
- const MIN_HERMES_LOCAL_CONTEXT_LENGTH = 65536;
13
+ // Conservative fallback when the model's real context window has not been probed.
14
+ // The prior 65536 default over-promised small models (an 8k gemma/llama was told it
15
+ // had 64k), causing silent server-side truncation. Callers that have a probed value
16
+ // (Ollama /api/show, llama.cpp /props) should pass it via options.contextLength.
17
+ const DEFAULT_HERMES_LOCAL_CONTEXT_LENGTH = 8192;
18
+ // Env vars forwarded to the operator-supplied Hermes Python worker. Forwarding the
19
+ // full parent process.env leaked unrelated API keys/secrets into a third-party
20
+ // process; only transport-relevant variables are allowlisted here.
21
+ const HERMES_ENV_ALLOWLIST_EXACT = new Set([
22
+ "PATH",
23
+ "HOME",
24
+ "TMPDIR",
25
+ "LANG",
26
+ "LC_ALL",
27
+ "OLLAMA_HOST",
28
+ "OLLAMA_BASE_URL",
29
+ "OPENAI_BASE_URL",
30
+ "OPENAI_API_BASE",
31
+ ]);
32
+ const HERMES_ENV_ALLOWLIST_PREFIXES = ["HERMES_", "ACE_"];
33
+ function scrubbedHermesEnv(base) {
34
+ const scrubbed = {};
35
+ for (const [key, value] of Object.entries(base)) {
36
+ if (value === undefined)
37
+ continue;
38
+ if (HERMES_ENV_ALLOWLIST_EXACT.has(key) ||
39
+ HERMES_ENV_ALLOWLIST_PREFIXES.some((prefix) => key.startsWith(prefix))) {
40
+ scrubbed[key] = value;
41
+ }
42
+ }
43
+ return scrubbed;
44
+ }
14
45
  function summarizeToolResult(value) {
15
46
  if (typeof value === "string")
16
47
  return value.slice(0, 500);
@@ -34,15 +65,38 @@ function hermesBaseUrl(provider, baseUrl) {
34
65
  }
35
66
  export class HermesSubprocessExecutor {
36
67
  options;
68
+ activeChildren = new Set();
37
69
  constructor(options = {}) {
38
70
  this.options = options;
39
71
  }
72
+ interrupt() {
73
+ for (const child of this.activeChildren)
74
+ this.interruptChild(child);
75
+ }
76
+ interruptChild(child) {
77
+ if (child.killed)
78
+ return;
79
+ child.kill("SIGTERM");
80
+ const killTimer = setTimeout(() => {
81
+ if (this.activeChildren.has(child) && child.exitCode === null && child.signalCode === null) {
82
+ child.kill("SIGKILL");
83
+ }
84
+ }, 1_000);
85
+ killTimer.unref();
86
+ }
40
87
  async runTurn(options) {
41
88
  const sessionId = `hermes-${randomUUID()}`;
42
89
  const shadowId = `shadow-${randomUUID()}`;
43
90
  const hermesHome = mkdtempSync(resolve(tmpdir(), "ace-hermes-home-"));
44
91
  mkdirSync(hermesHome, { recursive: true });
45
- const toolScope = options.toolScope ?? [];
92
+ const toolScope = options.toolScope;
93
+ if (!toolScope || toolScope.length === 0) {
94
+ throw new Error("hermes_local requires a non-empty tool scope (reason_code=hermes_empty_tool_scope). " +
95
+ "This is an upstream configuration bug — toolScope must be populated before reaching the Hermes executor.");
96
+ }
97
+ const contextLength = options.contextLength && options.contextLength > 0
98
+ ? options.contextLength
99
+ : DEFAULT_HERMES_LOCAL_CONTEXT_LENGTH;
46
100
  const cliPath = resolve(DIST_ROOT, "cli.js");
47
101
  const baseUrl = hermesBaseUrl(options.runtime.provider, options.runtime.baseUrl ?? options.runtime.ollamaUrl);
48
102
  writeFileSync(resolve(hermesHome, "config.yaml"), [
@@ -51,7 +105,7 @@ export class HermesSubprocessExecutor {
51
105
  " provider: custom",
52
106
  ` base_url: ${JSON.stringify(baseUrl)}`,
53
107
  " api_key: no-key-required",
54
- ` context_length: ${MIN_HERMES_LOCAL_CONTEXT_LENGTH}`,
108
+ ` context_length: ${contextLength}`,
55
109
  "",
56
110
  "auxiliary:",
57
111
  " compression:",
@@ -59,7 +113,7 @@ export class HermesSubprocessExecutor {
59
113
  " provider: custom",
60
114
  ` base_url: ${JSON.stringify(baseUrl)}`,
61
115
  " api_key: no-key-required",
62
- ` context_length: ${MIN_HERMES_LOCAL_CONTEXT_LENGTH}`,
116
+ ` context_length: ${contextLength}`,
63
117
  "",
64
118
  "mcp_servers:",
65
119
  " ace_shadow:",
@@ -88,12 +142,13 @@ export class HermesSubprocessExecutor {
88
142
  const child = spawn(bin, [...args, workerPath], {
89
143
  cwd: PACKAGE_ROOT,
90
144
  env: {
91
- ...process.env,
145
+ ...scrubbedHermesEnv(process.env),
92
146
  HERMES_HOME: hermesHome,
93
147
  ACE_WORKSPACE_ROOT: options.runtime.workspaceRoot,
94
148
  },
95
149
  stdio: ["pipe", "pipe", "pipe"],
96
150
  });
151
+ this.activeChildren.add(child);
97
152
  const decoder = new HermesBridgeFrameDecoder();
98
153
  const events = [];
99
154
  const malformed = [];
@@ -101,25 +156,50 @@ export class HermesSubprocessExecutor {
101
156
  let finalText = "";
102
157
  let turns = 0;
103
158
  let sawError = "";
104
- child.stdout.on("data", (chunk) => {
105
- for (const frame of decoder.push(chunk)) {
106
- if (frame.ok) {
107
- events.push(frame.event);
108
- if (frame.event.type === "final") {
109
- finalText = eventText(frame.event);
110
- const rawTurns = frame.event.turns;
111
- turns = typeof rawTurns === "number" ? rawTurns : turns;
112
- }
113
- if (frame.event.type === "error") {
114
- sawError = eventText(frame.event) || "Hermes bridge error";
115
- }
159
+ let finalCompleted;
160
+ let finalCount = 0;
161
+ let outputBytes = 0;
162
+ let outputOverflow = false;
163
+ const maxOutputBytes = this.options.maxOutputBytes ?? 4 * 1024 * 1024;
164
+ // Single per-frame consumer so the live stream and the post-close tail-flush
165
+ // (decoder.push(…, true)) extract final/error semantics identically and can never drift.
166
+ const applyFrame = (frame) => {
167
+ if (frame.ok) {
168
+ events.push(frame.event);
169
+ if (frame.event.type === "final") {
170
+ finalCount += 1;
171
+ finalText = eventText(frame.event);
172
+ const rawCompleted = frame.event.completed;
173
+ finalCompleted = typeof rawCompleted === "boolean" ? rawCompleted : true;
174
+ const rawTurns = frame.event.turns;
175
+ turns = typeof rawTurns === "number" ? rawTurns : turns;
116
176
  }
117
- else {
118
- malformed.push(frame.reason);
177
+ if (frame.event.type === "error") {
178
+ sawError = eventText(frame.event) || "Hermes bridge error";
119
179
  }
120
180
  }
181
+ else {
182
+ malformed.push(frame.reason);
183
+ }
184
+ };
185
+ child.stdout.on("data", (chunk) => {
186
+ outputBytes += chunk.byteLength;
187
+ if (outputBytes > maxOutputBytes) {
188
+ outputOverflow = true;
189
+ this.interruptChild(child);
190
+ return;
191
+ }
192
+ for (const frame of decoder.push(chunk)) {
193
+ applyFrame(frame);
194
+ }
121
195
  });
122
196
  child.stderr.on("data", (chunk) => {
197
+ outputBytes += chunk.byteLength;
198
+ if (outputBytes > maxOutputBytes) {
199
+ outputOverflow = true;
200
+ this.interruptChild(child);
201
+ return;
202
+ }
123
203
  stderr.push(chunk.toString("utf8"));
124
204
  });
125
205
  const request = {
@@ -135,15 +215,22 @@ export class HermesSubprocessExecutor {
135
215
  system_prompt: "You are running inside ACE Hermes-local mode. Use only the imported ACE MCP toolset for tool calls. Do not use plain text as executable authority.",
136
216
  };
137
217
  child.stdin.end(`${JSON.stringify(request)}\n`);
218
+ const timeoutMs = options.timeoutMs ?? this.options.timeoutMs ?? 120_000;
219
+ let timedOut = false;
220
+ const timeout = setTimeout(() => {
221
+ timedOut = true;
222
+ this.interruptChild(child);
223
+ }, timeoutMs);
224
+ timeout.unref();
138
225
  const exitCode = await new Promise((resolvePromise, reject) => {
139
226
  child.on("error", reject);
140
227
  child.on("close", resolvePromise);
228
+ }).finally(() => {
229
+ clearTimeout(timeout);
230
+ this.activeChildren.delete(child);
141
231
  });
142
232
  for (const frame of decoder.push(new Uint8Array(), true)) {
143
- if (frame.ok)
144
- events.push(frame.event);
145
- else
146
- malformed.push(frame.reason);
233
+ applyFrame(frame);
147
234
  }
148
235
  if (!this.options.keepTemp) {
149
236
  rmSync(hermesHome, { recursive: true, force: true });
@@ -156,11 +243,26 @@ export class HermesSubprocessExecutor {
156
243
  summary: summarizeToolResult(event.result ?? eventText(event)),
157
244
  isError: Boolean(event.is_error),
158
245
  }));
159
- const status = sawError || malformed.length > 0 || exitCode !== 0 ? "failed" : "completed";
246
+ const touchedPaths = events
247
+ .filter((event) => event.type === "tool_start" && event.tool === "write_workspace_file")
248
+ .map((event) => event.input?.path)
249
+ .filter((path) => typeof path === "string" && path.trim().length > 0);
250
+ const finalIndex = events.findIndex((event) => event.type === "final");
251
+ const protocolIdentityInvalid = events.some((event) => (event.session_id !== undefined && event.session_id !== sessionId) ||
252
+ (event.turn_id !== undefined && event.turn_id !== request.turn_id));
253
+ const eventAfterFinal = finalIndex >= 0 && events.slice(finalIndex + 1).some((event) => event.type !== "session_close");
254
+ const protocolInvalid = protocolIdentityInvalid || eventAfterFinal;
255
+ const status = sawError || malformed.length > 0 || exitCode !== 0 || timedOut || outputOverflow || finalCount !== 1 || protocolInvalid
256
+ ? "failed"
257
+ : finalCompleted === false
258
+ ? "blocked"
259
+ : "completed";
160
260
  const summary = status === "failed"
161
261
  ? sawError ||
162
- `Hermes bridge failed${malformed.length ? `; malformed_frames=${malformed.join(",")}` : ""}${stderr.length ? `; stderr=${stderr.join("").slice(0, 500)}` : ""}`
163
- : finalText || "Hermes-local turn completed.";
262
+ `Hermes bridge failed${timedOut ? "; reason_code=hermes_timeout" : ""}${outputOverflow ? "; reason_code=hermes_output_limit" : ""}${finalCount !== 1 ? `; terminal_frames=${finalCount}` : ""}${protocolIdentityInvalid ? "; reason_code=hermes_frame_identity" : ""}${eventAfterFinal ? "; reason_code=hermes_frame_after_final" : ""}${malformed.length ? `; malformed_frames=${malformed.join(",")}` : ""}${stderr.length ? `; stderr=${stderr.join("").slice(0, 500)}` : ""}`
263
+ : status === "blocked"
264
+ ? finalText || "Hermes reported completed=false."
265
+ : finalText || "Hermes-local turn completed.";
164
266
  return {
165
267
  result: {
166
268
  bridge_id: sessionId,
@@ -170,7 +272,7 @@ export class HermesSubprocessExecutor {
170
272
  turns,
171
273
  tool_calls: toolCalls,
172
274
  child_results: [],
173
- evidence_refs: toolCalls.map((tool) => `tool:${tool.tool}`),
275
+ evidence_refs: touchedPaths,
174
276
  },
175
277
  metadata: {
176
278
  bridge_protocol_version: HERMES_BRIDGE_PROTOCOL_VERSION,
@@ -180,6 +282,7 @@ export class HermesSubprocessExecutor {
180
282
  provider: options.runtime.provider,
181
283
  model: options.runtime.model,
182
284
  events,
285
+ touched_paths: touchedPaths,
183
286
  },
184
287
  };
185
288
  }
@@ -94,6 +94,12 @@ export interface DispatchJobsInput {
94
94
  owner?: string;
95
95
  lease_ttl_seconds?: number;
96
96
  now_iso?: string;
97
+ /**
98
+ * How long a running job may go without a worker heartbeat before the
99
+ * watchdog reaps it into `unknown_recovery`. Defaults to
100
+ * DEFAULT_HEARTBEAT_STALE_MS. Set to <=0 to disable reaping for this tick.
101
+ */
102
+ heartbeat_stale_seconds?: number;
97
103
  }
98
104
  export interface DispatchJobsResult {
99
105
  ok: boolean;
@@ -111,6 +117,7 @@ export interface DispatchJobsResult {
111
117
  running_jobs: number;
112
118
  started_jobs: number;
113
119
  recovered_jobs: number;
120
+ reaped_jobs: number;
114
121
  };
115
122
  }
116
123
  export interface DispatchJobNowInput {
@@ -148,6 +155,34 @@ export interface CompleteJobResult {
148
155
  retry_scheduled_for?: string;
149
156
  error?: string;
150
157
  }
158
+ export interface HeartbeatJobInput {
159
+ job_id: string;
160
+ owner?: string;
161
+ now_iso?: string;
162
+ lease_ttl_seconds?: number;
163
+ }
164
+ export interface HeartbeatJobResult {
165
+ ok: boolean;
166
+ queue_path: string;
167
+ lock_path: string;
168
+ heartbeat_at?: string;
169
+ resources?: string[];
170
+ error?: string;
171
+ }
172
+ export interface ReapStaleRunningJobsInput {
173
+ owner?: string;
174
+ now_iso?: string;
175
+ heartbeat_stale_seconds?: number;
176
+ }
177
+ export interface ReapStaleRunningJobsResult {
178
+ ok: boolean;
179
+ queue_path: string;
180
+ lock_path: string;
181
+ queue: JobQueueFile;
182
+ locks: JobLockTableFile;
183
+ reaped_jobs: number;
184
+ reaped_job_ids: string[];
185
+ }
151
186
  export interface AckJobInput {
152
187
  job_id: string;
153
188
  action: "accept" | "reject" | "cancel" | "resume";
@@ -167,9 +202,32 @@ export declare function getSchedulerLeasePath(): string;
167
202
  export declare function readJobQueue(): JobQueueFile;
168
203
  export declare function readJobLockTable(): JobLockTableFile;
169
204
  export declare function readSchedulerLease(): SchedulerLeaseFile | undefined;
205
+ /**
206
+ * Default window a running job may go without a worker heartbeat before the
207
+ * watchdog presumes the worker is dead and reaps the job into
208
+ * `unknown_recovery`. A live in-process worker is expected to call
209
+ * `heartbeatJob` well within this window (see job-worker.ts).
210
+ */
211
+ export declare const DEFAULT_HEARTBEAT_STALE_MS = 120000;
170
212
  export declare function enqueueJob(input: EnqueueJobInput): Promise<EnqueueJobResult>;
171
213
  export declare function acknowledgeJob(input: AckJobInput): Promise<AckJobResult>;
172
214
  export declare function dispatchJobs(input?: DispatchJobsInput): Promise<DispatchJobsResult>;
173
215
  export declare function dispatchJobNow(input: DispatchJobNowInput): Promise<DispatchJobNowResult>;
174
216
  export declare function completeJob(input: CompleteJobInput): Promise<CompleteJobResult>;
217
+ /**
218
+ * Prove liveness for a running job. A live in-process worker calls this
219
+ * periodically while executing a job body so the heartbeat watchdog does not
220
+ * reap it. Advances the job's active-lock heartbeats and renews the owner
221
+ * lease. This is the ONLY path (besides a fresh start) that advances a running
222
+ * job's heartbeat — the dispatcher deliberately does not, so a missing
223
+ * heartbeat reliably signals a dead worker.
224
+ */
225
+ export declare function heartbeatJob(input: HeartbeatJobInput): Promise<HeartbeatJobResult>;
226
+ /**
227
+ * Standalone heartbeat-watchdog tick: reap stale running jobs without planning
228
+ * or starting anything. Takes its own scheduler lock. Useful for a dedicated
229
+ * watchdog cadence separate from dispatch. Does not require lease ownership —
230
+ * reaping a dead worker's job must not depend on holding the lease.
231
+ */
232
+ export declare function reapStaleRunningJobsTick(input?: ReapStaleRunningJobsInput): Promise<ReapStaleRunningJobsResult>;
175
233
  //# sourceMappingURL=job-scheduler.d.ts.map