@tangle-network/agent-knowledge 17.1.10 → 19.0.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.
package/dist/index.js CHANGED
@@ -1,13 +1,14 @@
1
- import { $ as formatKnowledgeCitationReference, $t as inspectPendingKnowledgeMutation, A as addSourceText, An as writeJsonDurableWithinRoot, At as ResearchSourceVersionSchema, B as KB_STORE_DIR, Bt as claimId, C as gradeClaims, Cn as renameDurable, Ct as KnowledgePageSchema, D as buildKnowledgeIndex, Dn as writeFileDurable, Dt as ResearchClaimEvidenceSchema, E as verifyGradeableEvidence, En as withSafeDirectory, Et as KnowledgeRelationSchema, F as ClaimLedgerMigrationRequiredError, Ft as assertDeepQuestionIntegrity, G as KnowledgeCitationAuditError, Gt as materializeRegisteredClaimEvidence, H as assertClaimLedgerId, Ht as deepQuestionId, I as FileSystemKbStore, It as assertResearchClaimEvidenceIntegrity, J as assertKnowledgeCitationAudit, Jt as normalizeClaimText, K as KnowledgeCitationResolutionError, Kt as mergeClaimLedgers, L as KB_CLAIM_LEDGER_DIR, Lt as assertResearchClaimLedgerIntegrity, M as snapshotSourceTextInput, Mn as textSourceAdapter, Mt as SourceRecordSchema, N as sourceRegistryPath, Nt as KNOWLEDGE_EVENT_TYPES, O as writeKnowledgeIndex, On as writeFileDurableWithinRoot, Ot as ResearchClaimLedgerSchema, P as writeSourceRegistry, Pt as ClaimLedgerGoalConflictError, Q as auditKnowledgeCitations, Qt as acquireDurableFileLock, R as KB_EVENTS_PATH, Rt as assertTrackedClaimIntegrity, S as claimCheckKey, Sn as removeDurable, St as KnowledgePageInvalidationSchema, T as verdictFor, Tn as withSafeDescendant, Tt as KnowledgeRelationNodeSchema, U as buildKnowledgeGraph, Ut as emptyClaimLedger, V as MemoryKbStore, Vt as claimSourceHost, W as knowledgePageRelations, Wt as linkClaimContradictions, X as assertRunScopedCitationsResolved, Xt as formatFrontmatter, Y as assertKnowledgeCitationsResolved, Yt as researchSourceVersionKey, Z as auditCurrentRunCitations, Zt as parseFrontmatter, _ as lintKnowledgeIndex, _n as isMissingFile, _t as KnowledgeBaseCandidateSchema, a as applyKnowledgeWriteBlocksFile, an as applyKnowledgeFileTransaction, at as SCAFFOLD_PAGE_BASENAMES, b as UncheckableClaimError, bn as readRegularFileWithinRoot, bt as KnowledgeGraphNodeSchema, c as KnowledgeDuplicateIntakeError, cn as finishKnowledgeFileTransaction, ct as isScaffoldPath, d as createFileRunLineageAuthority, dn as rollbackKnowledgeFileTransaction, dt as loadKnowledgePages, en as isKnowledgeMutationHeld, et as parseKnowledgeCitationReference, f as createRunScopedStores, fn as DEFAULT_PAGES_DIRECTORY, ft as writeJson, g as validateKnowledgeIndex, gn as isKernelAnchoredPath, gt as DeepQuestionSchema, h as normalizePageText, hn as canonicalRelativeWithinRoot, ht as normalizeLinkTarget, i as applyKnowledgeWriteBlocks, in as withKnowledgeRead, it as resolveRunScopedCitations, j as loadSourceRegistry, jn as mediaTypeFor, jt as SourceAnchorSchema, k as addSourcePath, kn as writeJsonDurable, kt as ResearchClaimRecordSchema, l as assertKnowledgeWriteIntake, ln as knowledgeFileTransactionPlanHash, lt as knowledgePageFromMarkdown, m as detectNearDuplicatePages, mn as canonicalPathsEqual, mt as extractWikilinks, n as inspectKnowledgeIndex, nn as runInKnowledgeMutationScope, nt as resolveKnowledgeCitations, o as isSafeKnowledgePath, on as assertKnowledgeMutationPath, ot as initKnowledgeBase, p as originatedPages, pn as normalizePagesDirectory, pt as WIKILINK_REGEX, q as assertCurrentRunCitationsResolved, qt as mergeTrackedClaims, r as stringMetadata, rn as withKnowledgeMutation, rt as resolveRunScopedCitation, s as parseKnowledgeWriteBlocks, sn as commitKnowledgeFileMutations, st as isKnowledgePagePath, t as explainKnowledgeTarget, tn as recoverPendingKnowledgeMutation, tt as resolveKnowledgeCitation, u as RUN_LINEAGE_BASENAME, un as prepareKnowledgeFileTransaction, ut as layoutFor, v as CHECKABLE_RUNG_THRESHOLD, vn as listRegularFilesWithinRoot, vt as KnowledgeEventSchema, w as gradeFor, wn as syncDirectory, wt as KnowledgeRelationGraphSchema, x as assertGradeableEvidence, xn as relativeWithinRoot, xt as KnowledgeIndexSchema, y as DEADLINE_EXIT_CODE, yn as readRegularFileNoFollow, yt as KnowledgeGraphEdgeSchema, z as KB_INDEX_PATH, zt as claimEvidenceId } from "./inspect-DrJnvx5Z.js";
1
+ import { $ as FileSystemKbStore, $t as assertResearchClaimEvidenceIntegrity, A as visibilityIndexOf, An as normalizePagesDirectory, At as WIKILINK_REGEX, B as gradeClaims, Bn as renameDurable, Bt as KnowledgePageSchema, C as nonEmpty$1, Cn as assertKnowledgeMutationPath, Ct as initKnowledgeBase, D as validateOrigin, Dn as prepareKnowledgeFileTransaction, Dt as layoutFor, E as validateKnowledgePage, En as knowledgeFileTransactionPlanHash, Et as knowledgePageFromMarkdown, F as CHECKABLE_RUNG_THRESHOLD, Fn as listRegularFilesWithinRoot, Ft as KnowledgeEventSchema, G as writeKnowledgeIndex, Gn as writeFileDurableWithinRoot, Gt as ResearchClaimLedgerSchema, H as verdictFor, Hn as withSafeDescendant, Ht as KnowledgeRelationNodeSchema, I as DEADLINE_EXIT_CODE, In as readRegularFileNoFollow, It as KnowledgeGraphEdgeSchema, J as loadSourceRegistry, Jn as mediaTypeFor, Jt as SourceAnchorSchema, K as addSourcePath, Kn as writeJsonDurable, Kt as ResearchClaimRecordSchema, L as UncheckableClaimError, Ln as readRegularFileWithinRoot, Lt as KnowledgeGraphNodeSchema, M as normalizePageText, Mn as canonicalRelativeWithinRoot, Mt as normalizeLinkTarget, N as validateKnowledgeIndex, Nn as isKernelAnchoredPath, Nt as DeepQuestionSchema, O as verifyKnowledgeVisibilitySnapshot, On as rollbackKnowledgeFileTransaction, Ot as loadKnowledgePages, P as lintKnowledgeIndex, Pn as isMissingFile, Pt as KnowledgeBaseCandidateSchema, Q as ClaimLedgerMigrationRequiredError, Qt as assertDeepQuestionIntegrity, R as assertGradeableEvidence, Rn as relativeWithinRoot, Rt as KnowledgeIndexSchema, S as knowledgeVisibilityArtifactRef, Sn as applyKnowledgeFileTransaction, St as SCAFFOLD_PAGE_BASENAMES, T as normalizeVisibilityRef, Tn as finishKnowledgeFileTransaction, Tt as isScaffoldPath, U as verifyGradeableEvidence, Un as withSafeDirectory, Ut as KnowledgeRelationSchema, V as gradeFor, Vn as syncDirectory, Vt as KnowledgeRelationGraphSchema, W as buildKnowledgeIndex, Wn as writeFileDurable, Wt as ResearchClaimEvidenceSchema, X as sourceRegistryPath, Xt as KNOWLEDGE_EVENT_TYPES, Y as snapshotSourceTextInput, Yn as textSourceAdapter, Yt as SourceRecordSchema, Z as writeSourceRegistry, Zt as ClaimLedgerGoalConflictError, _ as createKnowledgeVisibilitySnapshot, _n as isKnowledgeMutationHeld, _t as parseKnowledgeCitationReference, a as applyKnowledgeWriteBlocksFile, an as deepQuestionId, at as assertClaimLedgerId, b as encodeKnowledgeVisibilitySnapshot, bn as withKnowledgeMutation, bt as resolveRunScopedCitation, c as KnowledgeDuplicateIntakeError, cn as materializeRegisteredClaimEvidence, ct as KnowledgeCitationAuditError, d as createFileRunLineageAuthority, dn as normalizeClaimText, dt as assertKnowledgeCitationAudit, en as assertResearchClaimLedgerIntegrity, et as KB_CLAIM_LEDGER_DIR, f as createRunScopedStores, fn as researchSourceVersionKey, ft as assertKnowledgeCitationsResolved, g as assertSchemaVersion, gn as inspectPendingKnowledgeMutation, gt as formatKnowledgeCitationReference, h as KNOWLEDGE_USE_RECEIPT_SCHEMA_VERSION, hn as acquireDurableFileLock, ht as auditKnowledgeCitations, i as applyKnowledgeWriteBlocks, in as claimSourceHost, it as MemoryKbStore, j as detectNearDuplicatePages, jn as canonicalPathsEqual, jt as extractWikilinks, k as visibilityIdentity, kn as DEFAULT_PAGES_DIRECTORY, kt as writeJson, l as assertKnowledgeWriteIntake, ln as mergeClaimLedgers, lt as KnowledgeCitationResolutionError, m as KNOWLEDGE_RECEIPT_DIGEST_ALGORITHM, mn as parseFrontmatter, mt as auditCurrentRunCitations, n as inspectKnowledgeIndex, nn as claimEvidenceId, nt as KB_INDEX_PATH, o as isSafeKnowledgePath, on as emptyClaimLedger, ot as buildKnowledgeGraph, p as originatedPages, pn as formatFrontmatter, pt as assertRunScopedCitationsResolved, q as addSourceText, qn as writeJsonDurableWithinRoot, qt as ResearchSourceVersionSchema, r as stringMetadata, rn as claimId, rt as KB_STORE_DIR, s as parseKnowledgeWriteBlocks, sn as linkClaimContradictions, st as knowledgePageRelations, t as explainKnowledgeTarget, tn as assertTrackedClaimIntegrity, tt as KB_EVENTS_PATH, u as RUN_LINEAGE_BASENAME, un as mergeTrackedClaims, ut as assertCurrentRunCitationsResolved, v as decodeKnowledgeVisibilitySnapshot, vn as recoverPendingKnowledgeMutation, vt as resolveKnowledgeCitation, w as normalizeVisibilityArtifact, wn as commitKnowledgeFileMutations, wt as isKnowledgePagePath, x as knowledgePageDigest, xn as withKnowledgeRead, xt as resolveRunScopedCitations, y as digest, yn as runInKnowledgeMutationScope, yt as resolveKnowledgeCitations, z as claimCheckKey, zn as removeDurable, zt as KnowledgePageInvalidationSchema } from "./inspect-C6YK7af7.js";
2
2
  import { i as textSourceId, n as slugify, r as stableId, t as sha256 } from "./ids-Bevz_pXV.js";
