@galaxy-stack/ai-coder-core 0.3.0-alpha.9 → 0.3.1

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.d/2026-09-19-advisory-guard-policy.md +1 -1
  2. package/CHANGELOG.d/2026-09-26-binary-review-pipe-guidance-finalization-test.md +14 -0
  3. package/CHANGELOG.d/2026-09-26-cache-capability-and-mcp-reconnect.md +16 -0
  4. package/CHANGELOG.d/2026-09-26-cache-hit-harness.md +13 -0
  5. package/CHANGELOG.d/2026-09-26-cache-hit-rate-and-inhistory-option.md +12 -0
  6. package/CHANGELOG.d/2026-09-26-cumulative-cache-hit.md +13 -0
  7. package/CHANGELOG.d/2026-09-26-finalization-prefix-and-verified-inhistory.md +12 -0
  8. package/CHANGELOG.d/2026-09-26-generated-state-and-churn-guard.md +16 -0
  9. package/CHANGELOG.d/2026-09-26-mcp-reconnect-fixture.md +11 -0
  10. package/CHANGELOG.d/2026-09-26-memory-hybrid-semantic-consolidation.md +16 -0
  11. package/CHANGELOG.d/2026-09-26-memory-recall-benchmark.md +12 -0
  12. package/CHANGELOG.d/2026-09-26-ollama-cache-usage.md +14 -0
  13. package/CHANGELOG.d/2026-09-26-ollama-embeddings-provider.md +13 -0
  14. package/CHANGELOG.d/2026-09-26-rerecord-live-fixture.md +6 -0
  15. package/CHANGELOG.d/2026-09-26-skill-index-signing.md +11 -0
  16. package/CHANGELOG.d/2026-09-26-skill-version-and-marketplace.md +14 -0
  17. package/CHANGELOG.d/2026-09-27-workspace-derived-build-output.md +16 -0
  18. package/CHANGELOG.d/2026-09-28-advisory-lenh-check-va-server.md +26 -0
  19. package/CHANGELOG.d/2026-09-28-clamp-tham-so-vuot-cap.md +20 -0
  20. package/CHANGELOG.d/2026-09-28-digest-context-item.md +16 -0
  21. package/CHANGELOG.d/2026-09-28-gate-bo-qua-path-da-xoa.md +20 -0
  22. package/CHANGELOG.d/2026-09-28-ngan-sach-luot-finalization.md +20 -0
  23. package/CHANGELOG.d/2026-09-28-scope-cua-project-validate.md +22 -0
  24. package/CHANGELOG.d/2026-09-28-staleness-theo-pham-vi.md +22 -0
  25. package/CHANGELOG.d/2026-09-28-thong-bao-guard-dependency.md +24 -0
  26. package/CHANGELOG.d/2026-09-28-thu-tu-mandatory-state-cache.md +20 -0
  27. package/CHANGELOG.d/2026-09-29-advisory-doc-source-node-modules-bang-shell.md +24 -0
  28. package/CHANGELOG.d/2026-09-29-retry-khi-model-dung-vi-output-length.md +21 -0
  29. package/CHANGELOG.d/README.md +3 -0
  30. package/CHANGELOG.md +78 -1
  31. package/README.md +10 -10
  32. package/dist/adapters/node/config/manual-provider-config.d.ts +3 -0
  33. package/dist/adapters/node/config/manual-provider-config.d.ts.map +1 -1
  34. package/dist/adapters/node/config/manual-provider-config.js +4 -0
  35. package/dist/adapters/node/config/manual-provider-config.js.map +1 -1
  36. package/dist/adapters/node/host/node-workspace-evidence-verifier.d.ts.map +1 -1
  37. package/dist/adapters/node/host/node-workspace-evidence-verifier.js +8 -13
  38. package/dist/adapters/node/host/node-workspace-evidence-verifier.js.map +1 -1
  39. package/dist/adapters/node/host/node-workspace-port.d.ts +1 -1
  40. package/dist/adapters/node/host/node-workspace-port.d.ts.map +1 -1
  41. package/dist/adapters/node/host/node-workspace-port.js +47 -4
  42. package/dist/adapters/node/host/node-workspace-port.js.map +1 -1
  43. package/dist/adapters/node/host/node-workspace-snapshot.d.ts.map +1 -1
  44. package/dist/adapters/node/host/node-workspace-snapshot.js +60 -4
  45. package/dist/adapters/node/host/node-workspace-snapshot.js.map +1 -1
  46. package/dist/adapters/node/host/project-tools.d.ts.map +1 -1
  47. package/dist/adapters/node/host/project-tools.js +8 -1
  48. package/dist/adapters/node/host/project-tools.js.map +1 -1
  49. package/dist/adapters/node/host/workspace-generated-state.d.ts +8 -0
  50. package/dist/adapters/node/host/workspace-generated-state.d.ts.map +1 -0
  51. package/dist/adapters/node/host/workspace-generated-state.js +8 -0
  52. package/dist/adapters/node/host/workspace-generated-state.js.map +1 -0
  53. package/dist/adapters/node/mcp/mcp-client.d.ts +7 -2
  54. package/dist/adapters/node/mcp/mcp-client.d.ts.map +1 -1
  55. package/dist/adapters/node/mcp/mcp-client.js +41 -6
  56. package/dist/adapters/node/mcp/mcp-client.js.map +1 -1
  57. package/dist/adapters/node/memory/ollama-embeddings.d.ts +26 -0
  58. package/dist/adapters/node/memory/ollama-embeddings.d.ts.map +1 -0
  59. package/dist/adapters/node/memory/ollama-embeddings.js +98 -0
  60. package/dist/adapters/node/memory/ollama-embeddings.js.map +1 -0
  61. package/dist/adapters/node/memory/sqlite-memory.d.ts +16 -3
  62. package/dist/adapters/node/memory/sqlite-memory.d.ts.map +1 -1
  63. package/dist/adapters/node/memory/sqlite-memory.js +138 -16
  64. package/dist/adapters/node/memory/sqlite-memory.js.map +1 -1
  65. package/dist/adapters/node/provider/ollama-chat-stream.d.ts.map +1 -1
  66. package/dist/adapters/node/provider/ollama-chat-stream.js +4 -0
  67. package/dist/adapters/node/provider/ollama-chat-stream.js.map +1 -1
  68. package/dist/adapters/node/provider/ollama-coding-model.d.ts +16 -2
  69. package/dist/adapters/node/provider/ollama-coding-model.d.ts.map +1 -1
  70. package/dist/adapters/node/provider/ollama-coding-model.js +18 -0
  71. package/dist/adapters/node/provider/ollama-coding-model.js.map +1 -1
  72. package/dist/adapters/node/skills/directory-skills.d.ts.map +1 -1
  73. package/dist/adapters/node/skills/directory-skills.js +16 -1
  74. package/dist/adapters/node/skills/directory-skills.js.map +1 -1
  75. package/dist/adapters/node/skills/skill-marketplace.d.ts +71 -0
  76. package/dist/adapters/node/skills/skill-marketplace.d.ts.map +1 -0
  77. package/dist/adapters/node/skills/skill-marketplace.js +259 -0
  78. package/dist/adapters/node/skills/skill-marketplace.js.map +1 -0
  79. package/dist/adapters/node/tools/tool-executor.d.ts.map +1 -1
  80. package/dist/adapters/node/tools/tool-executor.js +36 -15
  81. package/dist/adapters/node/tools/tool-executor.js.map +1 -1
  82. package/dist/adapters/node/tools/workspace-review.d.ts.map +1 -1
  83. package/dist/adapters/node/tools/workspace-review.js +51 -12
  84. package/dist/adapters/node/tools/workspace-review.js.map +1 -1
  85. package/dist/agent/index.d.ts +17 -0
  86. package/dist/agent/index.d.ts.map +1 -1
  87. package/dist/agent/index.js.map +1 -1
  88. package/dist/context/checkpoint.js +1 -1
  89. package/dist/context/checkpoint.js.map +1 -1
  90. package/dist/context/context-manager.d.ts +27 -1
  91. package/dist/context/context-manager.d.ts.map +1 -1
  92. package/dist/context/context-manager.js +54 -7
  93. package/dist/context/context-manager.js.map +1 -1
  94. package/dist/context/context-profile.d.ts.map +1 -1
  95. package/dist/context/context-profile.js +6 -2
  96. package/dist/context/context-profile.js.map +1 -1
  97. package/dist/context/token-ledger.d.ts +9 -0
  98. package/dist/context/token-ledger.d.ts.map +1 -1
  99. package/dist/context/token-ledger.js +8 -0
  100. package/dist/context/token-ledger.js.map +1 -1
  101. package/dist/ports/capability-port.d.ts +8 -0
  102. package/dist/ports/capability-port.d.ts.map +1 -1
  103. package/dist/ports/workspace-port.d.ts +2 -1
  104. package/dist/ports/workspace-port.d.ts.map +1 -1
  105. package/dist/prompt/prompt-assembler.d.ts +1 -1
  106. package/dist/prompt/prompt-assembler.d.ts.map +1 -1
  107. package/dist/prompt/prompt-assembler.js +12 -4
  108. package/dist/prompt/prompt-assembler.js.map +1 -1
  109. package/dist/runtime/completion-gate.d.ts +1 -0
  110. package/dist/runtime/completion-gate.d.ts.map +1 -1
  111. package/dist/runtime/completion-gate.js +54 -11
  112. package/dist/runtime/completion-gate.js.map +1 -1
  113. package/dist/runtime/run-controller.d.ts +6 -0
  114. package/dist/runtime/run-controller.d.ts.map +1 -1
  115. package/dist/runtime/run-controller.js +146 -14
  116. package/dist/runtime/run-controller.js.map +1 -1
  117. package/dist/runtime/runtime-error.d.ts +1 -1
  118. package/dist/runtime/runtime-error.d.ts.map +1 -1
  119. package/dist/runtime/runtime-error.js.map +1 -1
  120. package/dist/runtime/runtime-types.d.ts +10 -0
  121. package/dist/runtime/runtime-types.d.ts.map +1 -1
  122. package/dist/runtime/workspace-generated-path.d.ts +26 -0
  123. package/dist/runtime/workspace-generated-path.d.ts.map +1 -0
  124. package/dist/runtime/workspace-generated-path.js +61 -0
  125. package/dist/runtime/workspace-generated-path.js.map +1 -0
  126. package/dist/tools/command-shape.d.ts +11 -0
  127. package/dist/tools/command-shape.d.ts.map +1 -0
  128. package/dist/tools/command-shape.js +76 -0
  129. package/dist/tools/command-shape.js.map +1 -0
  130. package/dist/tools/json-schema.d.ts +13 -0
  131. package/dist/tools/json-schema.d.ts.map +1 -1
  132. package/dist/tools/json-schema.js +45 -0
  133. package/dist/tools/json-schema.js.map +1 -1
  134. package/dist/tools/tool-registry.js +2 -2
  135. package/dist/tools/tool-registry.js.map +1 -1
  136. package/dist/tools/validation-scope.d.ts +14 -0
  137. package/dist/tools/validation-scope.d.ts.map +1 -0
  138. package/dist/tools/validation-scope.js +16 -0
  139. package/dist/tools/validation-scope.js.map +1 -0
  140. package/docs/ARCHITECTURE.md +1 -1
  141. package/docs/CACHE_HIT.md +109 -0
  142. package/docs/GALAXY_AGENT_PLATFORM_TODO.md +11 -2
  143. package/docs/SKILL_MARKETPLACE.md +59 -0
  144. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { AiCoderContextBudgetError, AiCoderContextManager, } from "../context/context-manager.js";
