@galaxy-stack/ai-coder-core 0.3.0-alpha.8 → 0.3.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 (149) hide show
  1. package/CHANGELOG.d/2026-09-19-advisory-guard-policy.md +1 -1
  2. package/CHANGELOG.d/2026-09-25-tail-anchored-bounding.md +5 -0
  3. package/CHANGELOG.d/2026-09-26-binary-review-pipe-guidance-finalization-test.md +14 -0
  4. package/CHANGELOG.d/2026-09-26-cache-capability-and-mcp-reconnect.md +16 -0
  5. package/CHANGELOG.d/2026-09-26-cache-hit-harness.md +13 -0
  6. package/CHANGELOG.d/2026-09-26-cache-hit-rate-and-inhistory-option.md +12 -0
  7. package/CHANGELOG.d/2026-09-26-cumulative-cache-hit.md +13 -0
  8. package/CHANGELOG.d/2026-09-26-default-model-deepseek.md +8 -0
  9. package/CHANGELOG.d/2026-09-26-finalization-prefix-and-verified-inhistory.md +12 -0
  10. package/CHANGELOG.d/2026-09-26-generated-state-and-churn-guard.md +16 -0
  11. package/CHANGELOG.d/2026-09-26-mcp-reconnect-fixture.md +11 -0
  12. package/CHANGELOG.d/2026-09-26-memory-hybrid-semantic-consolidation.md +16 -0
  13. package/CHANGELOG.d/2026-09-26-memory-recall-benchmark.md +12 -0
  14. package/CHANGELOG.d/2026-09-26-ollama-cache-usage.md +14 -0
  15. package/CHANGELOG.d/2026-09-26-ollama-embeddings-provider.md +13 -0
  16. package/CHANGELOG.d/2026-09-26-rerecord-live-fixture.md +6 -0
  17. package/CHANGELOG.d/2026-09-26-skill-index-signing.md +11 -0
  18. package/CHANGELOG.d/2026-09-26-skill-version-and-marketplace.md +14 -0
  19. package/CHANGELOG.d/2026-09-27-workspace-derived-build-output.md +16 -0
  20. package/CHANGELOG.d/2026-09-28-advisory-lenh-check-va-server.md +26 -0
  21. package/CHANGELOG.d/2026-09-28-clamp-tham-so-vuot-cap.md +20 -0
  22. package/CHANGELOG.d/2026-09-28-digest-context-item.md +16 -0
  23. package/CHANGELOG.d/2026-09-28-gate-bo-qua-path-da-xoa.md +20 -0
  24. package/CHANGELOG.d/2026-09-28-ngan-sach-luot-finalization.md +20 -0
  25. package/CHANGELOG.d/2026-09-28-scope-cua-project-validate.md +22 -0
  26. package/CHANGELOG.d/2026-09-28-staleness-theo-pham-vi.md +22 -0
  27. package/CHANGELOG.d/2026-09-28-thong-bao-guard-dependency.md +24 -0
  28. package/CHANGELOG.d/2026-09-28-thu-tu-mandatory-state-cache.md +20 -0
  29. package/CHANGELOG.md +71 -1
  30. package/README.md +10 -10
  31. package/dist/adapters/node/config/manual-provider-config.d.ts +3 -0
  32. package/dist/adapters/node/config/manual-provider-config.d.ts.map +1 -1
  33. package/dist/adapters/node/config/manual-provider-config.js +4 -0
  34. package/dist/adapters/node/config/manual-provider-config.js.map +1 -1
  35. package/dist/adapters/node/host/node-workspace-evidence-verifier.d.ts.map +1 -1
  36. package/dist/adapters/node/host/node-workspace-evidence-verifier.js +8 -13
  37. package/dist/adapters/node/host/node-workspace-evidence-verifier.js.map +1 -1
  38. package/dist/adapters/node/host/node-workspace-port.d.ts +1 -1
  39. package/dist/adapters/node/host/node-workspace-port.d.ts.map +1 -1
  40. package/dist/adapters/node/host/node-workspace-port.js +47 -4
  41. package/dist/adapters/node/host/node-workspace-port.js.map +1 -1
  42. package/dist/adapters/node/host/node-workspace-snapshot.d.ts.map +1 -1
  43. package/dist/adapters/node/host/node-workspace-snapshot.js +60 -4
  44. package/dist/adapters/node/host/node-workspace-snapshot.js.map +1 -1
  45. package/dist/adapters/node/host/project-tools.d.ts.map +1 -1
  46. package/dist/adapters/node/host/project-tools.js +8 -1
  47. package/dist/adapters/node/host/project-tools.js.map +1 -1
  48. package/dist/adapters/node/host/workspace-generated-state.d.ts +8 -0
  49. package/dist/adapters/node/host/workspace-generated-state.d.ts.map +1 -0
  50. package/dist/adapters/node/host/workspace-generated-state.js +8 -0
  51. package/dist/adapters/node/host/workspace-generated-state.js.map +1 -0
  52. package/dist/adapters/node/mcp/mcp-client.d.ts +7 -2
  53. package/dist/adapters/node/mcp/mcp-client.d.ts.map +1 -1
  54. package/dist/adapters/node/mcp/mcp-client.js +41 -6
  55. package/dist/adapters/node/mcp/mcp-client.js.map +1 -1
  56. package/dist/adapters/node/memory/ollama-embeddings.d.ts +26 -0
  57. package/dist/adapters/node/memory/ollama-embeddings.d.ts.map +1 -0
  58. package/dist/adapters/node/memory/ollama-embeddings.js +98 -0
  59. package/dist/adapters/node/memory/ollama-embeddings.js.map +1 -0
  60. package/dist/adapters/node/memory/sqlite-memory.d.ts +16 -3
  61. package/dist/adapters/node/memory/sqlite-memory.d.ts.map +1 -1
  62. package/dist/adapters/node/memory/sqlite-memory.js +138 -16
  63. package/dist/adapters/node/memory/sqlite-memory.js.map +1 -1
  64. package/dist/adapters/node/provider/ollama-chat-stream.d.ts.map +1 -1
  65. package/dist/adapters/node/provider/ollama-chat-stream.js +4 -0
  66. package/dist/adapters/node/provider/ollama-chat-stream.js.map +1 -1
  67. package/dist/adapters/node/provider/ollama-coding-model.d.ts +16 -2
  68. package/dist/adapters/node/provider/ollama-coding-model.d.ts.map +1 -1
  69. package/dist/adapters/node/provider/ollama-coding-model.js +18 -0
  70. package/dist/adapters/node/provider/ollama-coding-model.js.map +1 -1
  71. package/dist/adapters/node/skills/directory-skills.d.ts.map +1 -1
  72. package/dist/adapters/node/skills/directory-skills.js +16 -1
  73. package/dist/adapters/node/skills/directory-skills.js.map +1 -1
  74. package/dist/adapters/node/skills/skill-marketplace.d.ts +71 -0
  75. package/dist/adapters/node/skills/skill-marketplace.d.ts.map +1 -0
  76. package/dist/adapters/node/skills/skill-marketplace.js +259 -0
  77. package/dist/adapters/node/skills/skill-marketplace.js.map +1 -0
  78. package/dist/adapters/node/tools/tool-executor.d.ts.map +1 -1
  79. package/dist/adapters/node/tools/tool-executor.js +36 -15
  80. package/dist/adapters/node/tools/tool-executor.js.map +1 -1
  81. package/dist/adapters/node/tools/workspace-review.d.ts.map +1 -1
  82. package/dist/adapters/node/tools/workspace-review.js +51 -12
  83. package/dist/adapters/node/tools/workspace-review.js.map +1 -1
  84. package/dist/agent/index.d.ts +17 -0
  85. package/dist/agent/index.d.ts.map +1 -1
  86. package/dist/agent/index.js.map +1 -1
  87. package/dist/context/checkpoint.js +1 -1
  88. package/dist/context/checkpoint.js.map +1 -1
  89. package/dist/context/context-manager.d.ts +27 -1
  90. package/dist/context/context-manager.d.ts.map +1 -1
  91. package/dist/context/context-manager.js +54 -7
  92. package/dist/context/context-manager.js.map +1 -1
  93. package/dist/context/context-profile.d.ts.map +1 -1
  94. package/dist/context/context-profile.js +6 -2
  95. package/dist/context/context-profile.js.map +1 -1
  96. package/dist/context/token-ledger.d.ts +9 -0
  97. package/dist/context/token-ledger.d.ts.map +1 -1
  98. package/dist/context/token-ledger.js +8 -0
  99. package/dist/context/token-ledger.js.map +1 -1
  100. package/dist/context/tool-output.d.ts.map +1 -1
  101. package/dist/context/tool-output.js +23 -4
  102. package/dist/context/tool-output.js.map +1 -1
  103. package/dist/ports/capability-port.d.ts +8 -0
  104. package/dist/ports/capability-port.d.ts.map +1 -1
  105. package/dist/ports/workspace-port.d.ts +2 -1
  106. package/dist/ports/workspace-port.d.ts.map +1 -1
  107. package/dist/prompt/prompt-assembler.d.ts +1 -1
  108. package/dist/prompt/prompt-assembler.d.ts.map +1 -1
  109. package/dist/prompt/prompt-assembler.js +12 -4
  110. package/dist/prompt/prompt-assembler.js.map +1 -1
  111. package/dist/runtime/completion-gate.d.ts +1 -0
  112. package/dist/runtime/completion-gate.d.ts.map +1 -1
  113. package/dist/runtime/completion-gate.js +54 -11
  114. package/dist/runtime/completion-gate.js.map +1 -1
  115. package/dist/runtime/run-controller.d.ts +6 -0
  116. package/dist/runtime/run-controller.d.ts.map +1 -1
  117. package/dist/runtime/run-controller.js +138 -13
  118. package/dist/runtime/run-controller.js.map +1 -1
  119. package/dist/runtime/runtime-error.d.ts +1 -1
  120. package/dist/runtime/runtime-error.d.ts.map +1 -1
  121. package/dist/runtime/runtime-error.js.map +1 -1
  122. package/dist/runtime/runtime-types.d.ts +10 -0
  123. package/dist/runtime/runtime-types.d.ts.map +1 -1
  124. package/dist/runtime/workspace-generated-path.d.ts +26 -0
  125. package/dist/runtime/workspace-generated-path.d.ts.map +1 -0
  126. package/dist/runtime/workspace-generated-path.js +61 -0
  127. package/dist/runtime/workspace-generated-path.js.map +1 -0
  128. package/dist/tools/command-shape.d.ts +4 -0
  129. package/dist/tools/command-shape.d.ts.map +1 -0
  130. package/dist/tools/command-shape.js +62 -0
  131. package/dist/tools/command-shape.js.map +1 -0
  132. package/dist/tools/json-schema.d.ts +13 -0
  133. package/dist/tools/json-schema.d.ts.map +1 -1
  134. package/dist/tools/json-schema.js +45 -0
  135. package/dist/tools/json-schema.js.map +1 -1
  136. package/dist/tools/settings-types.js +1 -1
  137. package/dist/tools/settings-types.js.map +1 -1
  138. package/dist/tools/tool-registry.js +2 -2
  139. package/dist/tools/tool-registry.js.map +1 -1
  140. package/dist/tools/validation-scope.d.ts +14 -0
  141. package/dist/tools/validation-scope.d.ts.map +1 -0
  142. package/dist/tools/validation-scope.js +16 -0
  143. package/dist/tools/validation-scope.js.map +1 -0
  144. package/docs/ARCHITECTURE.md +1 -1
  145. package/docs/CACHE_HIT.md +109 -0
  146. package/docs/GALAXY_AGENT_PLATFORM_PLAN.md +171 -348
  147. package/docs/GALAXY_AGENT_PLATFORM_TODO.md +13 -0
  148. package/docs/SKILL_MARKETPLACE.md +59 -0
  149. 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) {
@@ -1689,6 +1731,35 @@ export class AiCoderRunController {
1689
1731
  if (new Set(callIds).size !== callIds.length || callIds.some((id) => !id.trim())) {
1690
1732
  throw new AiCoderRuntimeError("INVALID_MODEL_STREAM", "Model returned duplicate or empty toolCallId values.");
1691
1733
  }
1734
+ // Models trained on other agent frameworks occasionally emit well-known
1735
+ // aliases (for example "bash" for run_command). Map an alias onto the
1736
+ // canonical model name when it is active this round.
1737
+ const visibleNames = new Set(roundToolSet.definitions.map((definition) => definition.function.name));
1738
+ const normalizedCalls = calls.map((call) => {
1739
+ let normalized = call;
1740
+ if (!visibleNames.has(normalized.name)) {
1741
+ const alias = TOOL_NAME_ALIASES[normalized.name];
1742
+ if (alias !== undefined && visibleNames.has(alias))
1743
+ normalized = Object.freeze({ ...normalized, name: alias });
1744
+ }
1745
+ if (normalized.name === "edit_file") {
1746
+ // The edit precondition is optional and advisory at the agent layer:
1747
+ // a hash remembered from an earlier turn or compacted context is stale
1748
+ // and would otherwise loop on PRECONDITION_FAILED. Drop it and let the
1749
+ // oldText match be the guard. The port still enforces a precondition
1750
+ // when a host supplies one directly.
1751
+ const args = normalized.arguments;
1752
+ if (args !== null && typeof args === "object" && !Array.isArray(args) && "precondition" in args) {
1753
+ const { precondition: _drop, ...rest } = args;
1754
+ normalized = Object.freeze({ ...normalized, arguments: Object.freeze(rest) });
1755
+ }
1756
+ }
1757
+ return normalized;
1758
+ });
1759
+ const unavailableName = normalizedCalls.find((call) => !visibleNames.has(call.name))?.name;
1760
+ if (unavailableName !== undefined) {
1761
+ throw new AiCoderRuntimeError("INVALID_MODEL_STREAM", `Tool ${unavailableName} was not active in the registry snapshot shown to the model for this round.`);
1762
+ }
1692
1763
  const reusedId = callIds.find((id) => session.evidence.seenToolCallIds.has(id));
1693
1764
  if (reusedId !== undefined) {
1694
1765
  throw new AiCoderRuntimeError("INVALID_MODEL_STREAM", `toolCallId ${reusedId} was already used in this run.`);
@@ -1696,11 +1767,6 @@ export class AiCoderRunController {
1696
1767
  if (session.toolCalls + calls.length > session.budget.maxToolCalls) {
1697
1768
  throw new AiCoderRuntimeError("MAX_TOOL_CALLS", `AI Coder reached maxToolCalls=${session.budget.maxToolCalls}.`);
1698
1769
  }
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
1770
  const prepared = [];
1705
1771
  for (const call of calls) {
1706
1772
  try {
@@ -1712,6 +1778,28 @@ export class AiCoderRunController {
1712
1778
  }
1713
1779
  return Object.freeze(prepared);
1714
1780
  }
1781
+ /**
1782
+ * Bound a single tool dispatch. A native or host tool that blocks forever
1783
+ * would otherwise hang the whole run; a timeout fails the run cleanly with a
1784
+ * diagnosable code instead.
1785
+ */
1786
+ async executeToolCallGuarded(session, prepared, roundToolSet) {
1787
+ const timeoutMs = 600_000;
1788
+ let timer;
1789
+ try {
1790
+ return await Promise.race([
1791
+ this.executeToolCall(session, prepared, roundToolSet),
1792
+ new Promise((_, reject) => {
1793
+ timer = setTimeout(() => reject(new AiCoderRuntimeError("TOOL_TIMEOUT", `Tool ${prepared.call.name} did not return within ${timeoutMs}ms.`)), timeoutMs);
1794
+ timer.unref?.();
1795
+ }),
1796
+ ]);
1797
+ }
1798
+ finally {
1799
+ if (timer !== undefined)
1800
+ clearTimeout(timer);
1801
+ }
1802
+ }
1715
1803
  async executeToolCall(session, prepared, roundToolSet) {
1716
1804
  if (!session.contextManager || !session.toolSet)
1717
1805
  throw new Error("Context manager or tool set is unavailable.");
@@ -1740,7 +1828,9 @@ export class AiCoderRunController {
1740
1828
  const callRecord = {
1741
1829
  // Bounded, redacted digest so the post-compaction checkpoint shows WHICH
1742
1830
  // paths/queries were already inspected; hashes alone cannot stop re-listing.
1743
- argumentDigest: redactAiCoderCheckpointText(canonicalJson(call.arguments)).slice(0, 160),
1831
+ argumentDigest: redactAiCoderCheckpointText(canonicalJson(call.arguments))
1832
+ .replace(/"contentSha256":"[a-f0-9]{64}"/gu, '"contentSha256":"<re-read to refresh>"')
1833
+ .slice(0, 160),
1744
1834
  argumentsHash,
1745
1835
  idempotencyKey,
1746
1836
  name: call.name,
@@ -1959,9 +2049,19 @@ export class AiCoderRunController {
1959
2049
  research: session.evidence.researchSources,
1960
2050
  writes: session.evidence.writes,
1961
2051
  });
2052
+ const authoredWrites = (normalizedResult.effects?.writes ?? []).filter((write) => !isGeneratedWorkspacePath(write.path.split("/")));
2053
+ const durableAuthoredProgress = Boolean(effectCanChangeState && normalizedResult.effects && (authoredWrites.length > 0
2054
+ || normalizedResult.effects.diffReview
2055
+ || normalizedResult.effects.plan
2056
+ || (normalizedResult.effects.researchSources?.length ?? 0) > 0
2057
+ || (normalizedResult.effects.acceptanceCriteriaSatisfied?.length ?? 0) > 0
2058
+ || (normalizedResult.effects.acceptanceCriteriaWaived?.length ?? 0) > 0
2059
+ || (normalizedResult.effects.inspectedPaths?.length ?? 0) > 0));
1962
2060
  if (hasStateEffect && nextStateVersion !== previousStateVersion) {
1963
2061
  session.stateVersion = nextStateVersion;
1964
- if ((normalizedResult.ok || normalizedResult.effects?.approval === "denied") && !noProgressDetected) {
2062
+ // A generated-only state change (build output, caches, tsbuildinfo) is not
2063
+ // authored progress, so it must not clear the no-progress budget.
2064
+ if (durableAuthoredProgress && (normalizedResult.ok || normalizedResult.effects?.approval === "denied") && !noProgressDetected) {
1965
2065
  session.repeatedToolFingerprint = 0;
1966
2066
  session.noProgressEpisodes = 0;
1967
2067
  session.lastNoProgressEpisodeTurn = -1;
@@ -2345,6 +2445,10 @@ export class AiCoderRunController {
2345
2445
  }
2346
2446
  const cyclingPaths = [];
2347
2447
  for (const write of effects.writes ?? []) {
2448
+ // Build output/caches are durable host effects (run_command proof) but are
2449
+ // not authored source; keep them out of the authored write evidence.
2450
+ if (isGeneratedWorkspacePath(write.path.split("/")))
2451
+ continue;
2348
2452
  const beforeState = workspaceStateKey(write.beforeKind, write.beforeHash);
2349
2453
  const afterState = workspaceStateKey(write.afterKind, write.afterHash);
2350
2454
  let history = session.writeStateHistory.get(write.path) ?? [];
@@ -2597,7 +2701,27 @@ export class AiCoderRunController {
2597
2701
  const messages = gate.issues.map((item) => `${item.code}: ${item.detail}`);
2598
2702
  const researchEvidence = completionRejectionResearchEvidence(session, content);
2599
2703
  const remediation = gate.issues.flatMap((item) => {
2704
+ if (item.code === "WORKSPACE_EVIDENCE_STALE") {
2705
+ return [
2706
+ "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.",
2707
+ ];
2708
+ }
2709
+ if (item.code === "WRITE_NOT_VALIDATED") {
2710
+ return [
2711
+ "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.",
2712
+ ];
2713
+ }
2600
2714
  if (item.code === "DIFF_NOT_REVIEWED") {
2715
+ // In a workspace without Git the git advice is a trap: the model
2716
+ // searched the catalog for a tool that can never exist and drained
2717
+ // the turn budget. Point it to the workspace review tool instead.
2718
+ const activeCanonicalIds = session.toolSet ? Object.values(session.toolSet.canonicalToolIds) : [];
2719
+ const gitAvailable = activeCanonicalIds.includes("git.diff") || activeCanonicalIds.includes("git.exec");
2720
+ if (!gitAvailable) {
2721
+ return [
2722
+ "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.",
2723
+ ];
2724
+ }
2601
2725
  return [
2602
2726
  "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
2727
  ];
@@ -2708,7 +2832,8 @@ export class AiCoderRunController {
2708
2832
  constraints: Object.freeze([...(session.request.constraints ?? [])]),
2709
2833
  decisions: Object.freeze([...session.evidence.decisions]),
2710
2834
  delivery: Object.freeze({ attachmentsDelivered: session.attachmentsDelivered }),
2711
- edits: Object.freeze(session.evidence.writes.map((item) => Object.freeze({
2835
+ editsTotal: session.evidence.writes.length,
2836
+ edits: Object.freeze(session.evidence.writes.slice(-500).map((item) => Object.freeze({
2712
2837
  afterHash: item.afterHash,
2713
2838
  ...(item.afterKind !== undefined ? { afterKind: item.afterKind } : {}),
2714
2839
  beforeHash: item.beforeHash,