3
- import { A as runSerializedKnowledgeOptimization, C as deterministicRng, D as toOffPolicyTrajectory, E as retrievalHoldoutConfigHash, M as assertImmutableRef, O as jsonCandidateCodec, S as applySessionStickyRetrievalHoldout, T as resetRetrievalHoldoutRegistry, _ as createAgentMemoryBranch, a as createNeo4jAgentMemoryAdapter, b as renderMemoryContext, c as runAgentMemoryImprovement, d as compareAgentMemoryLearning, f as runAgentMemoryLearningExperiment, g as buildAgentMemorySequencesFromBenchmarkCases, h as buildAgentMemorySequenceScenarios, i as AgentMemoryWriteInputSchema, j as scenarioContentFingerprint, k as jsonObjectCandidateCodec, l as createGraphitiMemoryAdapter, m as agentMemorySequenceJudge, n as AgentMemoryKindSchema, o as createMem0MemoryAdapter, p as runAgentMemoryExperiment, r as AgentMemoryScopeSchema, s as mem0MemoryAdapterIdentity, t as AgentMemoryHitSchema, u as graphitiMemoryAdapterIdentity, v as forkAgentMemoryBranchSnapshot, w as emitRetrievalHoldoutBypass, x as applyRetrievalHoldout, y as defaultGetMemoryContext } from "./memory-Bn1Xs1hY.js";
3
+ import { A as runSerializedKnowledgeOptimization, C as deterministicRng, D as toOffPolicyTrajectory, E as retrievalHoldoutConfigHash, M as assertImmutableRef, O as jsonCandidateCodec, S as applySessionStickyRetrievalHoldout, T as resetRetrievalHoldoutRegistry, _ as createAgentMemoryBranch, a as createNeo4jAgentMemoryAdapter, b as renderMemoryContext, c as runAgentMemoryImprovement, d as compareAgentMemoryLearning, f as runAgentMemoryLearningExperiment, g as buildAgentMemorySequencesFromBenchmarkCases, h as buildAgentMemorySequenceScenarios, i as AgentMemoryWriteInputSchema, j as scenarioContentFingerprint, k as jsonObjectCandidateCodec, l as createGraphitiMemoryAdapter, m as agentMemorySequenceJudge, n as AgentMemoryKindSchema, o as createMem0MemoryAdapter, p as runAgentMemoryExperiment, r as AgentMemoryScopeSchema, s as mem0MemoryAdapterIdentity, t as AgentMemoryHitSchema, u as graphitiMemoryAdapterIdentity, v as forkAgentMemoryBranchSnapshot, w as emitRetrievalHoldoutBypass, x as applyRetrievalHoldout, y as defaultGetMemoryContext } from "./memory-BYWNir2t.js";
4
4
  import { C as sleepForMemoryRecovery, F as buildIndustryRagBenchmarkSmokeCases, G as buildRetrievalEvalDispatch, H as createNoopMemoryBenchmarkAdapter, I as respondToIndustryMemoryBenchmarkSmokeCase, J as retrievalConfigSurface, K as partitionRetrievalScenarios, L as respondToIndustryRagBenchmarkSmokeCase, M as INDUSTRY_RAG_BENCHMARKS, N as buildFirstPartyMemoryLifecycleBenchmarkCases, P as buildIndustryMemoryBenchmarkSmokeCases, R as isKnowledgeMemoryBenchmarkCase, S as runBoundedMemoryLifecycle, U as memoryHitToSourceRecord, V as createInMemoryBenchmarkAdapter, W as memoryWriteResultToSourceRecord, X as scoreRetrievalArtifact, Y as retrievalRecallJudge, Z as mean, a as buildKnowledgeBenchmarkScenarios, b as memoryRecoveryDelayMs, c as runKnowledgeBenchmarkSuite, d as summarizeKnowledgeBenchmarkCampaign, f as acquireAgentMemoryRunLease, h as DEFAULT_MEMORY_CLEANUP_TIMEOUT_MS, i as runMemoryAdapterBenchmark, j as INDUSTRY_MEMORY_BENCHMARKS, l as scoreKnowledgeBenchmarkArtifact, m as AgentMemoryLifecycleUnsafeError, n as parseKnowledgeBenchmarkJsonl, o as knowledgeBenchmarkJudge, p as AgentMemoryLifecycleTimeoutError, q as retrievalConfigFromSurface, r as parseKnowledgeBenchmarkQrels, s as renderKnowledgeBenchmarkReportMarkdown, t as buildRetrievalBenchmarkCasesFromQrels, u as scoreMemoryBenchmarkArtifact, x as resolveMemoryCleanupTimeoutMs, y as createMemoryExecutionPool } from "./benchmarks-B3kU-Yxz.js";
5
5
  import { a as buildKnowledgeLexicalIndex, c as tokenizeText, i as searchKnowledgePages, n as reciprocalRankFusion, o as scoreBm25, r as searchKnowledge, s as tokenizeQuery, t as KNOWLEDGE_SEARCH_RETRIEVER_ID } from "./search-Cbyj2shJ.js";
6
6
  import { IRS_DIMENSION_HINTS, MAX_RESPONSE_BYTES, MIN_REQUEST_GAP_MS, POLITE_USER_AGENT, __resetHttpThrottle, createCornellLiiSource, createIrsPublicationsSource, createStateSosSource, extractLinks, firstMatch, htmlToText, innerHtmlById, looksLikeBlockPage, politeFetch } from "./sources/index.js";
7
7
  import { createHash } from "node:crypto";
8
+ import { TCloudClient, TCloudError } from "@tangle-network/tcloud";
8
9
  import { cp, lstat, mkdir, mkdtemp, readFile, rm, stat } from "node:fs/promises";
9
10
  import { dirname, join } from "node:path";
10
- import { agentImprovementActivationResultSchema, agentImprovementActivationSchema, canonicalCandidateBytes, canonicalCandidateDigest, canonicalCandidateJson, omitTopLevelDigest, sha256Bytes, sha256DigestSchema } from "@tangle-network/agent-interface";
11
+ import { agentImprovementActivationResultSchema, agentImprovementActivationSchema, canonicalCandidateDigest, canonicalCandidateJson, omitTopLevelDigest, sha256Bytes, sha256DigestSchema } from "@tangle-network/agent-interface";
11
12
  import { acquisitionPlansForKnowledgeGaps, blockingKnowledgeEval, canonicalJson, contentHash, evaluateReleaseConfidence, objectiveEval, scoreKnowledgeReadiness, userQuestionsForKnowledgeGaps, validateRunRecord } from "@tangle-network/agent-eval";
12
13
  import { z } from "zod";
13
14
  import { surfaceHash } from "@tangle-network/agent-eval/campaign";
@@ -39,10 +40,9 @@ import { pathToFileURL } from "node:url";
39
40
  * worker ADDS; the driver GATES. Together they build a cleaner knowledge base
40
41
  * than a single agent at the same compute budget.
41
42
  *
42
- * Dependency-free on purpose: it talks to the router over `fetch` directly with
43
- * the published OpenAI-compatible chat shape and the `/v1/search` shape, so it
44
- * works whether or not the `tcloud` CLI is installed. Point it at any router by
45
- * passing `baseUrl`; supply the key via `apiKey` or `TANGLE_API_KEY`.
43
+ * Model and search requests use the published TCloud SDK. Knowledge owns the
44
+ * research policy, not HTTP, bearer headers, retries or provider rate cards.
45
+ * Point it at any router with `baseUrl`; supply `apiKey` or `TANGLE_API_KEY`.
46
46
  */
47
47
  /** Default router model. Plain id (no namespace) — see CLAUDE/creds notes. */
48
48
  const DEFAULT_MODEL = "glm-5.2";
@@ -54,35 +54,7 @@ const DEFAULT_BASE_URL = "https://router.tangle.tools/v1";
54
54
  * reasoning model never silently yields nothing. (Verified creds note.)
55
55
  */
56
56
  const MIN_MAX_TOKENS = 1200;