2
2
  import { compareAiCoderText } from "../deterministic-order.js";
3
+ import { isGeneratedWorkspacePath } from "./workspace-generated-path.js";
3
4
  import { assertAiCoderRunCheckpoint, createAiCoderRunCheckpoint, hashAiCoderCanonicalValue, isAiCoderWorkspaceMutationEvidence, canonicalJson, redactAiCoderCheckpointText, } from "../context/checkpoint.js";
4
5
  import { boundAiCoderToolOutput } from "../context/tool-output.js";
5
6
  import { assembleAiCoderPrompt, createAiCoderTaskContract, formatAiCoderUserTask, } from "../prompt/prompt-assembler.js";
@@ -18,13 +19,23 @@ const DEFAULT_BUDGET = Object.freeze({
18
19
  maxModelRetries: 3,
19
20
  modelRetryDelaysMs: Object.freeze([1_000, 3_000, 8_000]),
20
21
  maxRepeatedToolRequests: 2,
21
- maxToolCalls: 128,
22
+ maxToolCalls: 256,
22
23
  maxTurns: 48,
23
24
  noProgressPolicy: "advisory",
24
25
  observationNudgeThresholds: Object.freeze([3, 5, 8]),
25
26
  persistenceGraceMs: 10_000,
26
27
  toolOutput: Object.freeze({ maxBytes: 48_000, maxTokens: 12_000, tailFraction: 0.25 }),
27
28
  });