57
- /** Transient upstream statuses worth a retry (capacity / rate-limit / gateway). */
58
- const transientStatuses = /* @__PURE__ */ new Set([
59
- 429,
60
- 502,
61
- 503,
62
- 504
63
- ]);
64
- /**
65
- * POST with bounded exponential backoff on transient upstream statuses. Returns
66
- * the first `res.ok` response, or the LAST response (so the caller throws the
67
- * real status). A non-transient failure returns immediately — only 502/503/504/
68
- * 429 are retried. Aborts propagate at once.
69
- */
70
- async function fetchWithRetry(url, init, opts) {
71
- let lastRes;
72
- for (let attempt = 0; attempt <= opts.maxRetries; attempt += 1) {
73
- if (opts.signal?.aborted) throw new RouterError(0, "aborted");
74
- const res = await fetch(url, init);
75
- if (res.ok || !transientStatuses.has(res.status)) return res;
76
- lastRes = res;
77
- if (attempt === opts.maxRetries) break;
78
- await res.text().catch(() => "");
79
- const jitter = opts.retryBaseMs * 2 ** attempt * (.75 + Math.random() * .5);
80
- await new Promise((resolve) => setTimeout(resolve, jitter));
81
- }
82
- if (lastRes) return lastRes;
83
- throw new RouterError(0, "fetchWithRetry produced no response");
84
- }
85
- /** A small error so a failed router call fails loud rather than returning junk. */
57
+ /** Compatibility error facade. HTTP execution and classification belong to TCloud. */
86
58
  var RouterError = class extends Error {
87
59
  status;
88
60
  constructor(status, message) {
@@ -91,26 +63,26 @@ var RouterError = class extends Error {
91
63
  this.name = "RouterError";
92
64
  }
93
65
  };
94
- /**
95
- * Build a dependency-free Tangle router client over `fetch`. This is the same
96
- * wire surface the `tcloud` SDK + `tcloud mcp` use (`/v1/search` for web search,
97
- * `/v1/chat/completions` for chat) so it needs no CLI installed.
98
- */
66
+ /** Adapt Knowledge's public contract to the published SDK, without a second transport. */
99
67
  function createTangleRouterClient(options = {}) {
100
- const baseUrl = (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/$/, "");
101
68
  const apiKey = options.apiKey ?? process.env.TANGLE_API_KEY;
102
69
  if (!apiKey) throw new RouterError(401, "no TANGLE_API_KEY (pass apiKey or set the env var)");
103
- const model = options.model ?? DEFAULT_MODEL;
104
- const maxRetries = Math.max(0, options.maxRetries ?? 4);
105
- const retryBaseMs = Math.max(1, options.retryBaseMs ?? 1500);
106
- const headers = {
107
- "Content-Type": "application/json",
108
- Authorization: `Bearer ${apiKey}`
109
- };
110
- const price = {
111
- prompt: .95 / 1e6,
112
- completion: 3 / 1e6
113
- };
70
+ const client = new TCloudClient({
71
+ baseURL: (options.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, ""),
72
+ apiKey,
73
+ model: options.model ?? DEFAULT_MODEL,
74
+ timeout: 0,
75
+ retry: {
76
+ maxRetries: Math.max(0, options.maxRetries ?? 4),
77
+ initialBackoffMs: Math.max(1, options.retryBaseMs ?? 1500),
78
+ retryableStatuses: [
79
+ 429,
80
+ 502,
81
+ 503,
82
+ 504
83
+ ]
84
+ }
85
+ });
114
86
  const acc = {
115
87
  chatCalls: 0,
116
88
  searchCalls: 0,
@@ -119,70 +91,79 @@ function createTangleRouterClient(options = {}) {
119
91
  usd: 0,
120
92
  wallMs: 0
121
93
  };
94
+ const recordCost = (cost) => {
95
+ acc.usd += typeof cost === "number" && Number.isFinite(cost) && cost >= 0 ? cost : NaN;
96
+ };
97
+ const translate = (error) => {
98
+ options.signal?.throwIfAborted();
99
+ if (error instanceof TCloudError) throw new RouterError(error.status, error.message);
100
+ throw error;
101
+ };
122
102
  return {
123
103
  async search(query, opts) {
124
- const t0 = Date.now();
125
- const res = await fetchWithRetry(`${baseUrl}/search`, {
126
- method: "POST",
127
- headers,
128
- signal: options.signal,
129
- body: JSON.stringify({
104
+ options.signal?.throwIfAborted();
105
+ const started = Date.now();
106
+ try {
107
+ const response = await client.search({
130
108
  query,
131
109
  ...options.searchProvider ? { provider: options.searchProvider } : {},
132
- ...opts?.maxResults != null ? { maxResults: opts.maxResults } : {}
133
- })
134
- }, {
135
- maxRetries,
136
- retryBaseMs,
137
- signal: options.signal
138
- });
139
- acc.searchCalls += 1;
140
- acc.wallMs += Date.now() - t0;
141
- if (!res.ok) throw new RouterError(res.status, await res.text().catch(() => res.statusText));
142
- return ((await res.json()).data ?? []).filter((hit) => typeof hit?.url === "string" && hit.url.length > 0).map((hit) => ({
143
- title: hit.title ?? hit.url,
144
- url: hit.url,
145
- snippet: hit.snippet
146
- }));
110
+ ...opts?.maxResults != null ? { maxResults: opts.maxResults } : {},
111
+ signal: options.signal
112
+ });
113
+ recordCost(response.usage?.billed_cost);
114
+ return (response.data ?? []).filter((hit) => typeof hit?.url === "string" && hit.url.length > 0).map((hit) => ({
115
+ title: hit.title ?? hit.url,
116
+ url: hit.url,
117
+ snippet: hit.snippet
118
+ }));
119
+ } catch (error) {
120
+ return translate(error);
121
+ } finally {
122
+ acc.searchCalls += 1;
123
+ acc.wallMs += Date.now() - started;
124
+ }
147
125
  },
148
126
  async chat(messages, maxTokens) {
149
- const max_tokens = Math.max(MIN_MAX_TOKENS, maxTokens ?? MIN_MAX_TOKENS);
150
- const t0 = Date.now();
151
- const res = await fetchWithRetry(`${baseUrl}/chat/completions`, {
152
- method: "POST",
153
- headers,
154
- signal: options.signal,
155
- body: JSON.stringify({
156
- model,
127
+ options.signal?.throwIfAborted();
128
+ const started = Date.now();
129
+ try {
130
+ const response = await client.chat({
157
131
  messages,
158
- max_tokens,
132
+ maxTokens: Math.max(MIN_MAX_TOKENS, maxTokens ?? MIN_MAX_TOKENS),
159
133
  temperature: .2,
160
- stream: false
161
- })
162
- }, {
163
- maxRetries,
164
- retryBaseMs,
165
- signal: options.signal
166
- });
167
- if (!res.ok) throw new RouterError(res.status, await res.text().catch(() => res.statusText));
168
- const body = await res.json();
169
- const promptTokens = body.usage?.prompt_tokens ?? 0;
170
- const completionTokens = body.usage?.completion_tokens ?? 0;
171
- acc.chatCalls += 1;
172
- acc.promptTokens += promptTokens;
173
- acc.completionTokens += completionTokens;
174
- acc.usd += promptTokens * price.prompt + completionTokens * price.completion;
175
- acc.wallMs += Date.now() - t0;
176
- return body.choices?.[0]?.message?.content ?? "";
134
+ signal: options.signal
135
+ });
136
+ acc.chatCalls += 1;
137
+ acc.promptTokens += response.usage?.prompt_tokens ?? 0;
138
+ acc.completionTokens += response.usage?.completion_tokens ?? 0;
139
+ recordCost(response.tangle?.costSource === "receipt" ? response.tangle.costUsd : void 0);
140
+ return response.choices?.[0]?.message?.content ?? "";
141
+ } catch (error) {
142
+ return translate(error);
143
+ } finally {
144
+ acc.wallMs += Date.now() - started;
145
+ }
177
146
  },
178
147
  usage() {
179
148
  return { ...acc };
180
149
  }
181
150
  };
182
151
  }
183
- /** Resolve the router client lazily so a worker with an injected client never reads env. */
184
- function resolveRouter(opts) {
185
- return opts.router ?? createTangleRouterClient(opts.router_options);
152
+ /** Bind run cancellation to the default router without changing an injected client. */
153
+ function resolveRouter(opts, signal) {
154
+ if (opts.router) return opts.router;
155
+ const requestSignal = combineRouterSignals(signal, opts.router_options?.signal);
156
+ return createTangleRouterClient({
157
+ ...opts.router_options,
158
+ signal: requestSignal
159
+ });
160
+ }
161
+ function combineRouterSignals(runSignal, configuredSignal) {
162
+ return runSignal && configuredSignal ? AbortSignal.any([runSignal, configuredSignal]) : runSignal ?? configuredSignal;
163
+ }
164
+ function throwIfRouterAborted(runSignal, opts) {
165
+ runSignal?.throwIfAborted();
166
+ if (!opts.router) opts.router_options?.signal?.throwIfAborted();
186
167
  }
187
168
  /**
188
169
  * The real web-research worker. Conforms to the loop's `ResearchWorker`
@@ -196,33 +177,41 @@ function createWebResearchWorker(options = {}) {
196
177
  const minTextChars = Math.max(1, options.minTextChars ?? 200);
197
178
  const maxTextChars = Math.max(minTextChars, options.maxTextChars ?? 4e3);
198
179
  return async (ctx) => {
199
- const router = resolveRouter(options);
180
+ const assertActive = () => throwIfRouterAborted(ctx.signal, options);
181
+ assertActive();
182
+ const router = resolveRouter(options, ctx.signal);
183
+ const fetchSignal = options.router ? ctx.signal : combineRouterSignals(ctx.signal, options.router_options?.signal);
200
184
  const targetGaps = ctx.gaps.filter((gap) => gap.blocking);
201
185
  const gaps = targetGaps.length > 0 ? targetGaps : ctx.gaps;
202
186
  if (gaps.length === 0) return {
203
187
  sources: [],
204
188
  notes: "no open gaps to research"
205
189
  };
206
- const queries = await formSearchQueries(router, ctx, gaps, queriesPerGap);
190
+ const queries = await formSearchQueries(router, ctx, gaps, queriesPerGap, assertActive);
191
+ assertActive();
207
192
  const proposals = [];
208
193
  const seenUris = /* @__PURE__ */ new Set();
209
194
  for (const query of queries) {
210
195
  if (proposals.length >= maxSourcesPerRound) break;
211
- if (ctx.signal?.aborted) break;
196
+ assertActive();
212
197
  let hits;
213
198
  try {
214
199
  hits = await router.search(query, { maxResults: resultsPerQuery });
215
200
  } catch (error) {
216
- if (error.name === "AbortError") break;
201
+ assertActive();
202
+ if (error?.name === "AbortError") throw error;
217
203
  continue;
218
204
  }
205
+ assertActive();
219
206
  for (const hit of hits) {
207
+ assertActive();
220
208
  if (proposals.length >= maxSourcesPerRound) break;
221
209
  if (seenUris.has(hit.url)) continue;
222
210
  const fetched = await politeFetch(hit.url, {
223
- signal: ctx.signal,
211
+ signal: fetchSignal,
224
212
  cacheDir: options.cacheDir
225
213
  });
214
+ assertActive();
226
215
  if (!fetched.verifiable) continue;
227
216
  const text = htmlToText(fetched.body).slice(0, maxTextChars);
228
217
  if (text.length < minTextChars) continue;
@@ -241,6 +230,7 @@ function createWebResearchWorker(options = {}) {
241
230
  });
242
231
  }
243
232
  }
233
+ assertActive();
244
234
  return {
245
235
  sources: proposals,
246
236
  buildPages: buildCitingPages(proposals),
@@ -253,7 +243,7 @@ function createWebResearchWorker(options = {}) {
253
243
  * to the gap's own readiness query if the model returns nothing parseable, so a
254
244
  * model hiccup degrades to a sane search rather than an empty round.
255
245
  */
256
- async function formSearchQueries(router, ctx, gaps, queriesPerGap) {
246
+ async function formSearchQueries(router, ctx, gaps, queriesPerGap, assertActive) {
257
247
  const gapLines = gaps.map((gap, i) => `${i + 1}. ${gap.description} (readiness query: "${gap.query}")`).join("\n");
258
248
  const want = gaps.length * queriesPerGap;
259
249
  const system = "You are a research librarian. Turn knowledge gaps into precise web search queries that will surface authoritative primary sources (papers, docs, standards, official pages). Return ONLY a JSON array of query strings, no prose.";
@@ -272,7 +262,10 @@ async function formSearchQueries(router, ctx, gaps, queriesPerGap) {
272
262
  role: "user",
273
263
  content: user
274
264
  }], MIN_MAX_TOKENS);
275
- } catch {
265
+ assertActive();
266
+ } catch (error) {
267
+ assertActive();
268
+ if (error?.name === "AbortError") throw error;
276
269
  raw = "";
277
270
  }
278
271
  const fromLlm = parseQueryList(raw).slice(0, want);
@@ -354,7 +347,9 @@ function escapeYaml(value) {
354
347
  function createVerifyingResearchDriver(options = {}) {
355
348
  const acceptOnParseFailure = options.acceptOnParseFailure ?? false;
356
349
  return { async verifySource(source, ctx) {
357
- const router = resolveRouter(options);
350
+ const assertActive = () => throwIfRouterAborted(ctx.signal, options);
351
+ assertActive();
352
+ const router = resolveRouter(options, ctx.signal);
358
353
  const gapLines = ctx.gaps.map((gap) => `- ${gap.description} (query: "${gap.query}")`).join("\n");
359
354
  const acceptedTitles = ctx.acceptedThisRound.map((accepted) => `- ${accepted.title ?? accepted.uri}`).join("\n");
360
355
  const excerpt = source.text.slice(0, 1500);
@@ -375,8 +370,10 @@ function createVerifyingResearchDriver(options = {}) {
375
370
  role: "user",
376
371
  content: user
377
372
  }], MIN_MAX_TOKENS);
373
+ assertActive();
378
374
  } catch (error) {
379
- if (error.name === "AbortError") throw error;
375
+ assertActive();
376
+ if (error?.name === "AbortError") throw error;
380
377
  return acceptOnParseFailure ? { accept: true } : {
381
378
  accept: false,
382
379
  reason: `verifier unavailable: ${error.message}`
@@ -494,7 +491,8 @@ const trackingParams = /* @__PURE__ */ new Set([
494
491
  * mirror/syndication of an already-accepted source even when the URL differs.
495
492
  */
496
493
  function contentKey(text) {
497
- return sha256(text.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, " ").replace(/\s+/g, " ").trim());
494
+ const normalized = text.toLowerCase().replace(/[^\p{L}\p{N}\s]+/gu, " ").replace(/\s+/g, " ").trim();
495
+ return sha256(normalized);
498
496
  }
499
497
  /** Default authoritative host suffixes — papers, official docs, standards. */
500
498
  const defaultAuthoritativeHosts = [
@@ -934,7 +932,8 @@ function knowledgeImprovementRunDir(root, runId) {
934
932
  return runDir;
935
933
  }
936
934
  async function withKnowledgeImprovementRun(root, runId, create, use) {
937
- const relativePath = relativeWithinRoot(root, knowledgeImprovementRunDir(root, runId));
935
+ const runDir = knowledgeImprovementRunDir(root, runId);
936
+ const relativePath = relativeWithinRoot(root, runDir);
938
937
  if (!relativePath) throw new Error("knowledge improvement run directory escaped its root");
939
938
  return withSafeDirectory(root, relativePath, create, async (openedRunDir) => {
940
939
  const result = await use(openedRunDir);
@@ -1005,8 +1004,9 @@ function parseKnowledgeImprovementEvent(value) {
1005
1004
  return event;
1006
1005
  }
1007
1006
  async function acquireRunLease(runDir, options) {
1007
+ const path = join(runDir, "run.lock.durable");
1008
1008
  const acquired = await acquireDurableFileLock(runDir, {
1009
- lockfilePath: join(runDir, "run.lock.durable"),
1009
+ lockfilePath: path,
1010
1010
  staleMs: options.ttlMs
1011
1011
  });
1012
1012
  return {
@@ -2461,100 +2461,114 @@ async function runRagKnowledgeImprovementPhases(options, previous) {
2461
2461
  let knowledgeUpdate = previous?.knowledgeUpdate;
2462
2462
  let answerQuality = previous?.answerQuality;
2463
2463
  let promotion = previous?.promotion;
2464
- if (phaseEnabled(options, "gap-diagnosis")) if (options.diagnose) findings = [...await runPhase(phases, now, "gap-diagnosis", async () => {
2465
- assertNotAborted(options.signal);
2466
- return options.diagnose({
2467
- goal: options.goal,
2468
- phases,
2469
- optimization: selectRagOptimization(optimization),
2470
- signal: options.signal,
2471
- retrieval: selectRetrievalOptimization(retrieval)
2472
- });
2473
- }, (diagnosed) => `${diagnosed.length} finding(s)`)];
2474
- else skipPhase(phases, now, "gap-diagnosis", "no diagnosis hook provided");
2475
- if (phaseEnabled(options, "knowledge-acquisition")) if (options.acquireKnowledge) acquisition = await runPhase(phases, now, "knowledge-acquisition", async () => {
2476
- assertNotAborted(options.signal);
2477
- return options.acquireKnowledge({
2478
- goal: options.goal,
2479
- phases,
2480
- optimization: selectRagOptimization(optimization),
2481
- signal: options.signal,
2482
- retrieval: selectRetrievalOptimization(retrieval),
2483
- findings
2484
- });
2485
- }, summarizeAcquisitionDecision);
2486
- else skipPhase(phases, now, "knowledge-acquisition", "no acquisition hook provided");
2487
- if (phaseEnabled(options, "knowledge-update")) if (options.updateKnowledge) knowledgeUpdate = await runPhase(phases, now, "knowledge-update", async () => {
2488
- assertNotAborted(options.signal);
2489
- return options.updateKnowledge({
2490
- goal: options.goal,
2491
- phases,
2492
- optimization: selectRagOptimization(optimization),
2493
- signal: options.signal,
2494
- retrieval: selectRetrievalOptimization(retrieval),
2495
- findings,
2496
- acquisition
2497
- });
2498
- }, (result) => result.summary);
2499
- else if (options.knowledgeResearch) knowledgeUpdate = await runPhase(phases, now, "knowledge-update", async () => {
2500
- assertNotAborted(options.signal);
2501
- return runKnowledgeResearchUpdate(options, acquisition);
2502
- }, (result) => result.summary);
2503
- else skipPhase(phases, now, "knowledge-update", "no update hook or research loop provided");
2504
- if (phaseEnabled(options, "rag-optimization") && (options.optimization || options.enabledPhases?.includes("rag-optimization") || options.requiredPhases?.includes("rag-optimization"))) if (options.optimization) optimization = await runPhase(phases, now, "rag-optimization", async () => {
2505
- assertNotAborted(options.signal);
2506
- return runRagOptimization(options.optimization);
2507
- }, summarizeRagOptimization);
2508
- else skipPhase(phases, now, "rag-optimization", "no full RAG optimization options provided");
2509
- if (phaseEnabled(options, "retrieval-tuning")) if (options.retrieval) retrieval = await runPhase(phases, now, "retrieval-tuning", async () => {
2510
- assertNotAborted(options.signal);
2511
- return runRetrievalImprovementLoop(options.retrieval);
2512
- }, summarizeRetrievalResult);
2513
- else skipPhase(phases, now, "retrieval-tuning", "no retrieval options provided");
2514
- if (phaseEnabled(options, "answer-quality")) if (options.evaluateAnswers) {
2515
- answerQuality = await runPhase(phases, now, "answer-quality", async () => {
2464
+ if (phaseEnabled(options, "gap-diagnosis")) {
2465
+ if (options.diagnose) findings = [...await runPhase(phases, now, "gap-diagnosis", async () => {
2466
+ assertNotAborted(options.signal);
2467
+ return options.diagnose({
2468
+ goal: options.goal,
2469
+ phases,
2470
+ optimization: selectRagOptimization(optimization),
2471
+ signal: options.signal,
2472
+ retrieval: selectRetrievalOptimization(retrieval)
2473
+ });
2474
+ }, (diagnosed) => `${diagnosed.length} finding(s)`)];
2475
+ else skipPhase(phases, now, "gap-diagnosis", "no diagnosis hook provided");
2476
+ }
2477
+ if (phaseEnabled(options, "knowledge-acquisition")) {
2478
+ if (options.acquireKnowledge) acquisition = await runPhase(phases, now, "knowledge-acquisition", async () => {
2479
+ assertNotAborted(options.signal);
2480
+ return options.acquireKnowledge({
2481
+ goal: options.goal,
2482
+ phases,
2483
+ optimization: selectRagOptimization(optimization),
2484
+ signal: options.signal,
2485
+ retrieval: selectRetrievalOptimization(retrieval),
2486
+ findings
2487
+ });
2488
+ }, summarizeAcquisitionDecision);
2489
+ else skipPhase(phases, now, "knowledge-acquisition", "no acquisition hook provided");
2490
+ }
2491
+ if (phaseEnabled(options, "knowledge-update")) {
2492
+ if (options.updateKnowledge) knowledgeUpdate = await runPhase(phases, now, "knowledge-update", async () => {
2493
+ assertNotAborted(options.signal);
2494
+ return options.updateKnowledge({
2495
+ goal: options.goal,
2496
+ phases,
2497
+ optimization: selectRagOptimization(optimization),
2498
+ signal: options.signal,
2499
+ retrieval: selectRetrievalOptimization(retrieval),
2500
+ findings,
2501
+ acquisition
2502
+ });
2503
+ }, (result) => result.summary);
2504
+ else if (options.knowledgeResearch) knowledgeUpdate = await runPhase(phases, now, "knowledge-update", async () => {
2505
+ assertNotAborted(options.signal);
2506
+ return runKnowledgeResearchUpdate(options, acquisition);
2507
+ }, (result) => result.summary);
2508
+ else skipPhase(phases, now, "knowledge-update", "no update hook or research loop provided");
2509
+ }
2510
+ if (phaseEnabled(options, "rag-optimization") && (options.optimization || options.enabledPhases?.includes("rag-optimization") || options.requiredPhases?.includes("rag-optimization"))) {
2511
+ if (options.optimization) optimization = await runPhase(phases, now, "rag-optimization", async () => {
2512
+ assertNotAborted(options.signal);
2513
+ return runRagOptimization(options.optimization);
2514
+ }, summarizeRagOptimization);
2515
+ else skipPhase(phases, now, "rag-optimization", "no full RAG optimization options provided");
2516
+ }
2517
+ if (phaseEnabled(options, "retrieval-tuning")) {
2518
+ if (options.retrieval) retrieval = await runPhase(phases, now, "retrieval-tuning", async () => {
2519
+ assertNotAborted(options.signal);
2520
+ return runRetrievalImprovementLoop(options.retrieval);
2521
+ }, summarizeRetrievalResult);
2522
+ else skipPhase(phases, now, "retrieval-tuning", "no retrieval options provided");
2523
+ }
2524
+ if (phaseEnabled(options, "answer-quality")) {
2525
+ if (options.evaluateAnswers) {
2526
+ answerQuality = await runPhase(phases, now, "answer-quality", async () => {
2527
+ assertNotAborted(options.signal);
2528
+ const result = await options.evaluateAnswers({
2529
+ goal: options.goal,
2530
+ phases,
2531
+ optimization: selectRagOptimization(optimization),
2532
+ signal: options.signal,
2533
+ retrieval: selectRetrievalOptimization(retrieval),
2534
+ findings,
2535
+ acquisition,
2536
+ knowledgeUpdate
2537
+ });
2538
+ assertRagAnswerEvidence(result);
2539
+ return result;
2540
+ }, summarizeAnswerQuality);
2541
+ findings = [...findings, ...answerQuality.findings ?? []];
2542
+ } else skipPhase(phases, now, "answer-quality", "no answer-quality hook provided");
2543
+ }
2544
+ if (phaseEnabled(options, "promotion")) {
2545
+ if (options.decidePromotion) promotion = await runPhase(phases, now, "promotion", async () => {
2516
2546
  assertNotAborted(options.signal);
2517
- const result = await options.evaluateAnswers({
2547
+ const evidenceRejection = rejectUnsafePromotionEvidence({
2548
+ optimization: optimization?.comparison,
2549
+ optimizationCostCeiling: options.optimization?.costCeiling,
2550
+ retrieval: retrieval?.comparison,
2551
+ retrievalCostCeiling: options.retrieval?.costCeiling,
2552
+ answerQuality,
2553
+ answerQualityCostCeiling: options.answerQualityCostCeiling
2554
+ });
2555
+ if (evidenceRejection) return evidenceRejection;
2556
+ return options.decidePromotion({
2518
2557
  goal: options.goal,
2519
2558
  phases,
2520
2559
  optimization: selectRagOptimization(optimization),
2560
+ optimizationComparison: optimization?.comparison,
2521
2561
  signal: options.signal,
2522
2562
  retrieval: selectRetrievalOptimization(retrieval),
2563
+ retrievalComparison: retrieval?.comparison,
2523
2564
  findings,
2524
2565
  acquisition,
2525
- knowledgeUpdate
2566
+ knowledgeUpdate,
2567
+ answerQuality
2526
2568
  });
2527
- assertRagAnswerEvidence(result);
2528
- return result;
2529
- }, summarizeAnswerQuality);
2530
- findings = [...findings, ...answerQuality.findings ?? []];
2531
- } else skipPhase(phases, now, "answer-quality", "no answer-quality hook provided");
2532
- if (phaseEnabled(options, "promotion")) if (options.decidePromotion) promotion = await runPhase(phases, now, "promotion", async () => {
2533
- assertNotAborted(options.signal);
2534
- const evidenceRejection = rejectUnsafePromotionEvidence({
2535
- optimization: optimization?.comparison,
2536
- optimizationCostCeiling: options.optimization?.costCeiling,
2537
- retrieval: retrieval?.comparison,
2538
- retrievalCostCeiling: options.retrieval?.costCeiling,
2539
- answerQuality,
2540
- answerQualityCostCeiling: options.answerQualityCostCeiling
2541
- });
2542
- if (evidenceRejection) return evidenceRejection;
2543
- return options.decidePromotion({
2544
- goal: options.goal,
2545
- phases,
2546
- optimization: selectRagOptimization(optimization),
2547
- optimizationComparison: optimization?.comparison,
2548
- signal: options.signal,
2549
- retrieval: selectRetrievalOptimization(retrieval),
2550
- retrievalComparison: retrieval?.comparison,
2551
- findings,
2552
- acquisition,
2553
- knowledgeUpdate,
2554
- answerQuality
2555
- });
2556
- }, (result) => `${result.promoted ? "promoted" : "held"}: ${result.reason}`);
2557
- else skipPhase(phases, now, "promotion", "no promotion decision hook provided");
2569
+ }, (result) => `${result.promoted ? "promoted" : "held"}: ${result.reason}`);
2570
+ else skipPhase(phases, now, "promotion", "no promotion decision hook provided");
2571
+ }
2558
2572
  return {
2559
2573
  goal: options.goal,
2560
2574
  phases,
@@ -2801,9 +2815,11 @@ async function withMeasuredCandidateSnapshot(liveRoot, runDir, state, candidateR
2801
2815
  await assertStateIdentity(liveRoot, candidateRef, state);
2802
2816
  const candidate = state.candidates.find((entry) => entry.candidateId === candidateRef.candidateId);
2803
2817
  if (!candidate) throw new Error(`knowledge candidate '${candidateRef.candidateId}' does not exist`);
2804
- if (canonicalJson(candidateRefFor(candidateRef.runId, state, candidate)) !== canonicalJson(candidateRef)) throw new Error("knowledge candidate approval does not match the measured candidate");
2818
+ const expectedRef = candidateRefFor(candidateRef.runId, state, candidate);
2819
+ if (canonicalJson(expectedRef) !== canonicalJson(candidateRef)) throw new Error("knowledge candidate approval does not match the measured candidate");
2805
2820
  const evidence = await assertCandidateEvidence(runDir, candidateRef, state.implementationRef);
2806
- return withSafeDirectory(runDir, join("candidates", candidate.candidateId, "snapshots", candidateRef.candidateHash), false, async (root) => {
2821
+ const relativePath = join("candidates", candidate.candidateId, "snapshots", candidateRef.candidateHash);
2822
+ return withSafeDirectory(runDir, relativePath, false, async (root) => {
2807
2823
  if (await hashKnowledgeBase(root, state.stateScope) !== candidateRef.candidateHash) throw new Error("knowledge candidate snapshot changed after approval");
2808
2824
  const result = await use({
2809
2825
  root,
@@ -2906,7 +2922,8 @@ async function withBaselineSnapshot(runDir, expectedHash, use, scope) {
2906
2922
  });
2907
2923
  }
2908
2924
  async function withFrozenCandidateWorkspace(runDir, candidate, candidateRoot, use, scope) {
2909
- return withSafeDirectory(runDir, join("candidates", safePathSegmentSchema.parse(candidate.candidateId), "snapshots"), true, async (snapshotsDir) => {
2925
+ const snapshotsPath = join("candidates", safePathSegmentSchema.parse(candidate.candidateId), "snapshots");
2926
+ return withSafeDirectory(runDir, snapshotsPath, true, async (snapshotsDir) => {
2910
2927
  const preparation = await mkdtemp(join(snapshotsDir, "prepare-"));
2911
2928
  let activated = false;
2912
2929
  try {
@@ -3325,7 +3342,8 @@ async function knowledgePlanMutations(targetRoot, plan) {
3325
3342
  }));
3326
3343
  }
3327
3344
  function assertCandidateTransitionPlan(plan, candidate, target, scope) {
3328
- const actualPlanHash = knowledgeFileTransactionPlanHash(target === "candidate" ? plan : reverseKnowledgeFilePlan(plan), normalizeKnowledgeStateScope(scope).pagesDirectory, scope?.researchState);
3345
+ const approvedDirection = target === "candidate" ? plan : reverseKnowledgeFilePlan(plan);
3346
+ const actualPlanHash = knowledgeFileTransactionPlanHash(approvedDirection, normalizeKnowledgeStateScope(scope).pagesDirectory, scope?.researchState);
3329
3347
  if (actualPlanHash !== candidate.promotionPlanHash) throw new Error(`knowledge candidate plan changed after approval: expected ${candidate.promotionPlanHash}, got ${actualPlanHash}`);
3330
3348
  }
3331
3349
  function assertCandidateTransitionTransaction(transaction, candidate, target, scope) {
@@ -3929,7 +3947,7 @@ async function improveSelectedKnowledgeCandidate(options) {
3929
3947
  if (sourcePlanHash !== sourceCandidate.promotionPlanHash) throw new Error("source knowledge candidate plan no longer matches its measured identity");
3930
3948
  const changedSourcePlan = sourcePlan.filter(planEntryChanged).filter((entry) => notDerivedPath(entry, scope));
3931
3949
  const selectedPaths = normalizeSelectedPaths(options.selectedPaths, changedSourcePlan, scope);
3932
- const selectionDigest = contentHash(immutableJson({
3950
+ const selectionMaterial = immutableJson({
3933
3951
  kind: "measured-knowledge-change-selection",
3934
3952
  version: 1,
3935
3953
  sourceCandidate,
@@ -3937,7 +3955,8 @@ async function improveSelectedKnowledgeCandidate(options) {
3937
3955
  selectedPaths,
3938
3956
  ...options.rationale?.trim() ? { rationale: options.rationale.trim() } : {},
3939
3957
  ...metadata ? { metadata } : {}
3940
- }));
3958
+ });
3959
+ const selectionDigest = contentHash(selectionMaterial);
3941
3960
  const derivedImplementationRef = immutableRefSchema.parse(`sha256:${contentHash({
3942
3961
  engine: "agent-knowledge/measured-selection-v1",
3943
3962
  implementationRef: requestedImplementationRef,
@@ -4112,7 +4131,8 @@ async function persistSelectionReceipt(root, receipt) {
4112
4131
  const runDir = knowledgeImprovementRunDir(root, receipt.runId);
4113
4132
  const relativePath = join("candidates", safePathSegmentSchema.parse(receipt.derivedCandidateId), "selection.json").replace(/\\/g, "/");
4114
4133
  try {
4115
- if (canonicalJson(measuredSelectionReceiptSchema.parse(JSON.parse((await readRegularFileWithinRoot(runDir, relativePath)).bytes.toString("utf8")))) !== canonicalJson(receipt)) throw new Error("measured knowledge selection receipt conflicts with durable content");
4134
+ const existing = measuredSelectionReceiptSchema.parse(JSON.parse((await readRegularFileWithinRoot(runDir, relativePath)).bytes.toString("utf8")));
4135
+ if (canonicalJson(existing) !== canonicalJson(receipt)) throw new Error("measured knowledge selection receipt conflicts with durable content");
4116
4136
  return;
4117
4137
  } catch (error) {
4118
4138
  if (!isMissingFile(error)) throw error;
@@ -5527,17 +5547,22 @@ async function materialFactsSurfaced(kb, checklist) {
5527
5547
  * and `searchKnowledge` — and reinvents none of them.
5528
5548
  */
5529
5549
  async function runVerifiedResearchLoop(options) {
5550
+ const assertActive = () => options.signal?.throwIfAborted();
5551
+ assertActive();
5530
5552
  const maxRounds = Math.max(1, options.maxRounds ?? 3);
5531
5553
  await initKnowledgeBase(options.root);
5554
+ assertActive();
5532
5555
  const store = new FileSystemKbStore({ root: options.root });
5533
5556
  const steps = [];
5534
5557
  let index = await buildKnowledgeIndex(options.root);
5558
+ assertActive();
5535
5559
  await confirmRegisteredSources(options.driver, index.sources);
5560
+ assertActive();
5536
5561
  let readiness = readinessFor(options, index);
5537
5562
  let ready = isReady(readiness?.report) && (options.driver.isComplete?.() ?? true);
5538
5563
  let steer;
5539
5564
  for (let round = 1; round <= maxRounds && !ready; round++) {
5540
- if (options.signal?.aborted) throw new Error("Verified research loop aborted");
5565
+ assertActive();
5541
5566
  const gaps = gapsFromReadiness(readiness);
5542
5567
  const workerContribution = await options.worker({
5543
5568
  root: options.root,
@@ -5549,6 +5574,7 @@ async function runVerifiedResearchLoop(options) {
5549
5574
  readiness: requireReadiness(readiness, options),
5550
5575
  signal: options.signal
5551
5576
  });
5577
+ assertActive();
5552
5578
  const accepted = [];
5553
5579
  const rejectedWorkerSources = [];
5554
5580
  const existingUris = new Set(index.sources.flatMap((source) => typeof source.metadata?.originalUri === "string" ? [source.metadata.originalUri] : []));
@@ -5570,6 +5596,7 @@ async function runVerifiedResearchLoop(options) {
5570
5596
  acceptedThisRound: accepted,
5571
5597
  signal: options.signal
5572
5598
  });
5599
+ assertActive();
5573
5600
  if (verdict.accept) accepted.push(source);
5574
5601
  else rejectedWorkerSources.push({
5575
5602
  source,
@@ -5577,10 +5604,14 @@ async function runVerifiedResearchLoop(options) {
5577
5604
  });
5578
5605
  }
5579
5606
  const acceptedWorkerSources = await registerSources(options, accepted);
5607
+ assertActive();
5580
5608
  await confirmRegisteredSources(options.driver, acceptedWorkerSources);
5609
+ assertActive();
5581
5610
  const writtenPages = [];
5582
5611
  writtenPages.push(...await applyPages(options.root, workerContribution, acceptedWorkerSources));
5612
+ assertActive();
5583
5613
  index = await buildKnowledgeIndex(options.root);
5614
+ assertActive();
5584
5615
  readiness = readinessFor(options, index);
5585
5616
  let driverSources = [];
5586
5617
  let driverNotes;
@@ -5595,11 +5626,16 @@ async function runVerifiedResearchLoop(options) {
5595
5626
  readiness: requireReadiness(readiness, options),
5596
5627
  signal: options.signal
5597
5628
  });
5629
+ assertActive();
5598
5630
  driverNotes = driverContribution.notes;
5599
5631
  driverSources = await registerSources(options, driverContribution.sources ?? []);
5632
+ assertActive();
5600
5633
  await confirmRegisteredSources(options.driver, driverSources);
5634
+ assertActive();
5601
5635
  writtenPages.push(...await applyPages(options.root, driverContribution, driverSources));
5636
+ assertActive();
5602
5637
  index = await buildKnowledgeIndex(options.root);
5638
+ assertActive();
5603
5639
  readiness = readinessFor(options, index);
5604
5640
  }
5605
5641
  const driverComplete = options.driver.isComplete?.() ?? true;
@@ -5608,6 +5644,7 @@ async function runVerifiedResearchLoop(options) {
5608
5644
  if (ready || remainingGaps.length === 0 && driverComplete) steer = void 0;
5609
5645
  else {
5610
5646
  await options.driver.prepareFold?.();
5647
+ assertActive();
5611
5648
  steer = foldGaps(options.driver, remainingGaps);
5612
5649
  }
5613
5650
  const step = {
@@ -5640,10 +5677,14 @@ async function runVerifiedResearchLoop(options) {
5640
5677
  }
5641
5678
  };
5642
5679
  await options.driver.checkpoint?.();
5680
+ assertActive();
5643
5681
  await store.putEvent(step.event);
5682
+ assertActive();
5644
5683
  steps.push(step);
5645
5684
  await options.onRound?.(step);
5685
+ assertActive();
5646
5686
  }
5687
+ assertActive();
5647
5688
  return {
5648
5689
  root: options.root,
5649
5690
  goal: options.goal,
@@ -5685,8 +5726,10 @@ function isDuplicate(source, existingUris, accepted) {
5685
5726
  async function registerSources(options, sources) {
5686
5727
  const records = [];
5687
5728
  for (const candidate of sources) {
5729
+ options.signal?.throwIfAborted();
5688
5730
  const source = snapshotSourceTextInput(candidate);
5689
5731
  records.push(await addSourceText(options.root, source, options.sourceOptions));
5732
+ options.signal?.throwIfAborted();
5690
5733
  }
5691
5734
  return records;
5692
5735
  }
@@ -5859,7 +5902,7 @@ function thesisSynthesisMessages(input, kbText) {
5859
5902
  async function writeThesisPage(root, input, thesis) {
5860
5903
  return withKnowledgeMutation(root, async () => {
5861
5904
  const relativePath = `knowledge/thesis-${slugify(input.ticker)}.md`;
5862
- await writeFileDurableWithinRoot(root, relativePath, [
5905
+ const body = [
5863
5906
  "---",
5864
5907
  `title: Investment thesis — ${input.company} (${input.ticker})`,
5865
5908
  `ticker: ${input.ticker}`,
@@ -5870,7 +5913,8 @@ async function writeThesisPage(root, input, thesis) {
5870
5913
  "",
5871
5914
  thesis.trim(),
5872
5915
  ""
5873
- ].join("\n"), { encoding: "utf8" });
5916
+ ].join("\n");
5917
+ await writeFileDurableWithinRoot(root, relativePath, body, { encoding: "utf8" });
5874
5918
  return join(root, relativePath);
5875
5919
  });
5876
5920
  }
@@ -5942,7 +5986,7 @@ function buildKnowledgeBrief(visiblePages, question, options = {}) {
5942
5986
  origin: entry.origin
5943
5987
  });
5944
5988
  const originals = /* @__PURE__ */ new Map();
5945
- const ranked = searchKnowledgePages(visiblePages.map((entry) => {
5989
+ const pages = visiblePages.map((entry) => {
5946
5990
  const page = {
5947
5991
  ...entry.page,
5948
5992
  id: qualified(entry),
@@ -5959,7 +6003,8 @@ function buildKnowledgeBrief(visiblePages, question, options = {}) {
5959
6003
  };
5960
6004
  originals.set(page, entry);
5961
6005
  return page;
5962
- }), question, {
6006
+ });
6007
+ const ranked = searchKnowledgePages(pages, question, {
5963
6008
  limit,
5964
6009
  excludeInvalidated,
5965
6010
  ...options.tags === void 0 ? {} : { tags: options.tags },
@@ -6011,212 +6056,6 @@ function briefLine(hit) {
6011
6056
  return snippet === "" ? `- [${hit.citationId}] ${hit.page.title}` : `- [${hit.citationId}] ${hit.page.title} — ${snippet}`;
6012
6057
  }
6013
6058
  //#endregion
6014
- //#region src/knowledge-visibility.ts
6015
- const KNOWLEDGE_USE_RECEIPT_SCHEMA_VERSION = "2.0.0";
6016
- const KNOWLEDGE_RECEIPT_DIGEST_ALGORITHM = "rfc8785-sha256";
6017
- /** Stable content identity for one exact knowledge page. */
6018
- function knowledgePageDigest(page) {
6019
- validateKnowledgePage(page);
6020
- return canonicalCandidateDigest({
6021
- id: page.id,
6022
- path: page.path,
6023
- title: page.title,
6024
- text: page.text,
6025
- frontmatter: page.frontmatter,
6026
- sourceIds: [...page.sourceIds],
6027
- tags: [...page.tags],
6028
- outLinks: [...page.outLinks],
6029
- contradicts: [...page.contradicts ?? []],
6030
- invalidation: page.invalidation ?? null
6031
- });
6032
- }
6033
- /** Snapshot the exact ordered current/ancestor/shared page view. */
6034
- function createKnowledgeVisibilitySnapshot(visiblePages) {
6035
- if (!Array.isArray(visiblePages)) throw new TypeError("knowledge visibility must be an array");
6036
- const identities = /* @__PURE__ */ new Set();
6037
- const entries = visiblePages.map((entry, position) => {
6038
- if (!entry || typeof entry !== "object") throw new TypeError(`knowledge visibility[${position}] must be an originated page`);
6039
- const origin = validateOrigin(entry.origin, `knowledge visibility[${position}].origin`);
6040
- validateKnowledgePage(entry.page);
6041
- const identity = visibilityIdentity(origin, entry.page.path);
6042
- if (identities.has(identity)) throw new Error(`knowledge visibility repeats path '${entry.page.path}' at origin '${origin}'`);
6043
- identities.add(identity);
6044
- return Object.freeze({
6045
- position,
6046
- pageId: entry.page.id,
6047
- origin,
6048
- path: entry.page.path,
6049
- pageDigest: knowledgePageDigest(entry.page),
6050
- sourceIds: Object.freeze([...entry.page.sourceIds]),
6051
- invalidated: entry.page.invalidation !== void 0
6052
- });
6053
- });
6054
- const material = visibilityMaterial(entries);
6055
- const snapshot = Object.freeze({
6056
- ...material,
6057
- snapshotDigest: canonicalCandidateDigest(material),
6058
- entries: Object.freeze(entries)
6059
- });
6060
- visibilityIndexes.set(snapshot, indexVisibility(snapshot));
6061
- return snapshot;
6062
- }
6063
- /**
6064
- * Verify a snapshot's schema and canonical digest. The check runs once per
6065
- * snapshot object; later calls with the same object are free.
6066
- */
6067
- function verifyKnowledgeVisibilitySnapshot(snapshot) {
6068
- return visibilityIndexOf(snapshot).snapshot;
6069
- }
6070
- /** Canonical bytes of a verified snapshot, for durable storage. */
6071
- function encodeKnowledgeVisibilitySnapshot(snapshot) {
6072
- const verified = verifyKnowledgeVisibilitySnapshot(snapshot);
6073
- return canonicalCandidateBytes({
6074
- schemaVersion: verified.schemaVersion,
6075
- digestAlgorithm: verified.digestAlgorithm,
6076
- snapshotDigest: verified.snapshotDigest,
6077
- entries: visibilityMaterial(verified.entries).entries
6078
- });
6079
- }
6080
- /** Parse stored snapshot bytes and verify their schema and digest. */
6081
- function decodeKnowledgeVisibilitySnapshot(bytes) {
6082
- if (!(bytes instanceof Uint8Array)) throw new TypeError("knowledge visibility snapshot bytes must be a Uint8Array");
6083
- let parsed;
6084
- try {
6085
- parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
6086
- } catch (error) {
6087
- throw new Error(`knowledge visibility snapshot bytes are not JSON: ${error instanceof Error ? error.message : String(error)}`);
6088
- }
6089
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new TypeError("knowledge visibility snapshot must be an object");
6090
- const candidate = parsed;
6091
- if (!Array.isArray(candidate.entries)) throw new TypeError("knowledge visibility snapshot entries must be an array");
6092
- return verifyKnowledgeVisibilitySnapshot(Object.freeze({
6093
- schemaVersion: candidate.schemaVersion,
6094
- digestAlgorithm: candidate.digestAlgorithm,
6095
- snapshotDigest: digest(candidate.snapshotDigest, "knowledge visibility snapshotDigest"),
6096
- entries: Object.freeze(candidate.entries.map((entry, position) => {
6097
- if (!entry || typeof entry !== "object") throw new TypeError(`knowledge visibility[${position}] must be an object`);
6098
- const value = entry;
6099
- if (!Array.isArray(value.sourceIds)) throw new TypeError(`knowledge visibility[${position}].sourceIds must be an array`);
6100
- return Object.freeze({
6101
- position: value.position,
6102
- pageId: value.pageId,
6103
- origin: value.origin,
6104
- path: value.path,
6105
- pageDigest: value.pageDigest,
6106
- sourceIds: Object.freeze([...value.sourceIds]),
6107
- invalidated: value.invalidated
6108
- });
6109
- }))
6110
- }));
6111
- }
6112
- /** Locator for snapshot bytes stored at `uri`. */
6113
- function knowledgeVisibilityArtifactRef(input) {
6114
- if (!input || typeof input !== "object" || !(input.bytes instanceof Uint8Array)) throw new TypeError("knowledge visibility artifact bytes must be a Uint8Array");
6115
- if (input.bytes.byteLength === 0) throw new TypeError("knowledge visibility artifact bytes must not be empty");
6116
- return Object.freeze({
6117
- uri: nonEmpty$1(input.uri, "knowledge visibility artifact uri"),
6118
- digest: sha256Bytes(input.bytes),
6119
- byteLength: input.bytes.byteLength
6120
- });
6121
- }
6122
- const visibilityIndexes = /* @__PURE__ */ new WeakMap();
6123
- function visibilityIndexOf(snapshot) {
6124
- if (!snapshot || typeof snapshot !== "object") throw new TypeError("knowledge visibility snapshot is required");
6125
- const cached = visibilityIndexes.get(snapshot);
6126
- if (cached !== void 0) return cached;
6127
- assertSchemaVersion(snapshot.schemaVersion, "knowledge visibility");
6128
- if (snapshot.digestAlgorithm !== "rfc8785-sha256") throw new Error(`unsupported knowledge visibility digestAlgorithm '${snapshot.digestAlgorithm}'`);
6129
- if (!Array.isArray(snapshot.entries)) throw new TypeError("knowledge visibility snapshot entries must be an array");
6130
- if (canonicalCandidateDigest(visibilityMaterial(snapshot.entries)) !== digest(snapshot.snapshotDigest, "knowledge visibility snapshotDigest")) throw new Error("knowledge visibility snapshot digest mismatch");
6131
- const index = indexVisibility(snapshot);
6132
- visibilityIndexes.set(snapshot, index);
6133
- return index;
6134
- }
6135
- function indexVisibility(snapshot) {
6136
- const byIdentity = /* @__PURE__ */ new Map();
6137
- for (const entry of snapshot.entries) {
6138
- const identity = visibilityIdentity(entry.origin, entry.path);
6139
- if (byIdentity.has(identity)) throw new Error(`knowledge visibility repeats path '${entry.path}' at origin '${entry.origin}'`);
6140
- byIdentity.set(identity, entry);
6141
- }
6142
- return {
6143
- snapshot,
6144
- byIdentity
6145
- };
6146
- }
6147
- function visibilityIdentity(origin, path) {
6148
- return `${origin}\u0000${path}`;
6149
- }
6150
- function assertSchemaVersion(value, label) {
6151
- if (value === "2.0.0") return;
6152
- throw new Error(`unsupported ${label} schemaVersion '${String(value)}'${value === "1.0.0" ? "; 1.0.0 records embed the visibility snapshot and have no reader, create a 2.0.0 record from the snapshot" : ""}`);
6153
- }
6154
- function visibilityMaterial(entries) {
6155
- return {
6156
- schemaVersion: KNOWLEDGE_USE_RECEIPT_SCHEMA_VERSION,
6157
- digestAlgorithm: KNOWLEDGE_RECEIPT_DIGEST_ALGORITHM,
6158
- entries: entries.map((entry, position) => {
6159
- if (!entry || typeof entry !== "object") throw new TypeError(`knowledge visibility[${position}] must be an object`);
6160
- if (entry.position !== position) throw new Error(`knowledge visibility position mismatch: expected ${position}, observed ${entry.position}`);
6161
- if (!Array.isArray(entry.sourceIds)) throw new TypeError(`knowledge visibility[${position}].sourceIds must be an array`);
6162
- return {
6163
- position,
6164
- pageId: nonEmpty$1(entry.pageId, `knowledge visibility[${position}].pageId`),
6165
- origin: validateOrigin(entry.origin, `knowledge visibility[${position}].origin`),
6166
- path: nonEmpty$1(entry.path, `knowledge visibility[${position}].path`),
6167
- pageDigest: digest(entry.pageDigest, `knowledge visibility[${position}].pageDigest`),
6168
- sourceIds: entry.sourceIds.map((sourceId, sourceIndex) => nonEmpty$1(sourceId, `knowledge visibility[${position}].sourceIds[${sourceIndex}]`)),
6169
- invalidated: Boolean(entry.invalidated)
6170
- };
6171
- })
6172
- };
6173
- }
6174
- function normalizeVisibilityRef(input) {
6175
- if (!input || typeof input !== "object") throw new TypeError("knowledge retrieval visibility reference is required");
6176
- if (!Number.isSafeInteger(input.pageCount) || input.pageCount < 0) throw new TypeError("knowledge retrieval visibility pageCount must be a non-negative integer");
6177
- return Object.freeze({
6178
- snapshotDigest: digest(input.snapshotDigest, "knowledge retrieval visibility snapshotDigest"),
6179
- pageCount: input.pageCount,
6180
- ...input.artifact === void 0 ? {} : { artifact: normalizeVisibilityArtifact(input.artifact, "knowledge retrieval visibility artifact") }
6181
- });
6182
- }
6183
- function normalizeVisibilityArtifact(input, label) {
6184
- if (!input || typeof input !== "object") throw new TypeError(`${label} must be an object`);
6185
- if (!Number.isSafeInteger(input.byteLength) || input.byteLength < 1) throw new TypeError(`${label} byteLength must be a positive integer`);
6186
- return Object.freeze({
6187
- uri: nonEmpty$1(input.uri, `${label} uri`),
6188
- digest: digest(input.digest, `${label} digest`),
6189
- byteLength: input.byteLength
6190
- });
6191
- }
6192
- function validateKnowledgePage(page) {
6193
- if (!page || typeof page !== "object") throw new TypeError("knowledge page must be an object");
6194
- nonEmpty$1(page.id, "knowledge page id");
6195
- nonEmpty$1(page.path, "knowledge page path");
6196
- nonEmpty$1(page.title, "knowledge page title");
6197
- if (typeof page.text !== "string") throw new TypeError("knowledge page text must be a string");
6198
- if (!page.frontmatter || typeof page.frontmatter !== "object" || Array.isArray(page.frontmatter)) throw new TypeError("knowledge page frontmatter must be an object");
6199
- for (const [name, values] of [
6200
- ["sourceIds", page.sourceIds],
6201
- ["tags", page.tags],
6202
- ["outLinks", page.outLinks]
6203
- ]) if (!Array.isArray(values) || values.some((value) => typeof value !== "string")) throw new TypeError(`knowledge page ${name} must be a string array`);
6204
- }
6205
- function validateOrigin(value, label) {
6206
- if (value === "here" || value === "shared") return value;
6207
- if (typeof value === "string" && value.startsWith("inherited:") && value.slice(10).trim().length > 0) return value;
6208
- throw new TypeError(`${label} is invalid: ${String(value)}`);
6209
- }
6210
- function digest(value, label) {
6211
- const parsed = sha256DigestSchema.safeParse(value);
6212
- if (!parsed.success) throw new TypeError(`${label} must be a lowercase sha256 digest`);
6213
- return parsed.data;
6214
- }
6215
- function nonEmpty$1(value, label) {
6216
- if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${label} must be a non-empty string`);
6217
- return value.trim();
6218
- }
6219
- //#endregion
6220
6059
  //#region src/knowledge-use-receipts.ts
6221
6060
  /**
6222
6061
  * The visibility snapshot a receipt references could not be obtained, so the
@@ -6296,7 +6135,7 @@ function verifyKnowledgeRetrievalReceipt(receipt) {
6296
6135
  if (receipt.digestAlgorithm !== "rfc8785-sha256") throw new Error(`unsupported knowledge retrieval digestAlgorithm '${receipt.digestAlgorithm}'`);
6297
6136
  const visibility = normalizeVisibilityRef(receipt.visibility);
6298
6137
  validateReceiptResultShape(receipt.results, visibility.pageCount);
6299
- if (canonicalCandidateDigest(retrievalMaterial({
6138
+ const material = retrievalMaterial({
6300
6139
  createdAt: isoTimestamp(receipt.createdAt, "knowledge retrieval createdAt"),
6301
6140
  runId: nonEmpty$1(receipt.runId, "knowledge retrieval runId"),
6302
6141
  actorId: optionalText(receipt.actorId, "knowledge retrieval actorId"),
@@ -6308,7 +6147,8 @@ function verifyKnowledgeRetrievalReceipt(receipt) {
6308
6147
  results: receipt.results,
6309
6148
  evidenceRefs: normalizeEvidenceRefs(receipt.evidenceRefs),
6310
6149
  attributes: normalizeAttributes(receipt.attributes)
6311
- })) !== receipt.receiptDigest) throw new Error("knowledge retrieval receipt digest mismatch");
6150
+ });
6151
+ if (canonicalCandidateDigest(material) !== receipt.receiptDigest) throw new Error("knowledge retrieval receipt digest mismatch");
6312
6152
  return receipt;
6313
6153
  }
6314
6154
  /**
@@ -6400,7 +6240,7 @@ function verifyKnowledgeUseReceipt(receipt, retrieval) {
6400
6240
  if (receipt.retrievalReceiptDigest !== verifiedRetrieval.receiptDigest) throw new Error("knowledge use receipt references a different retrieval receipt");
6401
6241
  const selected = verifiedRetrieval.results.find((result) => result.rank === receipt.used.rank);
6402
6242
  if (!selected || selected.pageId !== receipt.used.pageId || selected.origin !== receipt.used.origin || selected.path !== receipt.used.path || selected.pageDigest !== receipt.used.pageDigest) throw new Error("knowledge use receipt selected result does not match the retrieval receipt");
6403
- if (canonicalCandidateDigest(useMaterial({
6243
+ const material = useMaterial({
6404
6244
  createdAt: isoTimestamp(receipt.createdAt, "knowledge use createdAt"),
6405
6245
  retrieval: verifiedRetrieval,
6406
6246
  used: receipt.used,
@@ -6408,7 +6248,8 @@ function verifyKnowledgeUseReceipt(receipt, retrieval) {
6408
6248
  consumer: normalizeConsumer(receipt.consumer),
6409
6249
  evidenceRefs: normalizeEvidenceRefs(receipt.evidenceRefs),
6410
6250
  attributes: normalizeAttributes(receipt.attributes)
6411
- })) !== receipt.receiptDigest) throw new Error("knowledge use receipt digest mismatch");
6251
+ });
6252
+ if (canonicalCandidateDigest(material) !== receipt.receiptDigest) throw new Error("knowledge use receipt digest mismatch");
6412
6253
  return receipt;
6413
6254
  }
6414
6255
  /** Record that one retrieval led to no use, with no rank to select. */
@@ -6435,14 +6276,15 @@ function verifyKnowledgeRetrievalDisposition(disposition, retrieval) {
6435
6276
  if (disposition.kind !== "knowledge-retrieval-disposition") throw new Error(`knowledge retrieval disposition kind must be 'knowledge-retrieval-disposition'`);
6436
6277
  if (disposition.digestAlgorithm !== "rfc8785-sha256") throw new Error(`unsupported knowledge retrieval disposition digestAlgorithm '${disposition.digestAlgorithm}'`);
6437
6278
  if (disposition.retrievalReceiptDigest !== verifiedRetrieval.receiptDigest) throw new Error("knowledge retrieval disposition references a different retrieval receipt");
6438
- if (canonicalCandidateDigest(dispositionMaterial({
6279
+ const material = dispositionMaterial({
6439
6280
  createdAt: isoTimestamp(disposition.createdAt, "knowledge retrieval disposition createdAt"),
6440
6281
  retrieval: verifiedRetrieval,
6441
6282
  relation: validateDispositionRelation(disposition.relation),
6442
6283
  consumer: normalizeConsumer(disposition.consumer),
6443
6284
  evidenceRefs: normalizeEvidenceRefs(disposition.evidenceRefs),
6444
6285
  attributes: normalizeAttributes(disposition.attributes)
6445
- })) !== disposition.receiptDigest) throw new Error("knowledge retrieval disposition digest mismatch");
6286
+ });
6287
+ if (canonicalCandidateDigest(material) !== disposition.receiptDigest) throw new Error("knowledge retrieval disposition digest mismatch");
6446
6288
  return disposition;
6447
6289
  }
6448
6290
  function assertReceiptJoinsVisibility(receipt, index) {
@@ -6557,7 +6399,8 @@ function validateReceiptResultShape(results, pageCount) {
6557
6399
  const seen = /* @__PURE__ */ new Set();
6558
6400
  results.forEach((result, position) => {
6559
6401
  if (!result || typeof result !== "object") throw new TypeError(`knowledge retrieval results[${position}] must be an object`);
6560
- const identity = visibilityIdentity(validateOrigin(result.origin, `knowledge retrieval results[${position}].origin`), nonEmpty$1(result.path, `knowledge retrieval results[${position}].path`));
6402
+ const origin = validateOrigin(result.origin, `knowledge retrieval results[${position}].origin`);
6403
+ const identity = visibilityIdentity(origin, nonEmpty$1(result.path, `knowledge retrieval results[${position}].path`));
6561
6404
  if (seen.has(identity)) throw new Error(`knowledge retrieval repeats visible page '${result.path}'`);
6562
6405
  seen.add(identity);
6563
6406
  nonEmpty$1(result.pageId, `knowledge retrieval results[${position}].pageId`);
@@ -6680,7 +6523,10 @@ const searchInput = z.object({
6680
6523
  kinds: z.array(z.string()).optional()
6681
6524
  });
6682
6525
  const readInput = z.object({ pageId: z.string().min(1) });
6683
- const recordInput = z.object({ proposal: z.string().min(1).describe("One or more complete FILE blocks. Each begins with ---FILE: <page-path>--- and ends with ---END FILE---, each on its own line. Put the page content between them. Every block must be valid; malformed or empty proposals write nothing and return an error.") });
6526
+ const recordInput = z.object({
6527
+ expectedPageDigests: z.record(z.string().min(1), sha256DigestSchema.nullable()).optional().describe("For each existing page, pass its path and pageDigest from knowledge_read. Null requires a new page. Missing entries permit new pages only."),
6528
+ proposal: z.string().min(1).describe("One or more complete FILE blocks. Each begins with ---FILE: <page-path>--- and ends with ---END FILE---, each on its own line. Put the page content between them. Every block must be valid; malformed or empty proposals write nothing and return an error.")
6529
+ });
6684
6530
  const resolveInput = z.object({ references: z.array(z.string().min(1)).min(1) });
6685
6531
  /**
6686
6532
  * The four tools an agent needs to use a knowledge store: search, read, record,
@@ -6736,6 +6582,7 @@ function createKnowledgeTools(options) {
6736
6582
  origin: resolution.resolved.origin,
6737
6583
  path: resolution.resolved.page.path,
6738
6584
  title: resolution.resolved.page.title,
6585
+ pageDigest: knowledgePageDigest(resolution.resolved.page),
6739
6586
  text: resolution.resolved.page.text
6740
6587
  },
6741
6588
  candidates: resolution.candidates.map((candidate) => ({
@@ -6745,13 +6592,16 @@ function createKnowledgeTools(options) {
6745
6592
  }))
6746
6593
  };
6747
6594
  }),
6748
- tool("knowledge_record", `Write pages into this run's store. Use complete blocks exactly like:\n---FILE: ${pagesDirectory}/example.md---\n# Example\nPage content.\n---END FILE---\nUse paths under ${pagesDirectory}/. Both delimiters must be on their own lines. Malformed, unsafe, or empty proposals are rejected before writing any pages.`, recordInput, async (input) => {
6595
+ tool("knowledge_record", `Write pages into this run's store. Use complete blocks exactly like:\n---FILE: ${pagesDirectory}/example.md---\n# Example\nPage content.\n---END FILE---\nUse paths under ${pagesDirectory}/. Both delimiters must be on their own lines. To update a page, first knowledge_read it and pass expectedPageDigests[path] = pageDigest. Stale edits and malformed, unsafe, or empty proposals are rejected before writing any pages.`, recordInput, async (input) => {
6749
6596
  const parsed = parseKnowledgeWriteBlocks(input.proposal, [`${pagesDirectory}/`]);
6750
6597
  if (parsed.blocks.length === 0 || parsed.warnings.length > 0) throw new Error(`knowledge_record rejected the proposal without writing any pages. Use ---FILE: ${pagesDirectory}/example.md--- followed by content and ---END FILE---, each delimiter on its own line. ${parsed.warnings.join(" ")}`);
6751
6598
  const intake = options.intake;
6752
6599
  return applyKnowledgeWriteBlocks(stores.storePath(runId), input.proposal, {
6753
6600
  ...pages,
6754
- retainHistory: options.retainHistory,
6601
+ actorId: options.actorId,
6602
+ runId,
6603
+ expectedPageDigests: input.expectedPageDigests ?? {},
6604
+ retainHistory: options.retainHistory ?? true,
6755
6605
  ...intake === void 0 ? {} : { intake: {
6756
6606
  ...intake,
6757
6607
  inheritedPages: await inheritedOf(stores, runId)
@@ -6856,7 +6706,8 @@ async function promoteRunScopedPages(stores, runId, options) {
6856
6706
  const existing = await loadKnowledgePages(sharedRoot, { pagesDirectory });
6857
6707
  assertNoIdentityConflict(travellers, existing);
6858
6708
  const promotedPaths = new Set(travellers.map((traveller) => traveller.entry.page.path));
6859
- assertKnowledgeCitationsResolved(originatedPages([...existing.filter((page) => !promotedPaths.has(page.path)), ...travellers.map((traveller) => traveller.entry.page)], "shared"), travellers.flatMap((traveller) => (traveller.entry.page.cites ?? []).map((persisted) => parseKnowledgeCitationReference(persisted))));
6709
+ const promotedView = originatedPages([...existing.filter((page) => !promotedPaths.has(page.path)), ...travellers.map((traveller) => traveller.entry.page)], "shared");
6710
+ assertKnowledgeCitationsResolved(promotedView, travellers.flatMap((traveller) => (traveller.entry.page.cites ?? []).map((persisted) => parseKnowledgeCitationReference(persisted))));
6860
6711
  await commitKnowledgeFileMutations({
6861
6712
  root: sharedRoot,
6862
6713
  transactionRoot: lock.transactionRoot,
@@ -6947,7 +6798,8 @@ function assertNoIdentityConflict(travellers, existing) {
6947
6798
  async function readPageBytes(stores, runId, entry, pagesDirectory) {
6948
6799
  const sourceRunId = sourceRunOf(entry.origin, runId);
6949
6800
  const content = (await readRegularFileWithinRoot(stores.storePath(sourceRunId), entry.page.path)).bytes.toString("utf8");
6950
- if (knowledgePageDigest(knowledgePageFromMarkdown(entry.page.path, content, pagesDirectory)) !== knowledgePageDigest(entry.page)) throw new KnowledgePromotionError("path-conflict", `source page "${entry.page.path}" in run "${sourceRunId}" changed after the promotion snapshot`);
6801
+ const page = knowledgePageFromMarkdown(entry.page.path, content, pagesDirectory);
6802
+ if (knowledgePageDigest(page) !== knowledgePageDigest(entry.page)) throw new KnowledgePromotionError("path-conflict", `source page "${entry.page.path}" in run "${sourceRunId}" changed after the promotion snapshot`);
6951
6803
  return content;
6952
6804
  }
6953
6805
  function sourceRunOf(origin, runId) {
@@ -7672,8 +7524,18 @@ function buildDriver(options, persistence, restored) {
7672
7524
  }
7673
7525
  if (goal !== nextGoal) throw new Error(`claim ledger '${persistence?.ledgerId ?? "in-memory"}' accumulated evidence for goal '${goal}' and cannot be reused for '${nextGoal}'`);
7674
7526
  }
7675
- function resolveRouter() {
7676
- return options.router ?? createTangleRouterClient(options.router_options);
7527
+ function resolveRouter(signal) {
7528
+ if (options.router) return options.router;
7529
+ const configuredSignal = options.router_options?.signal;
7530
+ const requestSignal = configuredSignal && signal ? AbortSignal.any([configuredSignal, signal]) : configuredSignal ?? signal;
7531
+ return createTangleRouterClient({
7532
+ ...options.router_options,
7533
+ signal: requestSignal
7534
+ });
7535
+ }
7536
+ function throwIfRouterAborted(signal) {
7537
+ signal?.throwIfAborted();
7538
+ if (!options.router) options.router_options?.signal?.throwIfAborted();
7677
7539
  }
7678
7540
  /** Record a claim from a source, growing its independent-source support. */
7679
7541
  function recordClaim(extracted, sourceUri, round) {
@@ -7835,12 +7697,14 @@ function buildDriver(options, persistence, restored) {
7835
7697
  * because such a source cannot drive the research and pollutes the KB.
7836
7698
  */
7837
7699
  async verifySource(source, ctx) {
7700
+ throwIfRouterAborted(ctx.signal);
7838
7701
  const sourceSnapshot = snapshotSourceTextInput(source);
7839
7702
  const goalSnapshot = ctx.goal;
7840
7703
  const roundSnapshot = ctx.round;
7841
7704
  const sourceVersion = sourceVersionOfProposal(sourceSnapshot);
7842
7705
  bindGoal(goalSnapshot);
7843
- const extracted = await extractClaims(sourceSnapshot, goalSnapshot);
7706
+ const extracted = await extractClaims(sourceSnapshot, goalSnapshot, ctx.signal);
7707
+ throwIfRouterAborted(ctx.signal);
7844
7708
  if (extracted.length === 0) return {
7845
7709
  accept: false,
7846
7710
  reason: "no extractable claim: source yields nothing to drive the research deeper"
@@ -7855,6 +7719,7 @@ function buildDriver(options, persistence, restored) {
7855
7719
  if (registered && researchSourceVersionKey(registered) === researchSourceVersionKey(sourceVersion)) markAddressed(materializeEvidenceFor(/* @__PURE__ */ new Set([researchSourceVersionKey(sourceVersion)])));
7856
7720
  }
7857
7721
  await persist();
7722
+ throwIfRouterAborted(ctx.signal);
7858
7723
  return { accept: true };
7859
7724
  },
7860
7725
  /**
@@ -7898,8 +7763,9 @@ function buildDriver(options, persistence, restored) {
7898
7763
  commitSources,
7899
7764
  toLedger
7900
7765
  };
7901
- async function extractClaims(source, goal) {
7902
- const fromLlm = await extractClaimsWithLlm(source, goal, claimsForExtraction());
7766
+ async function extractClaims(source, goal, signal) {
7767
+ const fromLlm = await extractClaimsWithLlm(source, goal, claimsForExtraction(), signal);
7768
+ throwIfRouterAborted(signal);
7903
7769
  if (fromLlm.length > 0) return fromLlm.slice(0, maxClaimsPerSource);
7904
7770
  if (deterministicFallback) return deterministicClaims(source).slice(0, maxClaimsPerSource);
7905
7771
  return [];
@@ -7920,11 +7786,13 @@ function buildDriver(options, persistence, restored) {
7920
7786
  }
7921
7787
  return [...known.values()];
7922
7788
  }
7923
- async function extractClaimsWithLlm(source, goal, ledger) {
7789
+ async function extractClaimsWithLlm(source, goal, ledger, signal) {
7924
7790
  let router;
7925
7791
  try {
7926
- router = resolveRouter();
7927
- } catch {
7792
+ router = resolveRouter(signal);
7793
+ } catch (error) {
7794
+ throwIfRouterAborted(signal);
7795
+ if (error?.name === "AbortError") throw error;
7928
7796
  return [];
7929
7797
  }
7930
7798
  const excerpt = source.text.slice(0, 1800);
@@ -7946,9 +7814,12 @@ function buildDriver(options, persistence, restored) {
7946
7814
  role: "user",
7947
7815
  content: user
7948
7816
  }], 1200);
7949
- } catch {
7817
+ } catch (error) {
7818
+ throwIfRouterAborted(signal);
7819
+ if (error?.name === "AbortError") throw error;
7950
7820
  return [];
7951
7821
  }
7822
+ throwIfRouterAborted(signal);
7952
7823
  return parseExtractedClaims(raw, ledger);
7953
7824
  }
7954
7825
  /**