29
+ /** Common model aliases for Galaxy tool names, mapped only when the target is active. */
30
+ const TOOL_NAME_ALIASES = Object.freeze({
31
+ bash: "run_command", shell: "run_command", exec: "run_command", execute: "run_command", terminal: "run_command",
32
+ read: "read_file", cat: "read_file", view_file: "read_file",
33
+ write: "write_file", create_file: "write_file",
34
+ edit: "edit_file", str_replace: "edit_file", replace: "edit_file", apply_patch: "edit_file",
35
+ glob: "glob_files", find_files: "glob_files",
36
+ grep: "search_text", search: "search_text",
37
+ ls: "list_files", list: "list_files",
38
+ });
28
39
  const STABLE_OBSERVATION_TOOL_IDS = new Set([
29
40
  "artifact.list",
30
41
  "artifact.read",
@@ -660,7 +671,8 @@ function mandatoryState(session) {
660
671
  acceptanceCriteria: session.evidence.acceptanceCriteria,
661
672
  approvals: session.evidence.approvals,
662
673
  decisions: session.evidence.decisions,
663
- editedFiles: session.evidence.writes,
674
+ editedFiles: session.evidence.writes.slice(-200),
675
+ editedFilesTotal: session.evidence.writes.length,
664
676
  executionBudget: {
665
677
  remainingModelTurns: Math.max(0, session.budget.maxTurns - session.modelTurns),
666
678
  remainingToolCalls: Math.max(0, session.budget.maxToolCalls - session.toolCalls),
@@ -787,7 +799,9 @@ export class AiCoderRunController {
787
799
  evidence: createEvidence(requestSnapshot),
788
800
  executionId,
789
801
  failedToolFamilies: new Map(),
802
+ finalizationAttempted: false,
790
803
  finalizationMode: false,
804
+ generatedChurnEvents: 0,
791
805
  hostStateVersions: new Map(),
792
806
  integrity: null,
793
807
  latestCheckpoint: null,
@@ -1019,7 +1033,7 @@ export class AiCoderRunController {
1019
1033
  ...session.integrity,
1020
1034
  systemPromptHash,
1021
1035
  });
1022
- session.contextManager?.replaceSystemPrompt(promptSnapshot.systemPrompt, session.modelTurns);
1036
+ session.contextManager?.replaceSystemPrompt(promptSnapshot.systemPrompt, session.modelTurns, session.capabilities?.systemPromptUpdate === "in-history" ? "in-history" : "in-place");
1023
1037
  return true;
1024
1038
  }
1025
1039
  async prepare(session) {
@@ -1113,7 +1127,20 @@ export class AiCoderRunController {
1113
1127
  await this.notify(session, { pressure: diagnostic.pressure, tokens: diagnostic.currentInputTokens, type: "context_pressure" });
1114
1128
  },
1115
1129
  goalMessage: userTaskMessage,
1116
- ledgerSink: async (entry) => session.trace.emit("token_ledger", entry),
1130
+ ledgerSink: async (entry) => {
1131
+ await session.trace.emit("token_ledger", entry);
1132
+ await this.notify(session, {
1133
+ ledger: Object.freeze({
1134
+ actualInput: entry.actualInput,
1135
+ cacheHitRate: entry.cacheHitRate,
1136
+ cachedInput: entry.cachedInput,
1137
+ cumulativeCacheHitRate: entry.cumulativeCacheHitRate,
1138
+ outputTokens: entry.outputTokens,
1139
+ turn: entry.turn,
1140
+ }),
1141
+ type: "token_ledger",
1142
+ });
1143
+ },
1117
1144
  profile: session.request.tokenProfile ?? "balanced",
1118
1145
  ...(checkpoint ? { resumeCheckpoint: checkpoint } : {}),
1119
1146
  runId: session.context.runId,
@@ -1318,6 +1345,9 @@ export class AiCoderRunController {
1318
1345
  await this.emitPromptSnapshot(session);
1319
1346
  const contextManager = session.contextManager;
1320
1347
  const roundToolSet = session.toolSet;
1348
+ // The finalization turn must be tool-free (host conformance). The prompt
1349
+ // cache still benefits from the stable message prefix and appended
1350
+ // feedback; only the tool list is dropped.
1321
1351
  const roundDefinitions = session.finalizationMode
1322
1352
  ? Object.freeze([])
1323
1353
  : roundToolSet.definitions;
@@ -1393,7 +1423,7 @@ export class AiCoderRunController {
1393
1423
  const observations = [];
1394
1424
  for (let index = 0; index < preparedCalls.length; index += 1) {
1395
1425
  const preparedCall = preparedCalls[index];
1396
- observations.push(await this.executeToolCall(session, preparedCall, roundToolSet));
1426
+ observations.push(await this.executeToolCallGuarded(session, preparedCall, roundToolSet));
1397
1427
  if (session.evidence.pendingApprovals.size && index + 1 < preparedCalls.length) {
1398
1428
  for (const skipped of preparedCalls.slice(index + 1)) {
1399
1429
  observations.push(await this.recordApprovalBlockedToolCall(session, skipped, roundToolSet));
@@ -1438,6 +1468,18 @@ export class AiCoderRunController {
1438
1468
  return result;
1439
1469
  session.finalizationMode = false;
1440
1470
  }
1471
+ // Out of turns is not out of options. A heavy step can reach the turn cap with
1472
+ // its evidence already satisfied (observed: a gymflow step failed right after
1473
+ // "Project validation passed"). Grant exactly one tool-free finalization turn
1474
+ // so the run reports the evidence it has; the completion gate still decides
1475
+ // whether that report is acceptable, and a second exhaustion still fails.
1476
+ if (!session.finalizationAttempted && this.shouldAttemptFinalization(session)) {
1477
+ session.finalizationAttempted = true;
1478
+ this.enterFinalizationMode(session);
1479
+ session.budget = Object.freeze({ ...session.budget, maxTurns: session.modelTurns + 1 });
1480
+ await this.tracePolicyDecision(session, Object.freeze({ action: "budget_finalization_turn", maxTurns: session.budget.maxTurns }));
1481
+ return this.runLoop(session);
1482
+ }
1441
1483
  throw new AiCoderRuntimeError("MAX_TURNS", `AI Coder reached maxTurns=${session.budget.maxTurns}.`);
1442
1484
  }
1443
1485
  async runModelRound(session, messages, includeAttachments, maxOutputTokens, tools) {
@@ -1514,7 +1556,14 @@ export class AiCoderRunController {
1514
1556
  }
1515
1557
  if (!done)
1516
1558
  throw new CodingProviderError("MALFORMED_STREAM", "Model stream ended without a done event.");
1517
- if (done.stopReason === "length" || done.stopReason === "unknown") {
1559
+ if (done.stopReason === "length") {
1560
+ // The model spent its whole output allowance (usually on hidden thinking)
1561
+ // before emitting a tool call or an answer. That is a capacity failure, not
1562
+ // a task failure: retry the same verified context with thinking disabled
1563
+ // instead of failing the run (measured: a gymflow planning turn died here).
1564
+ throw new CodingProviderError("MALFORMED_STREAM", `Model stopped at the output limit ('length') after ${calls.length} tool call(s) and ${content.length} character(s) of content; retry with hidden thinking disabled and a shorter response.`, true, "without_thinking");
1565
+ }
1566
+ if (done.stopReason === "unknown") {
1518
1567
  throw new AiCoderRuntimeError("INVALID_MODEL_STREAM", `Model stopped with non-final reason '${done.stopReason}'.`);
1519
1568
  }
1520
1569
  if (done.stopReason === "tool_calls" && !calls.length) {
@@ -1689,6 +1738,35 @@ export class AiCoderRunController {
1689
1738
  if (new Set(callIds).size !== callIds.length || callIds.some((id) => !id.trim())) {
1690
1739
  throw new AiCoderRuntimeError("INVALID_MODEL_STREAM", "Model returned duplicate or empty toolCallId values.");
1691
1740
  }
1741
+ // Models trained on other agent frameworks occasionally emit well-known
1742
+ // aliases (for example "bash" for run_command). Map an alias onto the
1743
+ // canonical model name when it is active this round.
1744
+ const visibleNames = new Set(roundToolSet.definitions.map((definition) => definition.function.name));
1745
+ const normalizedCalls = calls.map((call) => {
1746
+ let normalized = call;
1747
+ if (!visibleNames.has(normalized.name)) {
1748
+ const alias = TOOL_NAME_ALIASES[normalized.name];
1749
+ if (alias !== undefined && visibleNames.has(alias))
1750
+ normalized = Object.freeze({ ...normalized, name: alias });
1751
+ }
1752
+ if (normalized.name === "edit_file") {
1753
+ // The edit precondition is optional and advisory at the agent layer:
1754
+ // a hash remembered from an earlier turn or compacted context is stale
1755
+ // and would otherwise loop on PRECONDITION_FAILED. Drop it and let the
1756
+ // oldText match be the guard. The port still enforces a precondition
1757
+ // when a host supplies one directly.
1758
+ const args = normalized.arguments;
1759
+ if (args !== null && typeof args === "object" && !Array.isArray(args) && "precondition" in args) {
1760
+ const { precondition: _drop, ...rest } = args;
1761
+ normalized = Object.freeze({ ...normalized, arguments: Object.freeze(rest) });
1762
+ }
1763
+ }
1764
+ return normalized;
1765
+ });
1766
+ const unavailableName = normalizedCalls.find((call) => !visibleNames.has(call.name))?.name;
1767
+ if (unavailableName !== undefined) {
1768
+ throw new AiCoderRuntimeError("INVALID_MODEL_STREAM", `Tool ${unavailableName} was not active in the registry snapshot shown to the model for this round.`);
1769
+ }
1692
1770
  const reusedId = callIds.find((id) => session.evidence.seenToolCallIds.has(id));
1693
1771
  if (reusedId !== undefined) {
1694
1772
  throw new AiCoderRuntimeError("INVALID_MODEL_STREAM", `toolCallId ${reusedId} was already used in this run.`);
@@ -1696,11 +1774,6 @@ export class AiCoderRunController {
1696
1774
  if (session.toolCalls + calls.length > session.budget.maxToolCalls) {
1697
1775
  throw new AiCoderRuntimeError("MAX_TOOL_CALLS", `AI Coder reached maxToolCalls=${session.budget.maxToolCalls}.`);
1698
1776
  }
1699
- const visibleNames = new Set(roundToolSet.definitions.map((definition) => definition.function.name));
1700
- const unavailableName = calls.find((call) => !visibleNames.has(call.name))?.name;
1701
- if (unavailableName !== undefined) {
1702
- throw new AiCoderRuntimeError("INVALID_MODEL_STREAM", `Tool ${unavailableName} was not active in the registry snapshot shown to the model for this round.`);
1703
- }
1704
1777
  const prepared = [];
1705
1778
  for (const call of calls) {
1706
1779
  try {
@@ -1712,6 +1785,28 @@ export class AiCoderRunController {
1712
1785
  }
1713
1786
  return Object.freeze(prepared);
1714
1787
  }
1788
+ /**
1789
+ * Bound a single tool dispatch. A native or host tool that blocks forever
1790
+ * would otherwise hang the whole run; a timeout fails the run cleanly with a
1791
+ * diagnosable code instead.
1792
+ */
1793
+ async executeToolCallGuarded(session, prepared, roundToolSet) {
1794
+ const timeoutMs = 600_000;
1795
+ let timer;
1796
+ try {
1797
+ return await Promise.race([
1798
+ this.executeToolCall(session, prepared, roundToolSet),
1799
+ new Promise((_, reject) => {
1800
+ timer = setTimeout(() => reject(new AiCoderRuntimeError("TOOL_TIMEOUT", `Tool ${prepared.call.name} did not return within ${timeoutMs}ms.`)), timeoutMs);
1801
+ timer.unref?.();
1802
+ }),
1803
+ ]);
1804
+ }
1805
+ finally {
1806
+ if (timer !== undefined)
1807
+ clearTimeout(timer);
1808
+ }
1809
+ }
1715
1810
  async executeToolCall(session, prepared, roundToolSet) {
1716
1811
  if (!session.contextManager || !session.toolSet)
1717
1812
  throw new Error("Context manager or tool set is unavailable.");
@@ -1740,7 +1835,9 @@ export class AiCoderRunController {
1740
1835
  const callRecord = {
1741
1836
  // Bounded, redacted digest so the post-compaction checkpoint shows WHICH
1742
1837
  // paths/queries were already inspected; hashes alone cannot stop re-listing.
1743
- argumentDigest: redactAiCoderCheckpointText(canonicalJson(call.arguments)).slice(0, 160),
1838
+ argumentDigest: redactAiCoderCheckpointText(canonicalJson(call.arguments))
1839
+ .replace(/"contentSha256":"[a-f0-9]{64}"/gu, '"contentSha256":"<re-read to refresh>"')
1840
+ .slice(0, 160),
1744
1841
  argumentsHash,
1745
1842
  idempotencyKey,
1746
1843
  name: call.name,
@@ -1959,9 +2056,19 @@ export class AiCoderRunController {
1959
2056
  research: session.evidence.researchSources,
1960
2057
  writes: session.evidence.writes,
1961
2058
  });
2059
+ const authoredWrites = (normalizedResult.effects?.writes ?? []).filter((write) => !isGeneratedWorkspacePath(write.path.split("/")));
2060
+ const durableAuthoredProgress = Boolean(effectCanChangeState && normalizedResult.effects && (authoredWrites.length > 0
2061
+ || normalizedResult.effects.diffReview
2062
+ || normalizedResult.effects.plan
2063
+ || (normalizedResult.effects.researchSources?.length ?? 0) > 0
2064
+ || (normalizedResult.effects.acceptanceCriteriaSatisfied?.length ?? 0) > 0
2065
+ || (normalizedResult.effects.acceptanceCriteriaWaived?.length ?? 0) > 0
2066
+ || (normalizedResult.effects.inspectedPaths?.length ?? 0) > 0));
1962
2067
  if (hasStateEffect && nextStateVersion !== previousStateVersion) {
1963
2068
  session.stateVersion = nextStateVersion;
1964
- if ((normalizedResult.ok || normalizedResult.effects?.approval === "denied") && !noProgressDetected) {
2069
+ // A generated-only state change (build output, caches, tsbuildinfo) is not
2070
+ // authored progress, so it must not clear the no-progress budget.
2071
+ if (durableAuthoredProgress && (normalizedResult.ok || normalizedResult.effects?.approval === "denied") && !noProgressDetected) {
1965
2072
  session.repeatedToolFingerprint = 0;
1966
2073
  session.noProgressEpisodes = 0;
1967
2074
  session.lastNoProgressEpisodeTurn = -1;
@@ -2345,6 +2452,10 @@ export class AiCoderRunController {
2345
2452
  }
2346
2453
  const cyclingPaths = [];
2347
2454
  for (const write of effects.writes ?? []) {
2455
+ // Build output/caches are durable host effects (run_command proof) but are
2456
+ // not authored source; keep them out of the authored write evidence.
2457
+ if (isGeneratedWorkspacePath(write.path.split("/")))
2458
+ continue;
2348
2459
  const beforeState = workspaceStateKey(write.beforeKind, write.beforeHash);
2349
2460
  const afterState = workspaceStateKey(write.afterKind, write.afterHash);
2350
2461
  let history = session.writeStateHistory.get(write.path) ?? [];
@@ -2597,7 +2708,27 @@ export class AiCoderRunController {
2597
2708
  const messages = gate.issues.map((item) => `${item.code}: ${item.detail}`);
2598
2709
  const researchEvidence = completionRejectionResearchEvidence(session, content);
2599
2710
  const remediation = gate.issues.flatMap((item) => {
2711
+ if (item.code === "WORKSPACE_EVIDENCE_STALE") {
2712
+ return [
2713
+ "WORKSPACE_EVIDENCE_STALE next action: workspace changed after those validations ran, so their evidence is void. Re-run validate_project with the SAME checks for each stale id AFTER the latest mutation; a validation older than the last write never counts. Do not re-run validations that are already current.",
2714
+ ];
2715
+ }
2716
+ if (item.code === "WRITE_NOT_VALIDATED") {
2717
+ return [
2718
+ "WRITE_NOT_VALIDATED next action: run validate_project (checks covering the written paths) AFTER the final write; a validation older than the write does not count. If the project has no matching script, create the smallest correct test script first, then validate.",
2719
+ ];
2720
+ }
2600
2721
  if (item.code === "DIFF_NOT_REVIEWED") {
2722
+ // In a workspace without Git the git advice is a trap: the model
2723
+ // searched the catalog for a tool that can never exist and drained
2724
+ // the turn budget. Point it to the workspace review tool instead.
2725
+ const activeCanonicalIds = session.toolSet ? Object.values(session.toolSet.canonicalToolIds) : [];
2726
+ const gitAvailable = activeCanonicalIds.includes("git.diff") || activeCanonicalIds.includes("git.exec");
2727
+ if (!gitAvailable) {
2728
+ return [
2729
+ "DIFF_NOT_REVIEWED next action: this workspace has no Git, so call review_changes (workspace review) once after the last workspace mutation to obtain diff-review evidence. If review_changes reports the change exceeds its size cap, run it again after removing temporary artifacts; output from run_command does not provide trusted diff_review evidence.",
2730
+ ];
2731
+ }
2601
2732
  return [
2602
2733
  "DIFF_NOT_REVIEWED next action: call git_operation with action 'diff' after the last workspace mutation. If git_operation is not active, first call search_tools with query 'final git diff' and category 'git', then call git_operation on the following turn. Output from run_command, including git diff or git status, does not provide trusted diff_review evidence.",
2603
2734
  ];
@@ -2708,7 +2839,8 @@ export class AiCoderRunController {
2708
2839
  constraints: Object.freeze([...(session.request.constraints ?? [])]),
2709
2840
  decisions: Object.freeze([...session.evidence.decisions]),
2710
2841
  delivery: Object.freeze({ attachmentsDelivered: session.attachmentsDelivered }),
2711
- edits: Object.freeze(session.evidence.writes.map((item) => Object.freeze({
2842
+ editsTotal: session.evidence.writes.length,
2843
+ edits: Object.freeze(session.evidence.writes.slice(-500).map((item) => Object.freeze({
2712
2844
  afterHash: item.afterHash,
2713
2845
  ...(item.afterKind !== undefined ? { afterKind: item.afterKind } : {}),
2714
2846
  beforeHash: item.beforeHash,