@vostride/agent-qa-agent-local 0.0.0-stage → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +105 -0
- package/NOTICE.md +11 -0
- package/dist/index.cjs +3801 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +2087 -0
- package/dist/index.d.ts +2087 -0
- package/dist/index.js +3875 -0
- package/dist/index.js.map +1 -0
- package/package.json +66 -4
- package/README.md +0 -3
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,3801 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ALLOW_PRODUCTION_OPT_IN: () => import_agent_qa_schema.ALLOW_PRODUCTION_OPT_IN,
|
|
34
|
+
BRIEF_CEILING: () => BRIEF_CEILING,
|
|
35
|
+
BRIEF_FRACTION: () => BRIEF_FRACTION,
|
|
36
|
+
BuildOrdinalReservationError: () => import_agent_qa_runner_local4.BuildOrdinalReservationError,
|
|
37
|
+
CAUTION_FRACTION: () => CAUTION_FRACTION,
|
|
38
|
+
CHAT_PLANNER_CALLER: () => CHAT_PLANNER_CALLER,
|
|
39
|
+
CHAT_RESOLVER_CALLER: () => CHAT_RESOLVER_CALLER,
|
|
40
|
+
CHAT_SESSIONS_SUBDIR: () => CHAT_SESSIONS_SUBDIR,
|
|
41
|
+
ChatSessionBusyError: () => ChatSessionBusyError,
|
|
42
|
+
ChatSessionHeaderError: () => ChatSessionHeaderError,
|
|
43
|
+
DIGEST_JOB_ID_SOURCE: () => DIGEST_JOB_ID_SOURCE,
|
|
44
|
+
DRAIN_BUDGET_MS: () => import_agent_qa_knowledge_local.DRAIN_BUDGET_MS,
|
|
45
|
+
EMBEDDER_WORKER_REGISTRATION_FILE: () => import_agent_qa_knowledge_local.EMBEDDER_WORKER_REGISTRATION_FILE,
|
|
46
|
+
EMBEDDER_WORKER_START_TIMEOUT_MS: () => import_agent_qa_knowledge_local.EMBEDDER_WORKER_START_TIMEOUT_MS,
|
|
47
|
+
EMBEDDER_WORKER_TERM_GRACE_MS: () => import_agent_qa_knowledge_local.EMBEDDER_WORKER_TERM_GRACE_MS,
|
|
48
|
+
ENTITY_INDEX_SWEEP_INTERVAL_MS: () => import_agent_qa_knowledge_local.ENTITY_INDEX_SWEEP_INTERVAL_MS,
|
|
49
|
+
EmbedderWorkerTimeoutError: () => import_agent_qa_knowledge_local.EmbedderWorkerTimeoutError,
|
|
50
|
+
GLOBAL_RECORD_CAP: () => GLOBAL_RECORD_CAP,
|
|
51
|
+
InvalidChatSessionIdError: () => InvalidChatSessionIdError,
|
|
52
|
+
JOB_RECOVERY_MAX_RESUMED_JOBS: () => JOB_RECOVERY_MAX_RESUMED_JOBS,
|
|
53
|
+
LOCAL_CURATION_CAPABILITIES: () => LOCAL_CURATION_CAPABILITIES,
|
|
54
|
+
LOCAL_DAEMON_CAPABILITIES: () => LOCAL_DAEMON_CAPABILITIES,
|
|
55
|
+
LOCAL_KNOWLEDGE_PROBES: () => LOCAL_KNOWLEDGE_PROBES,
|
|
56
|
+
LocalAgentClosedError: () => LocalAgentClosedError,
|
|
57
|
+
MIN_BRIEF: () => MIN_BRIEF,
|
|
58
|
+
MIN_CAUTION: () => MIN_CAUTION,
|
|
59
|
+
MIN_MEMORY_TOOL_CONTEXT_WINDOW: () => MIN_MEMORY_TOOL_CONTEXT_WINDOW,
|
|
60
|
+
QUARANTINE_CLASS_DISPOSITIONS: () => import_reader3.QUARANTINE_CLASS_DISPOSITIONS,
|
|
61
|
+
QUARANTINE_FLOOR_ELIGIBILITY: () => import_reader3.QUARANTINE_FLOOR_ELIGIBILITY,
|
|
62
|
+
READ_BODY_FRACTION: () => READ_BODY_FRACTION,
|
|
63
|
+
REFUSED_RUN_REQUEST_FIELDS: () => REFUSED_RUN_REQUEST_FIELDS,
|
|
64
|
+
RunDeniedError: () => RunDeniedError,
|
|
65
|
+
RunIdAlreadyInUseError: () => RunIdAlreadyInUseError,
|
|
66
|
+
RunIdNotMintedError: () => RunIdNotMintedError,
|
|
67
|
+
RunProducedNoEventsError: () => RunProducedNoEventsError,
|
|
68
|
+
RunRequestFieldRefusedError: () => RunRequestFieldRefusedError,
|
|
69
|
+
SUITE_LIFECYCLE_LANE: () => SUITE_LIFECYCLE_LANE,
|
|
70
|
+
SYSTEM_CLOCK: () => SYSTEM_CLOCK,
|
|
71
|
+
SuiteBoundaryWhileLiveError: () => SuiteBoundaryWhileLiveError,
|
|
72
|
+
UNCLAIMABLE_BY_PROSE_POSTURE: () => UNCLAIMABLE_BY_PROSE_POSTURE,
|
|
73
|
+
UnknownChatSessionError: () => UnknownChatSessionError,
|
|
74
|
+
WEAK_MODEL_CAP_SCALE: () => WEAK_MODEL_CAP_SCALE,
|
|
75
|
+
applyKnowledgeChange: () => applyKnowledgeChange,
|
|
76
|
+
buildMemberRunConfig: () => buildMemberRunConfig,
|
|
77
|
+
buildProductionRefusal: () => import_agent_qa_schema.buildProductionRefusal,
|
|
78
|
+
buildRunPorts: () => buildRunPorts,
|
|
79
|
+
buildScreenshotCompressor: () => buildScreenshotCompressor,
|
|
80
|
+
buildSuiteMemberExecutor: () => buildSuiteMemberExecutor,
|
|
81
|
+
composeJobRecovery: () => composeJobRecovery,
|
|
82
|
+
composeLocalAgent: () => composeLocalAgent,
|
|
83
|
+
computeKnowledgeBudgets: () => computeKnowledgeBudgets,
|
|
84
|
+
computeRunConfigDigests: () => computeRunConfigDigests,
|
|
85
|
+
constructKnowledgeReader: () => constructKnowledgeReader,
|
|
86
|
+
createEvidenceBlobReader: () => createEvidenceBlobReader,
|
|
87
|
+
createFrozenKnowledgeReader: () => createFrozenKnowledgeReader,
|
|
88
|
+
createJsonlSessionLog: () => createJsonlSessionLog,
|
|
89
|
+
createKnowledgeReader: () => createKnowledgeReader,
|
|
90
|
+
createKnowledgeReview: () => createKnowledgeReview,
|
|
91
|
+
createLocalRunInvocation: () => createLocalRunInvocation,
|
|
92
|
+
createMemorySessionLog: () => createMemorySessionLog,
|
|
93
|
+
createRunInvocationCore: () => createRunInvocationCore,
|
|
94
|
+
createRunsReader: () => createRunsReader,
|
|
95
|
+
createSuiteLifecycle: () => createSuiteLifecycle,
|
|
96
|
+
latestBuildOrdinal: () => import_agent_qa_runner_local4.latestBuildOrdinal,
|
|
97
|
+
listSessions: () => listSessions,
|
|
98
|
+
lookupEvidenceBlob: () => lookupEvidenceBlob,
|
|
99
|
+
mapVerdictToExitCode: () => import_agent_qa_runner3.mapVerdictToExitCode,
|
|
100
|
+
memoryToolGate: () => memoryToolGate,
|
|
101
|
+
mintChatSessionId: () => mintChatSessionId,
|
|
102
|
+
openRunKnowledgeComposition: () => openRunKnowledgeComposition,
|
|
103
|
+
readEvidenceBlobBytes: () => readEvidenceBlobBytes,
|
|
104
|
+
refuseProductionWithoutOptIn: () => refuseProductionWithoutOptIn,
|
|
105
|
+
refuseUnavailableRunId: () => refuseUnavailableRunId,
|
|
106
|
+
registerAdapterLiveStream: () => registerAdapterLiveStream,
|
|
107
|
+
reserveBuildOrdinalForProducts: () => import_agent_qa_runner_local4.reserveBuildOrdinalForProducts,
|
|
108
|
+
resolveBuildTag: () => import_agent_qa_runner_local5.resolveBuildTag,
|
|
109
|
+
resolveBuildTagSetting: () => resolveBuildTagSetting,
|
|
110
|
+
resolveHostRetrievalRung: () => resolveHostRetrievalRung,
|
|
111
|
+
resolveRunBuildOrdinal: () => resolveRunBuildOrdinal,
|
|
112
|
+
resolveRunExecutionContext: () => resolveRunExecutionContext,
|
|
113
|
+
resolveRunQuarantineSnapshot: () => resolveRunQuarantineSnapshot,
|
|
114
|
+
runSafetyScope: () => import_agent_qa_safety_local2.runSafetyScope,
|
|
115
|
+
scaledRecordCap: () => scaledRecordCap,
|
|
116
|
+
statEvidenceBlob: () => statEvidenceBlob,
|
|
117
|
+
sweepEntityIndexProjection: () => sweepEntityIndexProjection,
|
|
118
|
+
toMemberTest: () => toMemberTest
|
|
119
|
+
});
|
|
120
|
+
module.exports = __toCommonJS(index_exports);
|
|
121
|
+
|
|
122
|
+
// src/composition/run-composition.ts
|
|
123
|
+
var import_node_crypto = require("node:crypto");
|
|
124
|
+
var import_node_path = require("node:path");
|
|
125
|
+
var import_agent_qa_safety_local = require("@vostride/agent-qa-safety-local");
|
|
126
|
+
var import_agent_qa_model = require("@vostride/agent-qa-model");
|
|
127
|
+
var import_agent_qa_runner_local = require("@vostride/agent-qa-runner-local");
|
|
128
|
+
var import_agent_qa_runner = require("@vostride/agent-qa-runner");
|
|
129
|
+
|
|
130
|
+
// src/composition/knowledge-budgets.ts
|
|
131
|
+
var BRIEF_FRACTION = 0.05;
|
|
132
|
+
var CAUTION_FRACTION = 0.01;
|
|
133
|
+
var MIN_BRIEF = 256;
|
|
134
|
+
var MIN_CAUTION = 64;
|
|
135
|
+
var BRIEF_CEILING = 2048;
|
|
136
|
+
var READ_BODY_FRACTION = 0.1;
|
|
137
|
+
var GLOBAL_RECORD_CAP = 24;
|
|
138
|
+
var WEAK_MODEL_CAP_SCALE = 0.5;
|
|
139
|
+
var MIN_MEMORY_TOOL_CONTEXT_WINDOW = 16384;
|
|
140
|
+
function floorFinite(value) {
|
|
141
|
+
if (Number.isNaN(value)) return 0;
|
|
142
|
+
if (value === Number.POSITIVE_INFINITY) return Number.MAX_SAFE_INTEGER;
|
|
143
|
+
if (value === Number.NEGATIVE_INFINITY) return Number.MIN_SAFE_INTEGER;
|
|
144
|
+
return Math.floor(value);
|
|
145
|
+
}
|
|
146
|
+
function clamp(value, minimum, maximum) {
|
|
147
|
+
return Math.min(maximum, Math.max(minimum, value));
|
|
148
|
+
}
|
|
149
|
+
function computeKnowledgeBudgets(input) {
|
|
150
|
+
const usableContext = input.contextWindow - input.reservedOutput - input.qaRulesTokens - input.workingContextTokens;
|
|
151
|
+
const brief = clamp(
|
|
152
|
+
floorFinite(usableContext * BRIEF_FRACTION),
|
|
153
|
+
MIN_BRIEF,
|
|
154
|
+
BRIEF_CEILING
|
|
155
|
+
);
|
|
156
|
+
const caution = Math.max(MIN_CAUTION, floorFinite(usableContext * CAUTION_FRACTION));
|
|
157
|
+
const readBody = Math.max(MIN_BRIEF, floorFinite(usableContext * READ_BODY_FRACTION));
|
|
158
|
+
const searchResults = brief;
|
|
159
|
+
return { brief, searchResults, caution, readBody, recordCap: GLOBAL_RECORD_CAP };
|
|
160
|
+
}
|
|
161
|
+
function memoryToolGate(capabilities) {
|
|
162
|
+
return capabilities.toolCalling && capabilities.vision && capabilities.contextWindow >= MIN_MEMORY_TOOL_CONTEXT_WINDOW;
|
|
163
|
+
}
|
|
164
|
+
function scaledRecordCap(capabilities) {
|
|
165
|
+
if (memoryToolGate(capabilities)) return GLOBAL_RECORD_CAP;
|
|
166
|
+
return Math.max(1, Math.floor(GLOBAL_RECORD_CAP * WEAK_MODEL_CAP_SCALE));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// src/composition/knowledge-reader.ts
|
|
170
|
+
var import_reader = require("@vostride/agent-qa-knowledge-local/reader");
|
|
171
|
+
function createFrozenKnowledgeReader(reader) {
|
|
172
|
+
const briefs = /* @__PURE__ */ new Map();
|
|
173
|
+
const scopeKey = (scope) => JSON.stringify([scope.tier, scope.key]);
|
|
174
|
+
return {
|
|
175
|
+
getRunBrief(scope, budget) {
|
|
176
|
+
const key = scopeKey(scope);
|
|
177
|
+
const existing = briefs.get(key);
|
|
178
|
+
if (existing !== void 0) return existing;
|
|
179
|
+
const brief = reader.getRunBrief(scope, budget);
|
|
180
|
+
briefs.set(key, brief);
|
|
181
|
+
return brief;
|
|
182
|
+
},
|
|
183
|
+
search: (query, options) => reader.search(query, options),
|
|
184
|
+
read: (id, options) => reader.read(id, options),
|
|
185
|
+
getCautions: (anchors, budget) => reader.getCautions(anchors, budget),
|
|
186
|
+
getProcedure: (intent, screenState, availability) => reader.getProcedure(intent, screenState, availability),
|
|
187
|
+
emitCanaryOutcome: (payload) => reader.emitCanaryOutcome(payload)
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function constructKnowledgeReader(input) {
|
|
191
|
+
return createFrozenKnowledgeReader((0, import_reader.openKnowledgeReader)({
|
|
192
|
+
...input.resolveTrustPriors === void 0 ? {} : { priors: input.resolveTrustPriors() },
|
|
193
|
+
knowledgeRoot: input.knowledgeRoot,
|
|
194
|
+
...input.product === void 0 ? {} : { product: input.product },
|
|
195
|
+
scopeDefaults: {
|
|
196
|
+
asOfBuild: input.asOfBuild,
|
|
197
|
+
...input.parentScopes === void 0 ? {} : { parentScopes: input.parentScopes }
|
|
198
|
+
},
|
|
199
|
+
budgets: { recordCap: input.recordCap }
|
|
200
|
+
}));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// src/composition/run-composition.ts
|
|
204
|
+
function openRunKnowledgeComposition(input) {
|
|
205
|
+
const ceilings = input.accounting.getCeilings();
|
|
206
|
+
const qaRulesTokens = input.accounting.estimate([
|
|
207
|
+
{ role: "system", content: input.qaRulesText }
|
|
208
|
+
]);
|
|
209
|
+
const workingContextTokens = input.accounting.estimate([
|
|
210
|
+
{ role: "user", content: input.workingContextText }
|
|
211
|
+
]);
|
|
212
|
+
const computed = computeKnowledgeBudgets({
|
|
213
|
+
contextWindow: ceilings.contextWindow,
|
|
214
|
+
reservedOutput: ceilings.maxOutput,
|
|
215
|
+
qaRulesTokens,
|
|
216
|
+
workingContextTokens
|
|
217
|
+
});
|
|
218
|
+
const budgets = {
|
|
219
|
+
...computed,
|
|
220
|
+
recordCap: scaledRecordCap(input.capabilities)
|
|
221
|
+
};
|
|
222
|
+
const reader = constructKnowledgeReader({
|
|
223
|
+
...input.resolveTrustPriors === void 0 ? {} : { resolveTrustPriors: input.resolveTrustPriors },
|
|
224
|
+
knowledgeRoot: input.knowledgeRoot,
|
|
225
|
+
...input.product === void 0 ? {} : { product: input.product },
|
|
226
|
+
...input.parentScopes === void 0 ? {} : { parentScopes: input.parentScopes },
|
|
227
|
+
asOfBuild: input.asOfBuild,
|
|
228
|
+
recordCap: budgets.recordCap
|
|
229
|
+
});
|
|
230
|
+
const retrievalRung = input.resolveRetrievalRung({
|
|
231
|
+
knowledgeRoot: input.knowledgeRoot,
|
|
232
|
+
...input.agentQaRoot === void 0 ? {} : { agentQaRoot: input.agentQaRoot }
|
|
233
|
+
});
|
|
234
|
+
retrievalRung.catch(() => void 0);
|
|
235
|
+
const channel2Enabled = memoryToolGate(input.capabilities);
|
|
236
|
+
const recordLedger = (0, import_agent_qa_runner.createRecordLedger)({
|
|
237
|
+
cap: budgets.recordCap,
|
|
238
|
+
budget: budgets.brief + budgets.caution + (channel2Enabled ? budgets.searchResults : 0),
|
|
239
|
+
seedRecords: []
|
|
240
|
+
});
|
|
241
|
+
const memory = channel2Enabled ? (0, import_agent_qa_runner.createMemoryTools)({
|
|
242
|
+
reader,
|
|
243
|
+
scope: input.scope,
|
|
244
|
+
asOfBuild: input.asOfBuild,
|
|
245
|
+
searchResultBudget: budgets.searchResults,
|
|
246
|
+
readBodyBudget: budgets.readBody,
|
|
247
|
+
recordLedger,
|
|
248
|
+
estimateTokens: (text) => input.accounting.estimate([
|
|
249
|
+
{ role: "user", content: text }
|
|
250
|
+
])
|
|
251
|
+
}) : void 0;
|
|
252
|
+
return {
|
|
253
|
+
reader,
|
|
254
|
+
budgets,
|
|
255
|
+
retrievalRung,
|
|
256
|
+
recordLedger,
|
|
257
|
+
nonActionTools: memory?.tools,
|
|
258
|
+
beginMemoryTurn: memory?.beginTurn,
|
|
259
|
+
drainMemoryReadTraces: memory?.drainReadTraces
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
function digest16(input) {
|
|
263
|
+
return (0, import_node_crypto.createHash)("sha256").update(input, "utf8").digest("hex").slice(0, 16);
|
|
264
|
+
}
|
|
265
|
+
function computeRunConfigDigests(input) {
|
|
266
|
+
const perMember = input.members.map((m) => ({
|
|
267
|
+
suiteConfigDigest: digest16(input.configContent + "\n" + (m.suiteFileContent ?? "")),
|
|
268
|
+
testConfigDigest: digest16(m.testFileContent)
|
|
269
|
+
}));
|
|
270
|
+
if (perMember.length === 1) {
|
|
271
|
+
return { runLevel: { ...perMember[0] }, perMember };
|
|
272
|
+
}
|
|
273
|
+
const distinctSuiteDigests = [];
|
|
274
|
+
const seen = /* @__PURE__ */ new Set();
|
|
275
|
+
for (const pair of perMember) {
|
|
276
|
+
if (!seen.has(pair.suiteConfigDigest)) {
|
|
277
|
+
seen.add(pair.suiteConfigDigest);
|
|
278
|
+
distinctSuiteDigests.push(pair.suiteConfigDigest);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const runLevel = {
|
|
282
|
+
suiteConfigDigest: digest16(distinctSuiteDigests.join("\n")),
|
|
283
|
+
testConfigDigest: digest16(perMember.map((p) => p.testConfigDigest).join("\n"))
|
|
284
|
+
};
|
|
285
|
+
return { runLevel, perMember };
|
|
286
|
+
}
|
|
287
|
+
function resolveRunExecutionContext(override) {
|
|
288
|
+
return (0, import_agent_qa_safety_local.detectExecutionContext)(override);
|
|
289
|
+
}
|
|
290
|
+
function buildRunPorts(input) {
|
|
291
|
+
const root = (0, import_node_path.resolve)(input.configDir, ".agent-qa");
|
|
292
|
+
return {
|
|
293
|
+
eventLog: new import_agent_qa_runner_local.FsEventLog({ root, runId: input.runId }),
|
|
294
|
+
handoffStore: new import_agent_qa_runner_local.FsHandoffStore({ root, runId: input.runId }),
|
|
295
|
+
runArtifactStore: new import_agent_qa_runner_local.FsRunArtifactStore({ root, runId: input.runId }),
|
|
296
|
+
evidenceArtifacts: new import_agent_qa_runner_local.FsEvidenceArtifactStore({ root, runId: input.runId }),
|
|
297
|
+
// A NAMED, observable, non-durable destination: greppable by code, visible to a test through
|
|
298
|
+
// `process.once('warning', …)`, and no `console.` call in a package that makes none.
|
|
299
|
+
onSubscriberFault: (code, message) => {
|
|
300
|
+
process.emitWarning(message, { code });
|
|
301
|
+
}
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
function buildScreenshotCompressor(logger) {
|
|
305
|
+
return new import_agent_qa_runner_local.SharpScreenshotCompressor(logger);
|
|
306
|
+
}
|
|
307
|
+
function resolveBuildTagSetting(input) {
|
|
308
|
+
for (const value of [input.flag, input.env, input.use?.buildTag]) {
|
|
309
|
+
const trimmed = value?.trim();
|
|
310
|
+
if (trimmed) return trimmed;
|
|
311
|
+
}
|
|
312
|
+
return void 0;
|
|
313
|
+
}
|
|
314
|
+
async function resolveRunBuildOrdinal(input) {
|
|
315
|
+
const root = (0, import_node_path.resolve)(input.configDir, ".agent-qa");
|
|
316
|
+
const ordinal = await (0, import_agent_qa_runner_local.nextBuildOrdinal)({ root, product: input.product });
|
|
317
|
+
if (input.buildTag !== void 0) {
|
|
318
|
+
await (0, import_agent_qa_runner_local.recordBuildTag)({
|
|
319
|
+
root,
|
|
320
|
+
product: input.product,
|
|
321
|
+
tag: input.buildTag,
|
|
322
|
+
ordinal
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
return ordinal;
|
|
326
|
+
}
|
|
327
|
+
var SUITE_LIFECYCLE_LANE = "suite-lifecycle";
|
|
328
|
+
var SuiteBoundaryWhileLiveError = class extends Error {
|
|
329
|
+
constructor(activeSuiteKey, requestedSuiteKey, liveMembers) {
|
|
330
|
+
super(
|
|
331
|
+
`Cannot enter suite "${requestedSuiteKey}": suite "${activeSuiteKey}" still has ${liveMembers} live member(s). Suite boundaries are serialized.`
|
|
332
|
+
);
|
|
333
|
+
this.activeSuiteKey = activeSuiteKey;
|
|
334
|
+
this.requestedSuiteKey = requestedSuiteKey;
|
|
335
|
+
this.liveMembers = liveMembers;
|
|
336
|
+
this.name = "SuiteBoundaryWhileLiveError";
|
|
337
|
+
}
|
|
338
|
+
};
|
|
339
|
+
function createSuiteLifecycle() {
|
|
340
|
+
let activeKey;
|
|
341
|
+
let activeSetup;
|
|
342
|
+
let activeTeardown;
|
|
343
|
+
let liveMembers = 0;
|
|
344
|
+
const tearDownActive = async () => {
|
|
345
|
+
const teardown = activeTeardown;
|
|
346
|
+
activeKey = void 0;
|
|
347
|
+
activeSetup = void 0;
|
|
348
|
+
activeTeardown = void 0;
|
|
349
|
+
liveMembers = 0;
|
|
350
|
+
if (teardown) await teardown();
|
|
351
|
+
};
|
|
352
|
+
return {
|
|
353
|
+
async ensureReady(suiteKey, hooks) {
|
|
354
|
+
if (activeKey !== void 0 && activeKey !== suiteKey) {
|
|
355
|
+
if (liveMembers > 0) {
|
|
356
|
+
throw new SuiteBoundaryWhileLiveError(activeKey, suiteKey, liveMembers);
|
|
357
|
+
}
|
|
358
|
+
await tearDownActive();
|
|
359
|
+
}
|
|
360
|
+
if (activeKey !== suiteKey) {
|
|
361
|
+
activeKey = suiteKey;
|
|
362
|
+
activeTeardown = hooks.teardown;
|
|
363
|
+
activeSetup = (async () => {
|
|
364
|
+
try {
|
|
365
|
+
await hooks.setup();
|
|
366
|
+
} catch (error) {
|
|
367
|
+
activeTeardown = void 0;
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
})();
|
|
371
|
+
}
|
|
372
|
+
const setup = activeSetup;
|
|
373
|
+
liveMembers += 1;
|
|
374
|
+
let released = false;
|
|
375
|
+
const lease = {
|
|
376
|
+
release() {
|
|
377
|
+
if (released) return;
|
|
378
|
+
released = true;
|
|
379
|
+
liveMembers -= 1;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
try {
|
|
383
|
+
await setup;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
lease.release();
|
|
386
|
+
throw error;
|
|
387
|
+
}
|
|
388
|
+
return lease;
|
|
389
|
+
},
|
|
390
|
+
async finalize() {
|
|
391
|
+
await tearDownActive();
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
function registerAdapterLiveStream(ctx, adapter) {
|
|
396
|
+
const registration = (0, import_agent_qa_runner.liveStreamFor)(adapter);
|
|
397
|
+
ctx.registerLiveStream?.(registration?.capability, registration?.fallback);
|
|
398
|
+
}
|
|
399
|
+
function buildSuiteMemberExecutor(input) {
|
|
400
|
+
return async (member, ctx) => {
|
|
401
|
+
const binding = input.bindings[ctx.testIndex];
|
|
402
|
+
if (!binding) {
|
|
403
|
+
throw new Error(`No suite-member binding for testIndex ${ctx.testIndex} (testId ${member.testId})`);
|
|
404
|
+
}
|
|
405
|
+
const lease = await input.lifecycle.ensureReady(binding.suiteKey, binding.hooks);
|
|
406
|
+
try {
|
|
407
|
+
registerAdapterLiveStream(ctx, binding.adapter);
|
|
408
|
+
return await binding.run(ctx);
|
|
409
|
+
} finally {
|
|
410
|
+
lease.release();
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function runDirFor(input) {
|
|
415
|
+
return (0, import_node_path.resolve)(input.configDir, ".agent-qa", import_agent_qa_runner_local.RUNS_SUBDIR, (0, import_agent_qa_runner_local.slugBranch)(input.runId));
|
|
416
|
+
}
|
|
417
|
+
async function captureFailureScreenshotFromAdapter(adapter) {
|
|
418
|
+
try {
|
|
419
|
+
const shot = adapter.screenshot;
|
|
420
|
+
if (typeof shot !== "function") return void 0;
|
|
421
|
+
return await shot.call(adapter);
|
|
422
|
+
} catch {
|
|
423
|
+
return void 0;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
function buildRunJsHandler(adapter) {
|
|
427
|
+
return async (code, ctx) => {
|
|
428
|
+
if (adapter.platform !== "web") {
|
|
429
|
+
return { value: "[runJS error: runJS is only supported on web platform]", error: "runJS is only supported on web platform" };
|
|
430
|
+
}
|
|
431
|
+
const page = "getPage" in adapter ? adapter.getPage() : void 0;
|
|
432
|
+
if (!page) {
|
|
433
|
+
return { value: "[runJS error: no browser page available]", error: "no browser page available" };
|
|
434
|
+
}
|
|
435
|
+
void ctx;
|
|
436
|
+
const raw = await page.evaluate(code);
|
|
437
|
+
return { value: (0, import_agent_qa_runner.coerceRunJSResult)(raw) };
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
function buildAppiumScriptHandler(adapter, secretStore) {
|
|
441
|
+
return async (action) => adapter.execute((0, import_agent_qa_runner.resolveSecretTemplatesInValue)(action, secretStore));
|
|
442
|
+
}
|
|
443
|
+
function buildAccessibilityCheckHandler(adapter) {
|
|
444
|
+
return async (opts) => {
|
|
445
|
+
const page = "getPage" in adapter ? adapter.getPage() : void 0;
|
|
446
|
+
if (!page) throw new Error("Accessibility capture requires an active browser page");
|
|
447
|
+
const web = await import("@vostride/agent-qa-web");
|
|
448
|
+
return web.runAccessibilityCheck(page, opts);
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function buildFailureScreenshotWriter(adapter) {
|
|
452
|
+
return async () => captureFailureScreenshotFromAdapter(adapter);
|
|
453
|
+
}
|
|
454
|
+
function toMemberTest(test, filePath) {
|
|
455
|
+
return {
|
|
456
|
+
testId: test["test-id"] ?? test.name,
|
|
457
|
+
name: test.name,
|
|
458
|
+
filePath,
|
|
459
|
+
url: test.url,
|
|
460
|
+
context: test.context,
|
|
461
|
+
steps: test.steps,
|
|
462
|
+
meta: test.meta
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function buildMemberRunConfig(input) {
|
|
466
|
+
const sandbox = input.sandboxOptions;
|
|
467
|
+
return {
|
|
468
|
+
adapter: input.adapter,
|
|
469
|
+
planner: input.planner,
|
|
470
|
+
verifier: input.verifier,
|
|
471
|
+
cache: input.cache,
|
|
472
|
+
workingMemory: input.workingMemory,
|
|
473
|
+
storageState: input.storageState,
|
|
474
|
+
// CTXB-06: model's canonical compaction (compactLogs for log bursts, guardBlob
|
|
475
|
+
// otherwise) replaces the runner-local ceiling fallback on every tool-output value.
|
|
476
|
+
guardBlob: import_agent_qa_model.guardToolOutput,
|
|
477
|
+
compressor: input.compressor,
|
|
478
|
+
knowledgeReader: input.knowledgeReader,
|
|
479
|
+
knowledgeRetrievalRung: input.knowledgeRetrievalRung,
|
|
480
|
+
knowledgeBudgets: input.knowledgeBudgets,
|
|
481
|
+
knowledgeAsOfBuild: input.knowledgeAsOfBuild,
|
|
482
|
+
recordLedger: input.recordLedger,
|
|
483
|
+
beginMemoryTurn: input.beginMemoryTurn,
|
|
484
|
+
drainMemoryReadTraces: input.drainMemoryReadTraces,
|
|
485
|
+
configDigests: input.configDigests,
|
|
486
|
+
plannerModel: input.plannerModel,
|
|
487
|
+
verifierModel: input.verifierModel,
|
|
488
|
+
stepModelScope: input.stepModelScope,
|
|
489
|
+
envFileVars: input.envFileVars,
|
|
490
|
+
inlineVars: input.inlineVars,
|
|
491
|
+
suiteVars: input.suiteVars,
|
|
492
|
+
cliVars: input.cliVars,
|
|
493
|
+
hookSetupVars: input.hookSetupVars,
|
|
494
|
+
// Inline hooks: the runner-local DockerHookExecutor + sandbox env/authState come off the resolved
|
|
495
|
+
// sandbox options; the per-run secret env is resolved store-side (never crosses into the runner).
|
|
496
|
+
hookExecutor: sandbox?.executor,
|
|
497
|
+
inlineHookDefs: input.inlineHookDefs,
|
|
498
|
+
hookEnvVars: sandbox?.envVars,
|
|
499
|
+
hookSecretEnv: (0, import_agent_qa_runner.resolveSecretEnv)(input.secretStore),
|
|
500
|
+
hookAuthState: sandbox?.authState,
|
|
501
|
+
runJs: buildRunJsHandler(input.adapter),
|
|
502
|
+
appiumScript: buildAppiumScriptHandler(input.adapter, input.secretStore),
|
|
503
|
+
accessibilityCheck: "getPage" in input.adapter && typeof input.adapter.getPage === "function" ? buildAccessibilityCheckHandler(input.adapter) : void 0,
|
|
504
|
+
persistFailureScreenshot: buildFailureScreenshotWriter(input.adapter),
|
|
505
|
+
resolveSecrets: (text) => (0, import_agent_qa_runner.resolveSecretTemplatesInValue)(text, input.secretStore),
|
|
506
|
+
secretStore: input.secretStore,
|
|
507
|
+
secretRedactor: input.secretRedactor,
|
|
508
|
+
sink: input.sink,
|
|
509
|
+
testIndex: input.testIndex,
|
|
510
|
+
abortSignal: input.abortSignal,
|
|
511
|
+
evidenceArtifacts: input.evidenceArtifacts,
|
|
512
|
+
captureScreenshots: input.captureScreenshots,
|
|
513
|
+
accessibility: input.accessibility,
|
|
514
|
+
timeouts: input.timeouts,
|
|
515
|
+
plannerConfig: input.plannerConfig,
|
|
516
|
+
logger: input.logger,
|
|
517
|
+
logCapture: input.logCapture,
|
|
518
|
+
vision: input.vision,
|
|
519
|
+
visionGate: input.visionGate,
|
|
520
|
+
contextWindow: input.contextWindow,
|
|
521
|
+
suiteContext: input.suiteContext,
|
|
522
|
+
resolveUploadPath: (rawPath) => (0, import_node_path.isAbsolute)(rawPath) ? rawPath : (0, import_node_path.resolve)((0, import_node_path.dirname)(input.filePath), rawPath)
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// src/composition/run-invocation-core.ts
|
|
527
|
+
var import_node_fs = require("node:fs");
|
|
528
|
+
var import_node_path2 = require("node:path");
|
|
529
|
+
var import_agent_qa_runner2 = require("@vostride/agent-qa-runner");
|
|
530
|
+
|
|
531
|
+
// src/composition/run-risk.ts
|
|
532
|
+
var import_agent_qa_schema = require("@vostride/agent-qa-schema");
|
|
533
|
+
|
|
534
|
+
// src/composition/run-invocation-core.ts
|
|
535
|
+
var REFUSED_RUN_REQUEST_FIELDS = [
|
|
536
|
+
"scope",
|
|
537
|
+
"executionContext"
|
|
538
|
+
];
|
|
539
|
+
var RunRequestFieldRefusedError = class extends Error {
|
|
540
|
+
constructor(refusedFields) {
|
|
541
|
+
super(
|
|
542
|
+
`agent-qa cannot start a run with: ${refusedFields.join(", ")}. scope resolves from the config the run is started against, and executionContext is supplied by the composition that owns the door \u2014 a caller-named execution context would be a caller relabelling where its run came from.`
|
|
543
|
+
);
|
|
544
|
+
this.refusedFields = refusedFields;
|
|
545
|
+
this.name = "RunRequestFieldRefusedError";
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
var RunIdNotMintedError = class extends Error {
|
|
549
|
+
constructor(runId) {
|
|
550
|
+
super(
|
|
551
|
+
`agent-qa cannot start a run under ${JSON.stringify(runId)}: a caller-named run id must be one this system minted. Start the run without naming an id and read the id back off the acknowledgment, or name an id \`agent-qa\` handed you.`
|
|
552
|
+
);
|
|
553
|
+
this.runId = runId;
|
|
554
|
+
this.name = "RunIdNotMintedError";
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
var RunIdAlreadyInUseError = class extends Error {
|
|
558
|
+
constructor(runId) {
|
|
559
|
+
super(
|
|
560
|
+
`agent-qa cannot start a run under ${JSON.stringify(runId)}: that id already names a run in this project. Starting a second run under it would append to the first run's event log and overwrite its handoff. Start the run without naming an id.`
|
|
561
|
+
);
|
|
562
|
+
this.runId = runId;
|
|
563
|
+
this.name = "RunIdAlreadyInUseError";
|
|
564
|
+
}
|
|
565
|
+
};
|
|
566
|
+
var RunDeniedError = class extends Error {
|
|
567
|
+
constructor(runId, reason) {
|
|
568
|
+
super(reason);
|
|
569
|
+
this.runId = runId;
|
|
570
|
+
this.reason = reason;
|
|
571
|
+
this.name = "RunDeniedError";
|
|
572
|
+
this.neutralReason = reason;
|
|
573
|
+
}
|
|
574
|
+
neutralReason;
|
|
575
|
+
};
|
|
576
|
+
function refuseProductionWithoutOptIn(runId, declaration) {
|
|
577
|
+
const production = declaration.productionTargetNames.length > 0 || declaration.targetClass === "production";
|
|
578
|
+
if (!production || declaration.allowProduction === true) return;
|
|
579
|
+
const reason = (0, import_agent_qa_schema.buildProductionRefusal)(declaration.productionTargetNames);
|
|
580
|
+
throw new RunDeniedError(runId, reason);
|
|
581
|
+
}
|
|
582
|
+
function refuseUnavailableRunId(input) {
|
|
583
|
+
if (!input.isMintedRunId(input.runId)) throw new RunIdNotMintedError(input.runId);
|
|
584
|
+
if ((0, import_node_fs.existsSync)(runDirFor({ configDir: input.configDir, runId: input.runId }))) {
|
|
585
|
+
throw new RunIdAlreadyInUseError(input.runId);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
function createRunInvocationCore(options) {
|
|
589
|
+
const configDir = (0, import_node_path2.dirname)(options.agentQaRoot);
|
|
590
|
+
return {
|
|
591
|
+
async execute(request) {
|
|
592
|
+
const refused = REFUSED_RUN_REQUEST_FIELDS.filter((field) => request[field] !== void 0);
|
|
593
|
+
if (refused.length > 0) throw new RunRequestFieldRefusedError(refused);
|
|
594
|
+
if (request.runId !== void 0) {
|
|
595
|
+
refuseUnavailableRunId({
|
|
596
|
+
configDir,
|
|
597
|
+
runId: request.runId,
|
|
598
|
+
isMintedRunId: options.isMintedRunId
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
const runId = request.runId ?? options.mintRunId();
|
|
602
|
+
const declaration = await options.resolveTargetDeclaration(request);
|
|
603
|
+
const targetClass = declaration.targetClass;
|
|
604
|
+
refuseProductionWithoutOptIn(runId, declaration);
|
|
605
|
+
await options.afterRefusal?.();
|
|
606
|
+
const ports = buildRunPorts({ configDir, runId });
|
|
607
|
+
const postVerdict = options.postVerdict?.({ runId, configDir });
|
|
608
|
+
const assembled = await options.assembleRun({
|
|
609
|
+
runId,
|
|
610
|
+
request,
|
|
611
|
+
executionContext: options.executionContext,
|
|
612
|
+
ports: {
|
|
613
|
+
...ports,
|
|
614
|
+
...postVerdict === void 0 ? {} : { postVerdict }
|
|
615
|
+
},
|
|
616
|
+
configDir,
|
|
617
|
+
configPath: request.configPath ?? options.configPath,
|
|
618
|
+
targetClass
|
|
619
|
+
});
|
|
620
|
+
const session = (0, import_agent_qa_runner2.startRun)({
|
|
621
|
+
...assembled,
|
|
622
|
+
runId,
|
|
623
|
+
...request.gateRun === true ? { gateRun: true } : {},
|
|
624
|
+
...request.excludedFromOutcomes === true ? { excludedFromOutcomes: true } : {}
|
|
625
|
+
});
|
|
626
|
+
options.onSession?.(session);
|
|
627
|
+
await session.done;
|
|
628
|
+
return { runId };
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// src/composition/run-drive-vocabulary.ts
|
|
634
|
+
var import_agent_qa_safety_local2 = require("@vostride/agent-qa-safety-local");
|
|
635
|
+
var import_agent_qa_runner3 = require("@vostride/agent-qa-runner");
|
|
636
|
+
|
|
637
|
+
// src/composition/evidence-blob-reader.ts
|
|
638
|
+
var import_node_fs2 = require("node:fs");
|
|
639
|
+
var import_node_path3 = require("node:path");
|
|
640
|
+
var import_agent_qa_runner_local2 = require("@vostride/agent-qa-runner-local");
|
|
641
|
+
var SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
642
|
+
function runDirectoryExists(input, runId) {
|
|
643
|
+
return (0, import_node_fs2.existsSync)((0, import_node_path3.join)(input.agentQaRoot, import_agent_qa_runner_local2.RUNS_SUBDIR, (0, import_agent_qa_runner_local2.slugBranch)(runId)));
|
|
644
|
+
}
|
|
645
|
+
function lookupEvidenceBlob(input, runId, sha) {
|
|
646
|
+
if (!SHA256_HEX_RE.test(sha)) return { status: "blob_absent" };
|
|
647
|
+
if (!runDirectoryExists(input, runId)) return { status: "unknown_run" };
|
|
648
|
+
const bytes = new import_agent_qa_runner_local2.FsEvidenceArtifactStore({ root: input.agentQaRoot, runId }).getBlob(sha);
|
|
649
|
+
return bytes === void 0 ? { status: "blob_absent" } : { status: "found", bytes };
|
|
650
|
+
}
|
|
651
|
+
function statEvidenceBlob(input, runId, sha) {
|
|
652
|
+
if (!SHA256_HEX_RE.test(sha)) return "blob_absent";
|
|
653
|
+
if (!runDirectoryExists(input, runId)) return "unknown_run";
|
|
654
|
+
return new import_agent_qa_runner_local2.FsEvidenceArtifactStore({ root: input.agentQaRoot, runId }).hasBlob(sha) ? "found" : "blob_absent";
|
|
655
|
+
}
|
|
656
|
+
function statEvidenceBlobSize(input, runId, sha) {
|
|
657
|
+
if (!SHA256_HEX_RE.test(sha)) return { status: "blob_absent" };
|
|
658
|
+
if (!runDirectoryExists(input, runId)) return { status: "unknown_run" };
|
|
659
|
+
const byteLength = new import_agent_qa_runner_local2.FsEvidenceArtifactStore({ root: input.agentQaRoot, runId }).blobSize(sha);
|
|
660
|
+
return byteLength === void 0 ? { status: "blob_absent" } : { status: "found", byteLength };
|
|
661
|
+
}
|
|
662
|
+
function readEvidenceBlobRange(input, runId, sha, start, end) {
|
|
663
|
+
if (!SHA256_HEX_RE.test(sha)) return { status: "blob_absent" };
|
|
664
|
+
if (!runDirectoryExists(input, runId)) return { status: "unknown_run" };
|
|
665
|
+
const bytes = new import_agent_qa_runner_local2.FsEvidenceArtifactStore({ root: input.agentQaRoot, runId }).readBlobRange(sha, start, end);
|
|
666
|
+
return bytes === void 0 ? { status: "blob_absent" } : { status: "found", bytes };
|
|
667
|
+
}
|
|
668
|
+
function createEvidenceBlobReader(input) {
|
|
669
|
+
return {
|
|
670
|
+
read: (runId, sha) => Promise.resolve(lookupEvidenceBlob(input, String(runId), sha)),
|
|
671
|
+
has: (runId, sha) => Promise.resolve(statEvidenceBlob(input, String(runId), sha)),
|
|
672
|
+
stat: (runId, sha) => Promise.resolve(statEvidenceBlobSize(input, String(runId), sha)),
|
|
673
|
+
readRange: (runId, sha, start, end) => Promise.resolve(readEvidenceBlobRange(input, String(runId), sha, start, end))
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function readEvidenceBlobBytes(input, runId, sha) {
|
|
677
|
+
const lookup = lookupEvidenceBlob(input, runId, sha);
|
|
678
|
+
return lookup.status === "found" ? lookup.bytes : void 0;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/composition/quarantine-snapshot.ts
|
|
682
|
+
var import_agent_qa_schema2 = require("@vostride/agent-qa-schema");
|
|
683
|
+
var import_reader2 = require("@vostride/agent-qa-knowledge-local/reader");
|
|
684
|
+
function resolveRunQuarantineSnapshot(input) {
|
|
685
|
+
const warn = input.warn ?? (() => {
|
|
686
|
+
});
|
|
687
|
+
const product = (0, import_agent_qa_schema2.slugBranch)(input.product);
|
|
688
|
+
let read;
|
|
689
|
+
try {
|
|
690
|
+
read = (0, import_reader2.readQuarantineLedger)(input.knowledgeRoot);
|
|
691
|
+
} catch (error) {
|
|
692
|
+
warn(
|
|
693
|
+
`${import_reader2.QUARANTINE_LEDGER_FILENAME} could not be read (${error instanceof Error ? error.message : String(error)}); no test is quarantined for this run`
|
|
694
|
+
);
|
|
695
|
+
return [];
|
|
696
|
+
}
|
|
697
|
+
if (read.refusedLines.length > 0) {
|
|
698
|
+
warn(
|
|
699
|
+
`${import_reader2.QUARANTINE_LEDGER_FILENAME} has ${read.refusedLines.length} unreadable line(s) \u2014 ${read.refusedLines.map((refused) => `line ${refused.lineNumber}: ${refused.reason}`).join("; ")}`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
return (0, import_reader2.foldQuarantineLedgerState)(read.events, input.now).filter((entry) => entry.state === "quarantined" && entry.product === product).map((entry) => ({
|
|
703
|
+
product: entry.product,
|
|
704
|
+
filePath: entry.filePath,
|
|
705
|
+
testName: entry.testName,
|
|
706
|
+
expiresAt: entry.expiresAt
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// src/composition/quarantine-vocabulary.ts
|
|
711
|
+
var import_reader3 = require("@vostride/agent-qa-knowledge-local/reader");
|
|
712
|
+
|
|
713
|
+
// src/composition/compose-local-agent.ts
|
|
714
|
+
var import_node_crypto4 = require("node:crypto");
|
|
715
|
+
var import_node_fs9 = require("node:fs");
|
|
716
|
+
var import_node_module = require("node:module");
|
|
717
|
+
var import_node_path10 = require("node:path");
|
|
718
|
+
var import_node_sqlite4 = require("node:sqlite");
|
|
719
|
+
var import_reader5 = require("@vostride/agent-qa-knowledge-local/reader");
|
|
720
|
+
var import_agent_qa_agent4 = require("@vostride/agent-qa-agent");
|
|
721
|
+
var import_agent_qa_knowledge_local = require("@vostride/agent-qa-knowledge-local");
|
|
722
|
+
var import_agent_qa_runner4 = require("@vostride/agent-qa-runner");
|
|
723
|
+
var import_agent_qa_safety = require("@vostride/agent-qa-safety");
|
|
724
|
+
var import_agent_qa_runner_local3 = require("@vostride/agent-qa-runner-local");
|
|
725
|
+
var import_agent_qa_schema10 = require("@vostride/agent-qa-schema");
|
|
726
|
+
|
|
727
|
+
// src/composition/knowledge-review-reader.ts
|
|
728
|
+
var import_reader4 = require("@vostride/agent-qa-knowledge-local/reader");
|
|
729
|
+
function createKnowledgeReaderPort(input) {
|
|
730
|
+
async function sight(bundleIds) {
|
|
731
|
+
if (input.sightMemoryEntities === void 0 || bundleIds.length === 0) return;
|
|
732
|
+
try {
|
|
733
|
+
await input.sightMemoryEntities(bundleIds);
|
|
734
|
+
} catch {
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
// 2026-09-07, 417 D-01: forward the candidate DATA verbatim. Since 441-41 (D-06) nothing here
|
|
739
|
+
// scans it; the chat and MCP doors mask declared values in every op result.
|
|
740
|
+
async candidateInjection({ candidateId }) {
|
|
741
|
+
return (0, import_reader4.candidateInjectionFor)({
|
|
742
|
+
...input.resolveTrustPriors === void 0 ? {} : { priors: input.resolveTrustPriors() },
|
|
743
|
+
knowledgeRoot: await input.resolveKnowledgeRoot(),
|
|
744
|
+
candidateId
|
|
745
|
+
});
|
|
746
|
+
},
|
|
747
|
+
// 416 D-17: the caller supplied the identity; forward the reader's answer verbatim.
|
|
748
|
+
async gateReceipts({ candidateId }) {
|
|
749
|
+
return (0, import_reader4.gateReceiptsFor)({
|
|
750
|
+
knowledgeRoot: await input.resolveKnowledgeRoot(),
|
|
751
|
+
candidateId,
|
|
752
|
+
resolvePromotionPosture: await input.resolvePromotionPosture?.()
|
|
753
|
+
});
|
|
754
|
+
},
|
|
755
|
+
async listBundles() {
|
|
756
|
+
const rows = (0, import_reader4.listBundles)(await input.resolveKnowledgeRoot(), input.resolveTrustPriors?.());
|
|
757
|
+
await sight(rows.map((row) => row.bundleId));
|
|
758
|
+
return rows;
|
|
759
|
+
},
|
|
760
|
+
async inspectRecords({ filter }) {
|
|
761
|
+
const records = (0, import_reader4.inspectRecords)({
|
|
762
|
+
...input.resolveTrustPriors === void 0 ? {} : { priors: input.resolveTrustPriors() },
|
|
763
|
+
knowledgeRoot: await input.resolveKnowledgeRoot(),
|
|
764
|
+
filter
|
|
765
|
+
});
|
|
766
|
+
await sight(records.map((record) => record.recordId));
|
|
767
|
+
return records;
|
|
768
|
+
},
|
|
769
|
+
async diffSince({ sinceOrdinal }) {
|
|
770
|
+
const changes = (0, import_reader4.diffSince)({
|
|
771
|
+
knowledgeRoot: await input.resolveKnowledgeRoot(),
|
|
772
|
+
sinceOrdinal
|
|
773
|
+
});
|
|
774
|
+
await sight(changes.map((change) => change.recordId));
|
|
775
|
+
return changes;
|
|
776
|
+
},
|
|
777
|
+
async showRecord({ recordId }) {
|
|
778
|
+
const result = (0, import_reader4.showRecord)({
|
|
779
|
+
...input.resolveTrustPriors === void 0 ? {} : { priors: input.resolveTrustPriors() },
|
|
780
|
+
knowledgeRoot: await input.resolveKnowledgeRoot(),
|
|
781
|
+
recordId
|
|
782
|
+
});
|
|
783
|
+
await sight(result.kind === "found" ? [result.record.recordId] : []);
|
|
784
|
+
return result;
|
|
785
|
+
},
|
|
786
|
+
// Caller-supplied union IDs are already known, like canary tuples below. Forward
|
|
787
|
+
// the maintenance batch without address-ledger sightings or per-record scans.
|
|
788
|
+
async showRecords({ recordIds }) {
|
|
789
|
+
return (0, import_reader4.showRecords)({ knowledgeRoot: await input.resolveKnowledgeRoot(), recordIds });
|
|
790
|
+
},
|
|
791
|
+
/**
|
|
792
|
+
* 407 D-08: what became of the canary lines one run served.
|
|
793
|
+
*
|
|
794
|
+
* Resolved per call like its four siblings, and NOT sighted. The caller already holds every
|
|
795
|
+
* candidate id in the tuples it passes — it read them off its own run's evidence — so a
|
|
796
|
+
* sighting here would record that the reader was handed ids rather than that an entity was
|
|
797
|
+
* surfaced to anyone, which is the distinction the ledger exists to keep.
|
|
798
|
+
*
|
|
799
|
+
* The reader entry's answer is forwarded VERBATIM, `skippedBundles` included (407-19, REVIEW
|
|
800
|
+
* WR-03). This binding neither counts nor interprets: how much of the store went unread is a
|
|
801
|
+
* fact only the walk knows, and a composition that dropped it here would hand the op a
|
|
802
|
+
* clean-looking answer over a half-read store.
|
|
803
|
+
*/
|
|
804
|
+
async canaryAttributions({ runId, tuples }) {
|
|
805
|
+
return (0, import_reader4.canaryAttributionsFor)({
|
|
806
|
+
knowledgeRoot: await input.resolveKnowledgeRoot(),
|
|
807
|
+
runId,
|
|
808
|
+
tuples
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/analysis/persist-analysis.ts
|
|
815
|
+
var import_node_fs3 = require("node:fs");
|
|
816
|
+
var import_node_crypto2 = require("node:crypto");
|
|
817
|
+
var import_node_path4 = require("node:path");
|
|
818
|
+
var import_agent_qa_schema3 = require("@vostride/agent-qa-schema");
|
|
819
|
+
var ANALYSIS_STAGE_PREFIX = "analysis.json";
|
|
820
|
+
function fsyncFile(file) {
|
|
821
|
+
const fd = (0, import_node_fs3.openSync)(file, "r");
|
|
822
|
+
try {
|
|
823
|
+
(0, import_node_fs3.fsyncSync)(fd);
|
|
824
|
+
} finally {
|
|
825
|
+
(0, import_node_fs3.closeSync)(fd);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
function writeAnalysisArtifact(path, artifact) {
|
|
829
|
+
const dir = (0, import_node_path4.dirname)(path);
|
|
830
|
+
const staged = (0, import_node_path4.join)(dir, `.${ANALYSIS_STAGE_PREFIX}.${(0, import_node_crypto2.randomUUID)()}.tmp`);
|
|
831
|
+
try {
|
|
832
|
+
(0, import_node_fs3.writeFileSync)(staged, `${(0, import_agent_qa_schema3.canonicalJson)(artifact)}
|
|
833
|
+
`, { encoding: "utf8", mode: 384 });
|
|
834
|
+
fsyncFile(staged);
|
|
835
|
+
(0, import_node_fs3.renameSync)(staged, path);
|
|
836
|
+
fsyncFile(dir);
|
|
837
|
+
} finally {
|
|
838
|
+
(0, import_node_fs3.rmSync)(staged, { force: true });
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// src/session/jsonl-session-log.ts
|
|
843
|
+
var import_node_fs5 = require("node:fs");
|
|
844
|
+
var import_node_path6 = require("node:path");
|
|
845
|
+
var import_agent_qa_agent2 = require("@vostride/agent-qa-agent");
|
|
846
|
+
var import_agent_qa_schema6 = require("@vostride/agent-qa-schema");
|
|
847
|
+
|
|
848
|
+
// src/store/job-checkpoint-store.ts
|
|
849
|
+
var import_node_crypto3 = require("node:crypto");
|
|
850
|
+
var import_node_fs4 = require("node:fs");
|
|
851
|
+
var import_node_path5 = require("node:path");
|
|
852
|
+
var import_agent_qa_schema5 = require("@vostride/agent-qa-schema");
|
|
853
|
+
|
|
854
|
+
// src/store/store-errors.ts
|
|
855
|
+
var import_agent_qa_schema4 = require("@vostride/agent-qa-schema");
|
|
856
|
+
var import_agent_qa_agent = require("@vostride/agent-qa-agent");
|
|
857
|
+
var JobStoreSchemaVersionError = class extends Error {
|
|
858
|
+
constructor(path, expectedVersion, observedVersion) {
|
|
859
|
+
super(
|
|
860
|
+
`The agent job store at ${path} is stamped at schema version ${observedVersion}, but this build reads version ${expectedVersion}. There is no migration machinery by design \u2014 clear agent job queue state (delete the agent job database and its checkpoint directory) and re-enqueue.`
|
|
861
|
+
);
|
|
862
|
+
this.path = path;
|
|
863
|
+
this.expectedVersion = expectedVersion;
|
|
864
|
+
this.observedVersion = observedVersion;
|
|
865
|
+
}
|
|
866
|
+
name = "JobStoreSchemaVersionError";
|
|
867
|
+
};
|
|
868
|
+
var UnknownJobRowError = class extends Error {
|
|
869
|
+
constructor(jobId) {
|
|
870
|
+
super(`No agent job row with id ${jobId}`);
|
|
871
|
+
this.jobId = jobId;
|
|
872
|
+
}
|
|
873
|
+
name = "UnknownJobRowError";
|
|
874
|
+
};
|
|
875
|
+
var InvalidJobCheckpointIdError = class extends Error {
|
|
876
|
+
constructor(field, value) {
|
|
877
|
+
super(
|
|
878
|
+
`Unusable ${field} for a checkpoint path: ${JSON.stringify(value)}. A checkpoint id becomes a path segment, so it is validated before the path is built rather than after.`
|
|
879
|
+
);
|
|
880
|
+
this.field = field;
|
|
881
|
+
this.value = value;
|
|
882
|
+
}
|
|
883
|
+
name = "InvalidJobCheckpointIdError";
|
|
884
|
+
};
|
|
885
|
+
var JobCheckpointSchemaVersionError = class extends Error {
|
|
886
|
+
constructor(path, observedVersion) {
|
|
887
|
+
super(
|
|
888
|
+
`Agent job checkpoint ${path} was written at schema version ${String(observedVersion)}, but this build reads version ${import_agent_qa_schema4.JOB_CHECKPOINT_SCHEMA_VERSION}. There is no migration machinery by design \u2014 clear agent job queue state and re-enqueue.`
|
|
889
|
+
);
|
|
890
|
+
this.path = path;
|
|
891
|
+
this.observedVersion = observedVersion;
|
|
892
|
+
}
|
|
893
|
+
name = "JobCheckpointSchemaVersionError";
|
|
894
|
+
};
|
|
895
|
+
|
|
896
|
+
// src/store/job-checkpoint-store.ts
|
|
897
|
+
var JOB_CHECKPOINT_SUBDIR = ["agent-jobs", "checkpoints"];
|
|
898
|
+
var SAFE_PATH_SEGMENT = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
899
|
+
var DIRECTORY_FSYNC_SWALLOWED_CODES = [
|
|
900
|
+
"ENOENT",
|
|
901
|
+
"EINVAL",
|
|
902
|
+
"EISDIR",
|
|
903
|
+
"EPERM",
|
|
904
|
+
"ENOTSUP"
|
|
905
|
+
];
|
|
906
|
+
var REAL_JOB_CHECKPOINT_FS = Object.freeze({
|
|
907
|
+
mkdirSync: import_node_fs4.mkdirSync,
|
|
908
|
+
openSync: import_node_fs4.openSync,
|
|
909
|
+
writeFileSync: import_node_fs4.writeFileSync,
|
|
910
|
+
fsyncSync: import_node_fs4.fsyncSync,
|
|
911
|
+
closeSync: import_node_fs4.closeSync,
|
|
912
|
+
renameSync: import_node_fs4.renameSync,
|
|
913
|
+
rmSync: import_node_fs4.rmSync,
|
|
914
|
+
readFileSync: import_node_fs4.readFileSync
|
|
915
|
+
});
|
|
916
|
+
function requireSegment(field, value) {
|
|
917
|
+
if (!SAFE_PATH_SEGMENT.test(value)) {
|
|
918
|
+
throw new InvalidJobCheckpointIdError(field, value);
|
|
919
|
+
}
|
|
920
|
+
return value;
|
|
921
|
+
}
|
|
922
|
+
function checkpointPath(root, jobId, checkpointId) {
|
|
923
|
+
if (!(0, import_node_path5.isAbsolute)(root)) throw new TypeError("agent job checkpoint root must be absolute");
|
|
924
|
+
return (0, import_node_path5.join)(
|
|
925
|
+
root,
|
|
926
|
+
...JOB_CHECKPOINT_SUBDIR,
|
|
927
|
+
requireSegment("jobId", jobId),
|
|
928
|
+
`${requireSegment("checkpointId", checkpointId)}.json`
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
function readEnvelopeOn(fs, root, jobId, checkpointId) {
|
|
932
|
+
const file = checkpointPath(root, jobId, checkpointId);
|
|
933
|
+
let text;
|
|
934
|
+
try {
|
|
935
|
+
text = fs.readFileSync(file, "utf8");
|
|
936
|
+
} catch (error) {
|
|
937
|
+
if (error.code === "ENOENT") return { found: false };
|
|
938
|
+
throw error;
|
|
939
|
+
}
|
|
940
|
+
const raw = JSON.parse(text);
|
|
941
|
+
if (raw?.schemaVersion !== import_agent_qa_schema5.JOB_CHECKPOINT_SCHEMA_VERSION) {
|
|
942
|
+
throw new JobCheckpointSchemaVersionError(file, raw?.schemaVersion);
|
|
943
|
+
}
|
|
944
|
+
const envelope = import_agent_qa_schema5.jobCheckpointEnvelopeSchema.parse(raw);
|
|
945
|
+
if (envelope.jobId !== jobId || envelope.checkpointId !== checkpointId) {
|
|
946
|
+
throw new TypeError("Agent job checkpoint envelope does not match its address");
|
|
947
|
+
}
|
|
948
|
+
return { found: true, envelope };
|
|
949
|
+
}
|
|
950
|
+
function readJobCheckpointEnvelope(options, jobId, checkpointId) {
|
|
951
|
+
return readEnvelopeOn(REAL_JOB_CHECKPOINT_FS, options.root, jobId, checkpointId);
|
|
952
|
+
}
|
|
953
|
+
function isSwallowableDirectoryFsyncError(error) {
|
|
954
|
+
const code = error.code;
|
|
955
|
+
return DIRECTORY_FSYNC_SWALLOWED_CODES.some((allowed) => allowed === code);
|
|
956
|
+
}
|
|
957
|
+
function createJobCheckpointStore(options) {
|
|
958
|
+
return createJobCheckpointStoreOn(REAL_JOB_CHECKPOINT_FS, options);
|
|
959
|
+
}
|
|
960
|
+
function createJobCheckpointStoreOn(fs, options) {
|
|
961
|
+
if (!(0, import_node_path5.isAbsolute)(options.root)) {
|
|
962
|
+
throw new TypeError("agent job checkpoint root must be absolute");
|
|
963
|
+
}
|
|
964
|
+
const checkpointsRoot = (0, import_node_path5.join)(options.root, ...JOB_CHECKPOINT_SUBDIR);
|
|
965
|
+
function pathFor(jobId, checkpointId) {
|
|
966
|
+
return checkpointPath(options.root, jobId, checkpointId);
|
|
967
|
+
}
|
|
968
|
+
function bestEffortFsyncDirectory(dir) {
|
|
969
|
+
let fd;
|
|
970
|
+
try {
|
|
971
|
+
fd = fs.openSync(dir, "r");
|
|
972
|
+
fs.fsyncSync(fd);
|
|
973
|
+
} catch (error) {
|
|
974
|
+
if (!isSwallowableDirectoryFsyncError(error)) {
|
|
975
|
+
throw error;
|
|
976
|
+
}
|
|
977
|
+
} finally {
|
|
978
|
+
if (fd !== void 0) fs.closeSync(fd);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return {
|
|
982
|
+
checkpointsRoot,
|
|
983
|
+
checkpointPath: pathFor,
|
|
984
|
+
publish(jobId, checkpointId, payload) {
|
|
985
|
+
const file = pathFor(jobId, checkpointId);
|
|
986
|
+
const envelope = {
|
|
987
|
+
schemaVersion: import_agent_qa_schema5.JOB_CHECKPOINT_SCHEMA_VERSION,
|
|
988
|
+
jobId: (0, import_agent_qa_schema5.asJobId)(jobId),
|
|
989
|
+
checkpointId,
|
|
990
|
+
payload
|
|
991
|
+
};
|
|
992
|
+
fs.mkdirSync((0, import_node_path5.dirname)(file), { recursive: true });
|
|
993
|
+
const temporary = `${file}.tmp-${(0, import_node_crypto3.randomUUID)()}`;
|
|
994
|
+
let fd;
|
|
995
|
+
try {
|
|
996
|
+
fd = fs.openSync(temporary, "wx");
|
|
997
|
+
fs.writeFileSync(fd, `${(0, import_agent_qa_schema5.canonicalJson)(envelope)}
|
|
998
|
+
`, "utf8");
|
|
999
|
+
fs.fsyncSync(fd);
|
|
1000
|
+
fs.closeSync(fd);
|
|
1001
|
+
fd = void 0;
|
|
1002
|
+
fs.renameSync(temporary, file);
|
|
1003
|
+
bestEffortFsyncDirectory((0, import_node_path5.dirname)(file));
|
|
1004
|
+
} finally {
|
|
1005
|
+
if (fd !== void 0) fs.closeSync(fd);
|
|
1006
|
+
fs.rmSync(temporary, { force: true });
|
|
1007
|
+
}
|
|
1008
|
+
},
|
|
1009
|
+
discard(jobId, checkpointId) {
|
|
1010
|
+
fs.rmSync(pathFor(jobId, checkpointId), { force: true });
|
|
1011
|
+
},
|
|
1012
|
+
read(jobId, checkpointId) {
|
|
1013
|
+
const read = readEnvelopeOn(fs, options.root, jobId, checkpointId);
|
|
1014
|
+
return read.found ? { found: true, payload: read.envelope.payload } : read;
|
|
1015
|
+
}
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// src/session/jsonl-session-log.ts
|
|
1020
|
+
var CHAT_SESSIONS_SUBDIR = "sessions";
|
|
1021
|
+
var SESSION_FILE_SUFFIX = ".jsonl";
|
|
1022
|
+
var CHAT_SESSION_READ_CHUNK_BYTES = 64 * 1024;
|
|
1023
|
+
var SAFE_PATH_SEGMENT2 = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
1024
|
+
var InvalidChatSessionIdError = class extends Error {
|
|
1025
|
+
constructor(sessionId) {
|
|
1026
|
+
super(
|
|
1027
|
+
`${JSON.stringify(sessionId)} is not a usable chat session id. Ids are minted by this adapter and are limited to letters, digits, dot, dash and underscore.`
|
|
1028
|
+
);
|
|
1029
|
+
this.sessionId = sessionId;
|
|
1030
|
+
}
|
|
1031
|
+
name = "InvalidChatSessionIdError";
|
|
1032
|
+
};
|
|
1033
|
+
var UnknownChatSessionError = class extends Error {
|
|
1034
|
+
constructor(sessionId) {
|
|
1035
|
+
super(`No chat session ${sessionId} exists under this project's runtime root.`);
|
|
1036
|
+
this.sessionId = sessionId;
|
|
1037
|
+
}
|
|
1038
|
+
name = "UnknownChatSessionError";
|
|
1039
|
+
};
|
|
1040
|
+
var ChatSessionBusyError = class extends Error {
|
|
1041
|
+
constructor(sessionId) {
|
|
1042
|
+
super(
|
|
1043
|
+
`Chat session ${sessionId} is already open in this process. One writer at a time: a second handle would interleave two turns in one conversation.`
|
|
1044
|
+
);
|
|
1045
|
+
this.sessionId = sessionId;
|
|
1046
|
+
}
|
|
1047
|
+
name = "ChatSessionBusyError";
|
|
1048
|
+
};
|
|
1049
|
+
var ChatSessionHeaderError = class extends Error {
|
|
1050
|
+
constructor(sessionId, detail) {
|
|
1051
|
+
super(`The session file for ${sessionId} cannot be read: ${detail}.`);
|
|
1052
|
+
this.sessionId = sessionId;
|
|
1053
|
+
this.detail = detail;
|
|
1054
|
+
}
|
|
1055
|
+
name = "ChatSessionHeaderError";
|
|
1056
|
+
};
|
|
1057
|
+
var REAL_CHAT_SESSION_FS = Object.freeze({
|
|
1058
|
+
mkdirSync: import_node_fs5.mkdirSync,
|
|
1059
|
+
openSync: import_node_fs5.openSync,
|
|
1060
|
+
writeFileSync: import_node_fs5.writeFileSync,
|
|
1061
|
+
fsyncSync: import_node_fs5.fsyncSync,
|
|
1062
|
+
closeSync: import_node_fs5.closeSync,
|
|
1063
|
+
readSync: import_node_fs5.readSync,
|
|
1064
|
+
readFileSync: import_node_fs5.readFileSync,
|
|
1065
|
+
readdirSync: import_node_fs5.readdirSync
|
|
1066
|
+
});
|
|
1067
|
+
function mintChatSessionId(nowIso, random) {
|
|
1068
|
+
const stamp = nowIso.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z").replace("T", "-").replace("Z", "");
|
|
1069
|
+
const suffix = Math.floor(random() * 36 ** 4).toString(36).padStart(4, "0");
|
|
1070
|
+
return `chat-${stamp}-${suffix}`;
|
|
1071
|
+
}
|
|
1072
|
+
function requireSessionId(sessionId) {
|
|
1073
|
+
if (!SAFE_PATH_SEGMENT2.test(sessionId)) throw new InvalidChatSessionIdError(sessionId);
|
|
1074
|
+
return sessionId;
|
|
1075
|
+
}
|
|
1076
|
+
function isSwallowableDirectoryFsyncError2(error) {
|
|
1077
|
+
const code = error.code;
|
|
1078
|
+
return DIRECTORY_FSYNC_SWALLOWED_CODES.some((allowed) => allowed === code);
|
|
1079
|
+
}
|
|
1080
|
+
function guardHeader(sessionId, line) {
|
|
1081
|
+
let parsed;
|
|
1082
|
+
try {
|
|
1083
|
+
parsed = JSON.parse(line);
|
|
1084
|
+
} catch {
|
|
1085
|
+
throw new ChatSessionHeaderError(sessionId, "its first line is not JSON");
|
|
1086
|
+
}
|
|
1087
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1088
|
+
throw new ChatSessionHeaderError(sessionId, "its header line is not an object");
|
|
1089
|
+
}
|
|
1090
|
+
const header = parsed;
|
|
1091
|
+
if (header.schemaVersion !== import_agent_qa_schema6.CURRENT_CHAT_SESSION_SCHEMA_VERSION) {
|
|
1092
|
+
throw new ChatSessionHeaderError(
|
|
1093
|
+
sessionId,
|
|
1094
|
+
`it declares schemaVersion ${String(header.schemaVersion)} and this build reads ` + String(import_agent_qa_schema6.CURRENT_CHAT_SESSION_SCHEMA_VERSION)
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
if (header.sessionId !== sessionId) {
|
|
1098
|
+
throw new ChatSessionHeaderError(
|
|
1099
|
+
sessionId,
|
|
1100
|
+
`its header names session ${String(header.sessionId)}`
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
if (typeof header.createdAt !== "string" || typeof header.model !== "string") {
|
|
1104
|
+
throw new ChatSessionHeaderError(sessionId, "its header names no creation instant or model");
|
|
1105
|
+
}
|
|
1106
|
+
if (header.surface !== void 0 && typeof header.surface !== "string") {
|
|
1107
|
+
throw new ChatSessionHeaderError(
|
|
1108
|
+
sessionId,
|
|
1109
|
+
`its header names a surface of type ${typeof header.surface} and this build reads a string`
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
return header;
|
|
1113
|
+
}
|
|
1114
|
+
function listSessions(root) {
|
|
1115
|
+
if (!(0, import_node_path6.isAbsolute)(root)) throw new TypeError("chat session log root must be absolute");
|
|
1116
|
+
const fs = REAL_CHAT_SESSION_FS;
|
|
1117
|
+
const sessionsRoot = (0, import_node_path6.join)(root, CHAT_SESSIONS_SUBDIR);
|
|
1118
|
+
let files;
|
|
1119
|
+
try {
|
|
1120
|
+
files = fs.readdirSync(sessionsRoot);
|
|
1121
|
+
} catch (error) {
|
|
1122
|
+
if (error.code === "ENOENT") return { sessions: [], refused: [] };
|
|
1123
|
+
throw error;
|
|
1124
|
+
}
|
|
1125
|
+
const sessions = [];
|
|
1126
|
+
const refused = [];
|
|
1127
|
+
for (const file of [...files].sort()) {
|
|
1128
|
+
if (!file.endsWith(SESSION_FILE_SUFFIX)) continue;
|
|
1129
|
+
const sessionId = file.slice(0, -SESSION_FILE_SUFFIX.length);
|
|
1130
|
+
try {
|
|
1131
|
+
const scanned = scanSessionFile(fs, (0, import_node_path6.join)(sessionsRoot, file));
|
|
1132
|
+
if (scanned.headerLine === void 0) {
|
|
1133
|
+
refused.push({ file, reason: "it carries no complete header line" });
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const header = guardHeader(sessionId, scanned.headerLine);
|
|
1137
|
+
sessions.push({
|
|
1138
|
+
sessionId: header.sessionId,
|
|
1139
|
+
createdAt: header.createdAt,
|
|
1140
|
+
model: header.model,
|
|
1141
|
+
eventCount: Math.max(scanned.lineCount - 1, 0)
|
|
1142
|
+
});
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
refused.push({ file, reason: error instanceof Error ? error.message : String(error) });
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
return { sessions, refused };
|
|
1148
|
+
}
|
|
1149
|
+
function scanSessionFile(fs, path) {
|
|
1150
|
+
const fd = fs.openSync(path, "r");
|
|
1151
|
+
try {
|
|
1152
|
+
const buffer = Buffer.allocUnsafe(CHAT_SESSION_READ_CHUNK_BYTES);
|
|
1153
|
+
let headerLine;
|
|
1154
|
+
let headerBytes = "";
|
|
1155
|
+
let lineCount = 0;
|
|
1156
|
+
let read = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
1157
|
+
while (read > 0) {
|
|
1158
|
+
const chunk = buffer.subarray(0, read);
|
|
1159
|
+
for (let index = 0; index < chunk.length; index += 1) {
|
|
1160
|
+
if (chunk[index] === 10) lineCount += 1;
|
|
1161
|
+
}
|
|
1162
|
+
if (headerLine === void 0) {
|
|
1163
|
+
headerBytes += chunk.toString("utf8");
|
|
1164
|
+
const newline = headerBytes.indexOf("\n");
|
|
1165
|
+
if (newline >= 0) headerLine = headerBytes.slice(0, newline);
|
|
1166
|
+
else if (headerBytes.length > CHAT_SESSION_READ_CHUNK_BYTES) break;
|
|
1167
|
+
}
|
|
1168
|
+
read = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
1169
|
+
}
|
|
1170
|
+
return { headerLine, lineCount };
|
|
1171
|
+
} finally {
|
|
1172
|
+
fs.closeSync(fd);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
function createJsonlSessionLog(options) {
|
|
1176
|
+
return createJsonlSessionLogOn(REAL_CHAT_SESSION_FS, options);
|
|
1177
|
+
}
|
|
1178
|
+
function createJsonlSessionLogOn(fs, options) {
|
|
1179
|
+
if (!(0, import_node_path6.isAbsolute)(options.root)) {
|
|
1180
|
+
throw new TypeError("chat session log root must be absolute");
|
|
1181
|
+
}
|
|
1182
|
+
const sessionsRoot = (0, import_node_path6.join)(options.root, CHAT_SESSIONS_SUBDIR);
|
|
1183
|
+
const random = options.random ?? Math.random;
|
|
1184
|
+
const liveDescriptors = /* @__PURE__ */ new Map();
|
|
1185
|
+
function pathFor(sessionId) {
|
|
1186
|
+
return (0, import_node_path6.join)(sessionsRoot, `${sessionId}.jsonl`);
|
|
1187
|
+
}
|
|
1188
|
+
function bestEffortFsyncDirectory(dir) {
|
|
1189
|
+
let directoryFd;
|
|
1190
|
+
try {
|
|
1191
|
+
directoryFd = fs.openSync(dir, "r");
|
|
1192
|
+
fs.fsyncSync(directoryFd);
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
if (!isSwallowableDirectoryFsyncError2(error)) throw error;
|
|
1195
|
+
} finally {
|
|
1196
|
+
if (directoryFd !== void 0) fs.closeSync(directoryFd);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
function readLines(sessionId) {
|
|
1200
|
+
let text;
|
|
1201
|
+
try {
|
|
1202
|
+
text = fs.readFileSync(pathFor(sessionId), "utf8");
|
|
1203
|
+
} catch (error) {
|
|
1204
|
+
if (error.code === "ENOENT") return void 0;
|
|
1205
|
+
throw error;
|
|
1206
|
+
}
|
|
1207
|
+
return {
|
|
1208
|
+
// Line numbers are 1-based and count the header, so a refusal names what an editor shows the
|
|
1209
|
+
// person who has to go and look at the file.
|
|
1210
|
+
lines: text.split("\n").map((line, index) => ({ lineNumber: index + 1, text: line })).filter((line) => line.text !== ""),
|
|
1211
|
+
// Every line this adapter writes ends in a newline, so a file that does not is a file whose
|
|
1212
|
+
// last write was cut short. That fact cannot be recovered once the lines are split, and the
|
|
1213
|
+
// append path needs it BEFORE it writes anything (see `terminateTornTail`).
|
|
1214
|
+
terminated: text === "" || text.endsWith("\n")
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
function writeEvent(target, event) {
|
|
1218
|
+
fs.writeFileSync(target, `${JSON.stringify(event)}
|
|
1219
|
+
`, "utf8");
|
|
1220
|
+
}
|
|
1221
|
+
function handleFor(id, fd, opened) {
|
|
1222
|
+
let live = true;
|
|
1223
|
+
const requireLive = (verb) => {
|
|
1224
|
+
if (!live) {
|
|
1225
|
+
throw new TypeError(
|
|
1226
|
+
`a chat session ${verb} cannot go through a closed handle for ${id}. Open the session again to write to it.`
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
const handle = {
|
|
1231
|
+
header: opened.header,
|
|
1232
|
+
events: opened.events,
|
|
1233
|
+
refusedLines: opened.refusedLines,
|
|
1234
|
+
append(event) {
|
|
1235
|
+
requireLive("event");
|
|
1236
|
+
writeEvent(fd, event);
|
|
1237
|
+
return Promise.resolve();
|
|
1238
|
+
},
|
|
1239
|
+
turnBoundary() {
|
|
1240
|
+
requireLive("turn boundary");
|
|
1241
|
+
fs.fsyncSync(fd);
|
|
1242
|
+
return Promise.resolve();
|
|
1243
|
+
},
|
|
1244
|
+
close() {
|
|
1245
|
+
if (live) {
|
|
1246
|
+
live = false;
|
|
1247
|
+
liveDescriptors.delete(id);
|
|
1248
|
+
fs.closeSync(fd);
|
|
1249
|
+
}
|
|
1250
|
+
return Promise.resolve();
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
liveDescriptors.set(id, fd);
|
|
1254
|
+
return handle;
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
open(sessionId) {
|
|
1258
|
+
fs.mkdirSync(sessionsRoot, { recursive: true });
|
|
1259
|
+
if (sessionId !== void 0) {
|
|
1260
|
+
const id2 = requireSessionId(sessionId);
|
|
1261
|
+
if (liveDescriptors.has(id2)) throw new ChatSessionBusyError(id2);
|
|
1262
|
+
const read = readLines(id2);
|
|
1263
|
+
if (read === void 0 || read.lines.length === 0) throw new UnknownChatSessionError(id2);
|
|
1264
|
+
const header2 = guardHeader(id2, read.lines[0].text);
|
|
1265
|
+
const repair = (0, import_agent_qa_agent2.repairSession)(read.lines.slice(1), {
|
|
1266
|
+
declaredVersion: header2.schemaVersion,
|
|
1267
|
+
now: options.now
|
|
1268
|
+
});
|
|
1269
|
+
const resumed = fs.openSync(pathFor(id2), "a");
|
|
1270
|
+
let wroteDuringRepair = false;
|
|
1271
|
+
if (!read.terminated) {
|
|
1272
|
+
fs.writeFileSync(resumed, "\n", "utf8");
|
|
1273
|
+
wroteDuringRepair = true;
|
|
1274
|
+
}
|
|
1275
|
+
for (const event of repair.synthesized) {
|
|
1276
|
+
writeEvent(resumed, event);
|
|
1277
|
+
wroteDuringRepair = true;
|
|
1278
|
+
}
|
|
1279
|
+
if (wroteDuringRepair) {
|
|
1280
|
+
fs.fsyncSync(resumed);
|
|
1281
|
+
}
|
|
1282
|
+
return Promise.resolve(
|
|
1283
|
+
handleFor(id2, resumed, {
|
|
1284
|
+
header: header2,
|
|
1285
|
+
events: repair.repaired,
|
|
1286
|
+
refusedLines: repair.refusedLines
|
|
1287
|
+
})
|
|
1288
|
+
);
|
|
1289
|
+
}
|
|
1290
|
+
const createdAt = options.now();
|
|
1291
|
+
const id = mintChatSessionId(createdAt, random);
|
|
1292
|
+
const header = {
|
|
1293
|
+
schemaVersion: import_agent_qa_schema6.CURRENT_CHAT_SESSION_SCHEMA_VERSION,
|
|
1294
|
+
sessionId: id,
|
|
1295
|
+
createdAt,
|
|
1296
|
+
model: options.model(),
|
|
1297
|
+
toolsetHash: options.toolsetHash(),
|
|
1298
|
+
// CONDITIONALLY SPREAD, on the composition's own idiom (400-01) and for its reason: an
|
|
1299
|
+
// unconditional `surface: options.surface` would put `"surface":undefined` through
|
|
1300
|
+
// `JSON.stringify` — which drops it — but would also make an explicit `undefined` and an
|
|
1301
|
+
// unsupplied option two spellings of one state at every call site above. Absent here means
|
|
1302
|
+
// the option was never given, and that is exactly the pre-400 population's meaning.
|
|
1303
|
+
...options.surface === void 0 ? {} : { surface: options.surface }
|
|
1304
|
+
};
|
|
1305
|
+
const created = fs.openSync(pathFor(id), "wx");
|
|
1306
|
+
try {
|
|
1307
|
+
fs.writeFileSync(created, `${JSON.stringify(header)}
|
|
1308
|
+
`, "utf8");
|
|
1309
|
+
fs.fsyncSync(created);
|
|
1310
|
+
} finally {
|
|
1311
|
+
fs.closeSync(created);
|
|
1312
|
+
}
|
|
1313
|
+
bestEffortFsyncDirectory(sessionsRoot);
|
|
1314
|
+
const appendFd = fs.openSync(pathFor(id), "a");
|
|
1315
|
+
return Promise.resolve(handleFor(id, appendFd, { header, events: [], refusedLines: [] }));
|
|
1316
|
+
}
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
// src/store/post-processing-reader.ts
|
|
1321
|
+
var import_node_fs8 = require("node:fs");
|
|
1322
|
+
var import_node_path9 = require("node:path");
|
|
1323
|
+
var import_node_sqlite3 = require("node:sqlite");
|
|
1324
|
+
var import_agent_qa_schema9 = require("@vostride/agent-qa-schema");
|
|
1325
|
+
|
|
1326
|
+
// src/store/sqlite-job-store.ts
|
|
1327
|
+
var import_node_fs7 = require("node:fs");
|
|
1328
|
+
var import_node_path8 = require("node:path");
|
|
1329
|
+
var import_node_sqlite2 = require("node:sqlite");
|
|
1330
|
+
var import_agent_qa_agent3 = require("@vostride/agent-qa-agent");
|
|
1331
|
+
var import_agent_qa_schema8 = require("@vostride/agent-qa-schema");
|
|
1332
|
+
|
|
1333
|
+
// src/store/logical-dump.ts
|
|
1334
|
+
var import_node_fs6 = require("node:fs");
|
|
1335
|
+
var import_node_path7 = require("node:path");
|
|
1336
|
+
var import_node_sqlite = require("node:sqlite");
|
|
1337
|
+
var import_agent_qa_schema7 = require("@vostride/agent-qa-schema");
|
|
1338
|
+
var SAFE_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1339
|
+
function requireIdentifier(value) {
|
|
1340
|
+
if (!SAFE_IDENTIFIER.test(value)) {
|
|
1341
|
+
throw new TypeError(`unusable sqlite identifier in a logical dump: ${JSON.stringify(value)}`);
|
|
1342
|
+
}
|
|
1343
|
+
return value;
|
|
1344
|
+
}
|
|
1345
|
+
function orderedSelectSql(table, columns) {
|
|
1346
|
+
if (columns.length === 0) {
|
|
1347
|
+
throw new TypeError(`table ${table} reports no columns, so no order can be stated`);
|
|
1348
|
+
}
|
|
1349
|
+
const order = columns.map((column) => `"${requireIdentifier(column)}" ASC`).join(", ");
|
|
1350
|
+
return `SELECT * FROM "${requireIdentifier(table)}" ORDER BY ${order}`;
|
|
1351
|
+
}
|
|
1352
|
+
function listStoreTables(db) {
|
|
1353
|
+
return db.prepare(
|
|
1354
|
+
`SELECT name FROM sqlite_master
|
|
1355
|
+
WHERE type = 'table' AND name NOT LIKE 'sqlite_%'
|
|
1356
|
+
ORDER BY name ASC`
|
|
1357
|
+
).all().map((row) => String(row.name));
|
|
1358
|
+
}
|
|
1359
|
+
function tableColumns(db, table) {
|
|
1360
|
+
return db.prepare(`PRAGMA table_info("${requireIdentifier(table)}")`).all().map((row) => String(row.name));
|
|
1361
|
+
}
|
|
1362
|
+
function normalize(value) {
|
|
1363
|
+
if (typeof value === "bigint") return Number(value);
|
|
1364
|
+
if (value instanceof Uint8Array) return { base16: Buffer.from(value).toString("hex") };
|
|
1365
|
+
return value;
|
|
1366
|
+
}
|
|
1367
|
+
function selectOrderedRows(db, table) {
|
|
1368
|
+
const columns = tableColumns(db, table);
|
|
1369
|
+
const rows = db.prepare(orderedSelectSql(table, columns)).all().map((row) => {
|
|
1370
|
+
const normalized = {};
|
|
1371
|
+
for (const column of columns) normalized[column] = normalize(row[column]);
|
|
1372
|
+
return normalized;
|
|
1373
|
+
});
|
|
1374
|
+
return { table, orderBy: columns, rows };
|
|
1375
|
+
}
|
|
1376
|
+
function checkpointDigests(checkpointsRoot) {
|
|
1377
|
+
const found = [];
|
|
1378
|
+
function walk(dir) {
|
|
1379
|
+
let entries;
|
|
1380
|
+
try {
|
|
1381
|
+
entries = (0, import_node_fs6.readdirSync)(dir, { withFileTypes: true });
|
|
1382
|
+
} catch (error) {
|
|
1383
|
+
if (error.code === "ENOENT") return;
|
|
1384
|
+
throw error;
|
|
1385
|
+
}
|
|
1386
|
+
for (const entry of entries) {
|
|
1387
|
+
const path = (0, import_node_path7.join)(dir, entry.name);
|
|
1388
|
+
if (entry.isDirectory()) {
|
|
1389
|
+
walk(path);
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
found.push({
|
|
1393
|
+
path: (0, import_node_path7.relative)(checkpointsRoot, path).split(import_node_path7.sep).join("/"),
|
|
1394
|
+
digest: (0, import_agent_qa_schema7.sha256hex)((0, import_node_fs6.readFileSync)(path, "utf8"))
|
|
1395
|
+
});
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1398
|
+
walk(checkpointsRoot);
|
|
1399
|
+
return found.sort((left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0);
|
|
1400
|
+
}
|
|
1401
|
+
function createLogicalDump(input) {
|
|
1402
|
+
return {
|
|
1403
|
+
schemaVersion: input.schemaVersion,
|
|
1404
|
+
tables: listStoreTables(input.db).map((table) => selectOrderedRows(input.db, table)),
|
|
1405
|
+
checkpoints: checkpointDigests(input.checkpointsRoot)
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
function canonicalLogicalDump(dump) {
|
|
1409
|
+
return (0, import_agent_qa_schema7.canonicalJson)(dump);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// src/store/sqlite-job-store.ts
|
|
1413
|
+
var AGENT_JOB_STORE_USER_VERSION = 2;
|
|
1414
|
+
var AGENT_JOB_STORE_BUSY_TIMEOUT_MS = 2e3;
|
|
1415
|
+
var AGENT_JOB_STORE_DB_FILE = "agent-jobs.db";
|
|
1416
|
+
var SCHEMA_STATEMENTS = [
|
|
1417
|
+
`CREATE TABLE IF NOT EXISTS jobs (
|
|
1418
|
+
id TEXT PRIMARY KEY,
|
|
1419
|
+
kind TEXT NOT NULL,
|
|
1420
|
+
idempotency_key TEXT NOT NULL,
|
|
1421
|
+
status TEXT NOT NULL,
|
|
1422
|
+
summary TEXT NOT NULL,
|
|
1423
|
+
payload TEXT NOT NULL,
|
|
1424
|
+
attempts INTEGER NOT NULL,
|
|
1425
|
+
claimed_at TEXT,
|
|
1426
|
+
created_at TEXT NOT NULL,
|
|
1427
|
+
updated_at TEXT NOT NULL,
|
|
1428
|
+
-- The disposition the engine DECIDED, recorded so a failed job says why it failed.
|
|
1429
|
+
-- Nullable because most rows never fail; written only by recordFailure.
|
|
1430
|
+
last_failure_kind TEXT,
|
|
1431
|
+
last_failure_reason TEXT,
|
|
1432
|
+
-- When somebody asked this job to stop (D-19). A column rather than a status, for the
|
|
1433
|
+
-- same reason the claim is one: the job status vocabulary describes lifecycle stages,
|
|
1434
|
+
-- and "still queued, but asked to stop" is a fact about a request, not a stage.
|
|
1435
|
+
-- Appended LAST deliberately - a column inserted in the middle would move every
|
|
1436
|
+
-- positional read of this table for no gain.
|
|
1437
|
+
cancel_requested_at TEXT,
|
|
1438
|
+
-- The instant this row was FIRST claimed, kept after the lease is released (425). The
|
|
1439
|
+
-- claim column answers who holds the row now; no column answered how long the work took.
|
|
1440
|
+
-- Written once by the claim, cleared only by the operator requeue. Appended last, as above.
|
|
1441
|
+
started_at TEXT,
|
|
1442
|
+
-- 444.1 D-23: a retrying row is not claimable before this instant. Appended last, as above.
|
|
1443
|
+
not_before TEXT
|
|
1444
|
+
)`,
|
|
1445
|
+
// D-08's dedupe is THIS line. Making it a store-level constraint rather than a check the
|
|
1446
|
+
// caller performs is what turns "two enqueues of the same work produce one job" from a
|
|
1447
|
+
// convention into something no caller, in any process, can get wrong.
|
|
1448
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS jobs_kind_idempotency_key
|
|
1449
|
+
ON jobs (kind, idempotency_key)`,
|
|
1450
|
+
`CREATE TABLE IF NOT EXISTS job_store_fence (
|
|
1451
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
1452
|
+
epoch INTEGER NOT NULL
|
|
1453
|
+
)`,
|
|
1454
|
+
// The row is the authoritative checkpoint. The file remains a durable projection for
|
|
1455
|
+
// cross-process observability, but recovery accepts its bytes only when their digest agrees
|
|
1456
|
+
// with the latest fenced row for this job/checkpoint. Attempt stays in the key so retries
|
|
1457
|
+
// preserve their recorded history while a same-attempt takeover replaces only that attempt.
|
|
1458
|
+
`CREATE TABLE IF NOT EXISTS job_checkpoints (
|
|
1459
|
+
job_id TEXT NOT NULL,
|
|
1460
|
+
checkpoint_id TEXT NOT NULL,
|
|
1461
|
+
attempt INTEGER NOT NULL,
|
|
1462
|
+
payload_json TEXT NOT NULL,
|
|
1463
|
+
payload_digest TEXT NOT NULL,
|
|
1464
|
+
epoch INTEGER NOT NULL,
|
|
1465
|
+
PRIMARY KEY (job_id, attempt, checkpoint_id)
|
|
1466
|
+
)`,
|
|
1467
|
+
// 388 D-09: the chain-terminal record, beside the jobs it summarizes rather than in a
|
|
1468
|
+
// second durable medium. `created_at` is written from the injected clock like every other
|
|
1469
|
+
// timestamp in this file — a SQL-side default would leak host time into the D-03 dump and
|
|
1470
|
+
// make two identical histories compare unequal.
|
|
1471
|
+
`CREATE TABLE IF NOT EXISTS notifications (
|
|
1472
|
+
run_id TEXT NOT NULL,
|
|
1473
|
+
payload TEXT NOT NULL,
|
|
1474
|
+
created_at TEXT NOT NULL
|
|
1475
|
+
)`,
|
|
1476
|
+
// D-10's dedupe is THIS line, and it is the same idiom as the jobs index above for the same
|
|
1477
|
+
// reason: a crash-replayed chain terminal re-attempts the insert, and "one notification per
|
|
1478
|
+
// run, ever" has to be something no caller — in this process or the next — can get wrong.
|
|
1479
|
+
`CREATE UNIQUE INDEX IF NOT EXISTS notifications_run_id
|
|
1480
|
+
ON notifications (run_id)`
|
|
1481
|
+
];
|
|
1482
|
+
var TERMINAL_JOB_STATUSES = ["done", "failed", "cancelled"];
|
|
1483
|
+
var UNREADABLE_KIND_CONCLUSION = "unreadable_kind";
|
|
1484
|
+
var QUARANTINED_UNREADABLE_KIND_REASON = "agent job quarantined at claim: this build does not read the kind recorded for this row, so the row was landed permanently without being parsed. The original kind, id and payload are preserved in the row for inspection.";
|
|
1485
|
+
var QUARANTINE_SCAN_BOUND = 1e3;
|
|
1486
|
+
function isUnreadableKindRow(row) {
|
|
1487
|
+
return !(0, import_agent_qa_schema8.isJobKind)(String(row.kind));
|
|
1488
|
+
}
|
|
1489
|
+
function quarantineConclusionOf(row) {
|
|
1490
|
+
if (!isUnreadableKindRow(row)) return void 0;
|
|
1491
|
+
if (String(row.status) !== "failed") return void 0;
|
|
1492
|
+
if (String(row.last_failure_kind) !== "permanent") return void 0;
|
|
1493
|
+
if (String(row.last_failure_reason) !== QUARANTINED_UNREADABLE_KIND_REASON) return void 0;
|
|
1494
|
+
return UNREADABLE_KIND_CONCLUSION;
|
|
1495
|
+
}
|
|
1496
|
+
function runIdOfPayloadColumn(payload) {
|
|
1497
|
+
const record = JSON.parse(String(payload));
|
|
1498
|
+
if (typeof record !== "object" || record === null || Array.isArray(record)) return void 0;
|
|
1499
|
+
const runId = record.runId;
|
|
1500
|
+
return typeof runId === "string" && runId !== "" ? runId : void 0;
|
|
1501
|
+
}
|
|
1502
|
+
function toNumber(value) {
|
|
1503
|
+
return typeof value === "bigint" ? Number(value) : Number(value);
|
|
1504
|
+
}
|
|
1505
|
+
function createSqliteJobStore(options) {
|
|
1506
|
+
const { clock, checkpoints, hooks } = options;
|
|
1507
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path8.dirname)(options.path), { recursive: true });
|
|
1508
|
+
const db = new import_node_sqlite2.DatabaseSync(options.path, {
|
|
1509
|
+
timeout: options.timeoutMs ?? AGENT_JOB_STORE_BUSY_TIMEOUT_MS
|
|
1510
|
+
});
|
|
1511
|
+
db.exec("PRAGMA journal_mode=WAL");
|
|
1512
|
+
const stampedVersion = toNumber(db.prepare("PRAGMA user_version").get()?.user_version);
|
|
1513
|
+
if (stampedVersion !== 0 && stampedVersion !== AGENT_JOB_STORE_USER_VERSION) {
|
|
1514
|
+
db.close();
|
|
1515
|
+
throw new JobStoreSchemaVersionError(
|
|
1516
|
+
options.path,
|
|
1517
|
+
AGENT_JOB_STORE_USER_VERSION,
|
|
1518
|
+
stampedVersion
|
|
1519
|
+
);
|
|
1520
|
+
}
|
|
1521
|
+
for (const statement of SCHEMA_STATEMENTS) db.exec(statement);
|
|
1522
|
+
if (stampedVersion === 0) {
|
|
1523
|
+
db.exec(`PRAGMA user_version=${AGENT_JOB_STORE_USER_VERSION}`);
|
|
1524
|
+
}
|
|
1525
|
+
const insertJob = db.prepare(
|
|
1526
|
+
`INSERT INTO jobs
|
|
1527
|
+
(id, kind, idempotency_key, status, summary, payload, attempts, claimed_at,
|
|
1528
|
+
created_at, updated_at, last_failure_kind, last_failure_reason,
|
|
1529
|
+
cancel_requested_at, started_at, not_before)
|
|
1530
|
+
VALUES (?, ?, ?, 'queued', ?, ?, 0, NULL, ?, ?, NULL, NULL, NULL, NULL, NULL)
|
|
1531
|
+
ON CONFLICT (kind, idempotency_key) DO NOTHING`
|
|
1532
|
+
);
|
|
1533
|
+
const selectByKey = db.prepare("SELECT * FROM jobs WHERE kind = ? AND idempotency_key = ?");
|
|
1534
|
+
const selectById = db.prepare("SELECT * FROM jobs WHERE id = ?");
|
|
1535
|
+
const selectClaimable = db.prepare(
|
|
1536
|
+
`SELECT * FROM jobs
|
|
1537
|
+
WHERE status = 'queued' AND claimed_at IS NULL
|
|
1538
|
+
AND (not_before IS NULL OR not_before <= ?)
|
|
1539
|
+
ORDER BY created_at ASC, id ASC
|
|
1540
|
+
LIMIT 1`
|
|
1541
|
+
);
|
|
1542
|
+
const selectClaimableExcluding = /* @__PURE__ */ new Map();
|
|
1543
|
+
function claimableStatementFor(excludedCount) {
|
|
1544
|
+
const cached = selectClaimableExcluding.get(excludedCount);
|
|
1545
|
+
if (cached !== void 0) return cached;
|
|
1546
|
+
const placeholders = Array.from({ length: excludedCount }, () => "?").join(", ");
|
|
1547
|
+
const prepared = db.prepare(
|
|
1548
|
+
`SELECT * FROM jobs
|
|
1549
|
+
WHERE status = 'queued' AND claimed_at IS NULL
|
|
1550
|
+
AND (not_before IS NULL OR not_before <= ?) AND kind NOT IN (${placeholders})
|
|
1551
|
+
ORDER BY created_at ASC, id ASC
|
|
1552
|
+
LIMIT 1`
|
|
1553
|
+
);
|
|
1554
|
+
selectClaimableExcluding.set(excludedCount, prepared);
|
|
1555
|
+
return prepared;
|
|
1556
|
+
}
|
|
1557
|
+
const markClaimed = db.prepare(
|
|
1558
|
+
`UPDATE jobs
|
|
1559
|
+
SET claimed_at = ?, started_at = COALESCE(started_at, ?), attempts = attempts + 1,
|
|
1560
|
+
updated_at = ?
|
|
1561
|
+
WHERE id = ?`
|
|
1562
|
+
);
|
|
1563
|
+
const markDone = db.prepare(
|
|
1564
|
+
"UPDATE jobs SET status = 'done', claimed_at = NULL, updated_at = ? WHERE id = ?"
|
|
1565
|
+
);
|
|
1566
|
+
const markQuarantined = db.prepare(
|
|
1567
|
+
`UPDATE jobs
|
|
1568
|
+
SET status = 'failed', claimed_at = NULL, last_failure_kind = 'permanent',
|
|
1569
|
+
last_failure_reason = ?, updated_at = ?
|
|
1570
|
+
WHERE id = ?`
|
|
1571
|
+
);
|
|
1572
|
+
const touchJob = db.prepare("UPDATE jobs SET updated_at = ? WHERE id = ?");
|
|
1573
|
+
const markRetryable = db.prepare(
|
|
1574
|
+
`UPDATE jobs
|
|
1575
|
+
SET claimed_at = NULL, last_failure_kind = 'retry', last_failure_reason = ?,
|
|
1576
|
+
updated_at = ?, not_before = ?
|
|
1577
|
+
WHERE id = ?`
|
|
1578
|
+
);
|
|
1579
|
+
const markInterrupted = db.prepare(
|
|
1580
|
+
`UPDATE jobs
|
|
1581
|
+
SET claimed_at = NULL, attempts = MAX(attempts - 1, 0), updated_at = ?
|
|
1582
|
+
WHERE id = ?`
|
|
1583
|
+
);
|
|
1584
|
+
const markFailed = db.prepare(
|
|
1585
|
+
`UPDATE jobs
|
|
1586
|
+
SET status = 'failed', claimed_at = NULL, last_failure_kind = 'permanent',
|
|
1587
|
+
last_failure_reason = ?, updated_at = ?
|
|
1588
|
+
WHERE id = ?`
|
|
1589
|
+
);
|
|
1590
|
+
const markCancelledNow = db.prepare(
|
|
1591
|
+
`UPDATE jobs
|
|
1592
|
+
SET status = 'cancelled', claimed_at = NULL, cancel_requested_at = ?, updated_at = ?
|
|
1593
|
+
WHERE id = ?`
|
|
1594
|
+
);
|
|
1595
|
+
const markCancelRequested = db.prepare(
|
|
1596
|
+
"UPDATE jobs SET cancel_requested_at = ?, updated_at = ? WHERE id = ?"
|
|
1597
|
+
);
|
|
1598
|
+
const markCancelHonoured = db.prepare(
|
|
1599
|
+
`UPDATE jobs
|
|
1600
|
+
SET status = 'cancelled', claimed_at = NULL, updated_at = ?
|
|
1601
|
+
WHERE id = ?`
|
|
1602
|
+
);
|
|
1603
|
+
const markRequeued = db.prepare(
|
|
1604
|
+
`UPDATE jobs
|
|
1605
|
+
SET status = 'queued', claimed_at = NULL, started_at = NULL, attempts = 0,
|
|
1606
|
+
last_failure_kind = NULL, last_failure_reason = NULL, cancel_requested_at = NULL,
|
|
1607
|
+
not_before = NULL, updated_at = ?
|
|
1608
|
+
WHERE id = ?`
|
|
1609
|
+
);
|
|
1610
|
+
const selectCheckpointIdsForJob = db.prepare(
|
|
1611
|
+
"SELECT DISTINCT checkpoint_id FROM job_checkpoints WHERE job_id = ?"
|
|
1612
|
+
);
|
|
1613
|
+
const deleteCheckpointsForJob = db.prepare("DELETE FROM job_checkpoints WHERE job_id = ?");
|
|
1614
|
+
const selectFence = db.prepare("SELECT epoch FROM job_store_fence WHERE id = 1");
|
|
1615
|
+
const upsertFence = db.prepare(
|
|
1616
|
+
`INSERT INTO job_store_fence (id, epoch) VALUES (1, ?)
|
|
1617
|
+
ON CONFLICT (id) DO UPDATE SET epoch = excluded.epoch`
|
|
1618
|
+
);
|
|
1619
|
+
const upsertCheckpoint = db.prepare(
|
|
1620
|
+
`INSERT INTO job_checkpoints
|
|
1621
|
+
(job_id, checkpoint_id, attempt, payload_json, payload_digest, epoch)
|
|
1622
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
1623
|
+
ON CONFLICT (job_id, attempt, checkpoint_id) DO UPDATE SET
|
|
1624
|
+
payload_json = excluded.payload_json,
|
|
1625
|
+
payload_digest = excluded.payload_digest,
|
|
1626
|
+
epoch = excluded.epoch`
|
|
1627
|
+
);
|
|
1628
|
+
const selectLatestCheckpoint = db.prepare(
|
|
1629
|
+
`SELECT payload_json, payload_digest, attempt, epoch
|
|
1630
|
+
FROM job_checkpoints
|
|
1631
|
+
WHERE job_id = ? AND checkpoint_id = ?
|
|
1632
|
+
ORDER BY attempt DESC
|
|
1633
|
+
LIMIT 1`
|
|
1634
|
+
);
|
|
1635
|
+
const insertNotification = db.prepare(
|
|
1636
|
+
`INSERT INTO notifications (run_id, payload, created_at) VALUES (?, ?, ?)
|
|
1637
|
+
ON CONFLICT (run_id) DO NOTHING`
|
|
1638
|
+
);
|
|
1639
|
+
const selectJobStatus = db.prepare("SELECT status FROM jobs WHERE id = ?");
|
|
1640
|
+
const selectNotifications = db.prepare(
|
|
1641
|
+
"SELECT run_id, payload, created_at FROM notifications ORDER BY run_id ASC"
|
|
1642
|
+
);
|
|
1643
|
+
const deleteNotificationForRun = db.prepare("DELETE FROM notifications WHERE run_id = ?");
|
|
1644
|
+
function inTransaction(body) {
|
|
1645
|
+
db.exec("BEGIN IMMEDIATE");
|
|
1646
|
+
try {
|
|
1647
|
+
const result = body();
|
|
1648
|
+
db.exec("COMMIT");
|
|
1649
|
+
return result;
|
|
1650
|
+
} catch (error) {
|
|
1651
|
+
if (db.isTransaction) db.exec("ROLLBACK");
|
|
1652
|
+
throw error;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
function currentEpoch() {
|
|
1656
|
+
const row = selectFence.get();
|
|
1657
|
+
return row === void 0 ? 0 : toNumber(row.epoch);
|
|
1658
|
+
}
|
|
1659
|
+
function requireFence(fence) {
|
|
1660
|
+
const observed = currentEpoch();
|
|
1661
|
+
if (observed !== fence.epoch) {
|
|
1662
|
+
throw new import_agent_qa_agent3.StaleJobStoreEpochError(fence.epoch, observed);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
function rowToJob(row) {
|
|
1666
|
+
if (row === void 0) {
|
|
1667
|
+
throw new UnknownJobRowError("<missing>");
|
|
1668
|
+
}
|
|
1669
|
+
return import_agent_qa_schema8.jobSchema.parse({
|
|
1670
|
+
id: String(row.id),
|
|
1671
|
+
kind: String(row.kind),
|
|
1672
|
+
status: String(row.status),
|
|
1673
|
+
summary: String(row.summary),
|
|
1674
|
+
attempts: toNumber(row.attempts),
|
|
1675
|
+
createdAt: String(row.created_at),
|
|
1676
|
+
updatedAt: String(row.updated_at)
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
function requireRow(jobId) {
|
|
1680
|
+
const row = selectById.get(jobId);
|
|
1681
|
+
if (row === void 0) {
|
|
1682
|
+
throw new UnknownJobRowError(jobId);
|
|
1683
|
+
}
|
|
1684
|
+
return row;
|
|
1685
|
+
}
|
|
1686
|
+
function insertOrGet(request, now) {
|
|
1687
|
+
if (request.idempotencyKey.trim() === "") {
|
|
1688
|
+
throw new import_agent_qa_agent3.MissingIdempotencyKeyError(request.kind);
|
|
1689
|
+
}
|
|
1690
|
+
const changes = insertJob.run(
|
|
1691
|
+
String(request.id),
|
|
1692
|
+
request.kind,
|
|
1693
|
+
request.idempotencyKey,
|
|
1694
|
+
request.summary,
|
|
1695
|
+
JSON.stringify(request.payload ?? null),
|
|
1696
|
+
now,
|
|
1697
|
+
now
|
|
1698
|
+
).changes;
|
|
1699
|
+
return {
|
|
1700
|
+
job: rowToJob(selectByKey.get(request.kind, request.idempotencyKey)),
|
|
1701
|
+
deduped: toNumber(changes) === 0
|
|
1702
|
+
};
|
|
1703
|
+
}
|
|
1704
|
+
function recordChainTerminal(chainTerminal, now) {
|
|
1705
|
+
let headExists = false;
|
|
1706
|
+
for (const candidate of chainTerminal.candidateJobIds) {
|
|
1707
|
+
const row = selectJobStatus.get(String(candidate));
|
|
1708
|
+
if (row === void 0) continue;
|
|
1709
|
+
headExists ||= String(candidate) === String(chainTerminal.candidateJobIds[0]);
|
|
1710
|
+
if (!TERMINAL_JOB_STATUSES.includes(String(row.status))) return;
|
|
1711
|
+
}
|
|
1712
|
+
if (!headExists) return;
|
|
1713
|
+
insertNotification.run(chainTerminal.dedupeKey, chainTerminal.payloadJson, now);
|
|
1714
|
+
}
|
|
1715
|
+
function dumpRow(row) {
|
|
1716
|
+
const conclusionKind = quarantineConclusionOf(row);
|
|
1717
|
+
return {
|
|
1718
|
+
id: String(row.id),
|
|
1719
|
+
kind: String(row.kind),
|
|
1720
|
+
idempotencyKey: String(row.idempotency_key),
|
|
1721
|
+
status: String(row.status),
|
|
1722
|
+
summary: String(row.summary),
|
|
1723
|
+
payload: JSON.parse(String(row.payload)),
|
|
1724
|
+
attempts: toNumber(row.attempts),
|
|
1725
|
+
claimed: row.claimed_at !== null,
|
|
1726
|
+
cancelRequested: row.cancel_requested_at !== null,
|
|
1727
|
+
// 422 D-16: OMITTED rather than `undefined` when there is nothing to say, so a reader that
|
|
1728
|
+
// serializes this row does not publish an empty conclusion key for every ordinary job.
|
|
1729
|
+
...conclusionKind === void 0 ? {} : { conclusionKind },
|
|
1730
|
+
createdAt: String(row.created_at),
|
|
1731
|
+
updatedAt: String(row.updated_at)
|
|
1732
|
+
};
|
|
1733
|
+
}
|
|
1734
|
+
return {
|
|
1735
|
+
close() {
|
|
1736
|
+
db.close();
|
|
1737
|
+
},
|
|
1738
|
+
async adoptEpoch(epoch) {
|
|
1739
|
+
return inTransaction(() => {
|
|
1740
|
+
const observed = currentEpoch();
|
|
1741
|
+
if (epoch < observed) {
|
|
1742
|
+
throw new import_agent_qa_agent3.StaleJobStoreEpochError(epoch, observed);
|
|
1743
|
+
}
|
|
1744
|
+
upsertFence.run(epoch);
|
|
1745
|
+
return { epoch };
|
|
1746
|
+
});
|
|
1747
|
+
},
|
|
1748
|
+
async enqueue(request) {
|
|
1749
|
+
const now = clock.now();
|
|
1750
|
+
return inTransaction(() => insertOrGet(request, now));
|
|
1751
|
+
},
|
|
1752
|
+
async claimNext(fence, options2) {
|
|
1753
|
+
const now = clock.now();
|
|
1754
|
+
const excludeKinds = options2?.excludeKinds ?? [];
|
|
1755
|
+
return inTransaction(() => {
|
|
1756
|
+
requireFence(fence);
|
|
1757
|
+
for (let scanned = 0; scanned <= QUARANTINE_SCAN_BOUND; scanned += 1) {
|
|
1758
|
+
const row = excludeKinds.length === 0 ? selectClaimable.get(now) : claimableStatementFor(excludeKinds.length).get(now, ...excludeKinds);
|
|
1759
|
+
if (row === void 0) {
|
|
1760
|
+
return { claimed: false };
|
|
1761
|
+
}
|
|
1762
|
+
markClaimed.run(now, now, now, String(row.id));
|
|
1763
|
+
const claimed = requireRow(String(row.id));
|
|
1764
|
+
if (isUnreadableKindRow(claimed)) {
|
|
1765
|
+
markQuarantined.run(QUARANTINED_UNREADABLE_KIND_REASON, now, String(row.id));
|
|
1766
|
+
continue;
|
|
1767
|
+
}
|
|
1768
|
+
return {
|
|
1769
|
+
claimed: true,
|
|
1770
|
+
job: rowToJob(claimed),
|
|
1771
|
+
payload: JSON.parse(String(claimed.payload))
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
throw new Error(
|
|
1775
|
+
`agent job claim scanned ${QUARANTINE_SCAN_BOUND} rows without reaching a claimable or exhausted queue; the claimable set is not shrinking, so no row was claimed and the transaction is rolled back rather than reporting an empty queue`
|
|
1776
|
+
);
|
|
1777
|
+
});
|
|
1778
|
+
},
|
|
1779
|
+
async completeAndChain(jobId, next, fence, chainTerminal) {
|
|
1780
|
+
const now = clock.now();
|
|
1781
|
+
return inTransaction(() => {
|
|
1782
|
+
requireFence(fence);
|
|
1783
|
+
requireRow(String(jobId));
|
|
1784
|
+
markDone.run(now, String(jobId));
|
|
1785
|
+
const chained = next.map((request) => insertOrGet(request, now));
|
|
1786
|
+
if (chainTerminal !== void 0) recordChainTerminal(chainTerminal, now);
|
|
1787
|
+
return { job: rowToJob(requireRow(String(jobId))), chained };
|
|
1788
|
+
});
|
|
1789
|
+
},
|
|
1790
|
+
async failAndChain(jobId, disposition, next, fence, chainTerminal) {
|
|
1791
|
+
const now = clock.now();
|
|
1792
|
+
return inTransaction(() => {
|
|
1793
|
+
requireFence(fence);
|
|
1794
|
+
requireRow(String(jobId));
|
|
1795
|
+
markFailed.run(disposition.reason, now, String(jobId));
|
|
1796
|
+
const chained = next.map((request) => insertOrGet(request, now));
|
|
1797
|
+
if (chainTerminal !== void 0) recordChainTerminal(chainTerminal, now);
|
|
1798
|
+
return { job: rowToJob(requireRow(String(jobId))), chained };
|
|
1799
|
+
});
|
|
1800
|
+
},
|
|
1801
|
+
async recordFailure(jobId, disposition, fence) {
|
|
1802
|
+
const now = clock.now();
|
|
1803
|
+
inTransaction(() => {
|
|
1804
|
+
requireFence(fence);
|
|
1805
|
+
requireRow(String(jobId));
|
|
1806
|
+
if (disposition.kind === "retry") {
|
|
1807
|
+
const notBefore = disposition.retryAfterMs === void 0 ? null : new Date(Date.parse(now) + disposition.retryAfterMs).toISOString();
|
|
1808
|
+
markRetryable.run(disposition.reason, now, notBefore, String(jobId));
|
|
1809
|
+
} else {
|
|
1810
|
+
markFailed.run(disposition.reason, now, String(jobId));
|
|
1811
|
+
}
|
|
1812
|
+
});
|
|
1813
|
+
},
|
|
1814
|
+
async releaseInterrupted(jobId, fence) {
|
|
1815
|
+
const now = clock.now();
|
|
1816
|
+
inTransaction(() => {
|
|
1817
|
+
requireFence(fence);
|
|
1818
|
+
requireRow(String(jobId));
|
|
1819
|
+
markInterrupted.run(now, String(jobId));
|
|
1820
|
+
});
|
|
1821
|
+
},
|
|
1822
|
+
async requestCancel(jobId, fence) {
|
|
1823
|
+
const now = clock.now();
|
|
1824
|
+
return inTransaction(() => {
|
|
1825
|
+
requireFence(fence);
|
|
1826
|
+
const row = requireRow(String(jobId));
|
|
1827
|
+
const status = String(row.status);
|
|
1828
|
+
if (TERMINAL_JOB_STATUSES.includes(status)) {
|
|
1829
|
+
return { outcome: "already-terminal", status };
|
|
1830
|
+
}
|
|
1831
|
+
if (row.claimed_at !== null) {
|
|
1832
|
+
markCancelRequested.run(now, now, String(jobId));
|
|
1833
|
+
return { outcome: "cancel-requested" };
|
|
1834
|
+
}
|
|
1835
|
+
markCancelledNow.run(now, now, String(jobId));
|
|
1836
|
+
return { outcome: "cancelled" };
|
|
1837
|
+
});
|
|
1838
|
+
},
|
|
1839
|
+
async recordCancellation(jobId, fence) {
|
|
1840
|
+
const now = clock.now();
|
|
1841
|
+
inTransaction(() => {
|
|
1842
|
+
requireFence(fence);
|
|
1843
|
+
const row = requireRow(String(jobId));
|
|
1844
|
+
if (row.cancel_requested_at === null) {
|
|
1845
|
+
throw new import_agent_qa_agent3.JobCancellationNotRequestedError(jobId);
|
|
1846
|
+
}
|
|
1847
|
+
markCancelHonoured.run(now, String(jobId));
|
|
1848
|
+
});
|
|
1849
|
+
},
|
|
1850
|
+
// The 413-13 gap closure, and the checkpoint forget is its load-bearing half.
|
|
1851
|
+
//
|
|
1852
|
+
// Phase 413's pump composed with no post-run capabilities, so the kind registry selected
|
|
1853
|
+
// the STAND-IN analyze-run handler: nineteen rows recorded a `verdicts-authored`
|
|
1854
|
+
// checkpoint and reached `done` with nothing authored. Resetting the status alone would
|
|
1855
|
+
// not undo that, it would REPEAT it — `selectLatestCheckpoint` reads
|
|
1856
|
+
// `ORDER BY attempt DESC`, so the real handler's next attempt would resume on the
|
|
1857
|
+
// stand-in's recorded value, author nothing again, and report completion again. So the
|
|
1858
|
+
// rows go with the status, inside the same transaction, and the files follow the rows.
|
|
1859
|
+
async requeueTerminal(jobId, fence) {
|
|
1860
|
+
const now = clock.now();
|
|
1861
|
+
const decided = inTransaction(() => {
|
|
1862
|
+
requireFence(fence);
|
|
1863
|
+
const row = requireRow(String(jobId));
|
|
1864
|
+
const status = String(row.status);
|
|
1865
|
+
if (isUnreadableKindRow(row)) {
|
|
1866
|
+
throw new import_agent_qa_agent3.QuarantinedJobRequeueError(String(jobId), String(row.kind));
|
|
1867
|
+
}
|
|
1868
|
+
if (!TERMINAL_JOB_STATUSES.includes(status)) {
|
|
1869
|
+
return { outcome: "not-terminal", status };
|
|
1870
|
+
}
|
|
1871
|
+
const discard = selectCheckpointIdsForJob.all(String(jobId)).map((checkpoint) => String(checkpoint.checkpoint_id));
|
|
1872
|
+
deleteCheckpointsForJob.run(String(jobId));
|
|
1873
|
+
const requeuedRun = runIdOfPayloadColumn(row.payload);
|
|
1874
|
+
if (requeuedRun !== void 0) deleteNotificationForRun.run(requeuedRun);
|
|
1875
|
+
markRequeued.run(now, String(jobId));
|
|
1876
|
+
return { outcome: "requeued", discard };
|
|
1877
|
+
});
|
|
1878
|
+
if (decided.outcome === "requeued") {
|
|
1879
|
+
for (const checkpointId of decided.discard ?? []) {
|
|
1880
|
+
checkpoints.discard(String(jobId), checkpointId);
|
|
1881
|
+
}
|
|
1882
|
+
return { outcome: "requeued" };
|
|
1883
|
+
}
|
|
1884
|
+
return decided;
|
|
1885
|
+
},
|
|
1886
|
+
async publishCheckpoint(jobId, checkpointId, payload, fence) {
|
|
1887
|
+
const now = clock.now();
|
|
1888
|
+
const payloadJson = (0, import_agent_qa_schema8.canonicalJson)(payload);
|
|
1889
|
+
const payloadDigest = (0, import_agent_qa_schema8.sha256hex)(payloadJson);
|
|
1890
|
+
inTransaction(() => {
|
|
1891
|
+
requireFence(fence);
|
|
1892
|
+
requireRow(String(jobId));
|
|
1893
|
+
touchJob.run(now, String(jobId));
|
|
1894
|
+
upsertCheckpoint.run(
|
|
1895
|
+
String(jobId),
|
|
1896
|
+
checkpointId,
|
|
1897
|
+
payload.attempt,
|
|
1898
|
+
payloadJson,
|
|
1899
|
+
payloadDigest,
|
|
1900
|
+
fence.epoch
|
|
1901
|
+
);
|
|
1902
|
+
});
|
|
1903
|
+
if (hooks?.beforeCheckpointFilePublish !== void 0) {
|
|
1904
|
+
await hooks.beforeCheckpointFilePublish();
|
|
1905
|
+
}
|
|
1906
|
+
checkpoints.publish(String(jobId), checkpointId, payload);
|
|
1907
|
+
},
|
|
1908
|
+
async readCheckpoint(jobId, checkpointId) {
|
|
1909
|
+
const row = selectLatestCheckpoint.get(String(jobId), checkpointId);
|
|
1910
|
+
if (row === void 0) return { found: false };
|
|
1911
|
+
const authoritative = {
|
|
1912
|
+
found: true,
|
|
1913
|
+
payload: JSON.parse(String(row.payload_json))
|
|
1914
|
+
};
|
|
1915
|
+
try {
|
|
1916
|
+
const projection = checkpoints.read(String(jobId), checkpointId);
|
|
1917
|
+
if (!projection.found) return authoritative;
|
|
1918
|
+
const projectionDigest = (0, import_agent_qa_schema8.sha256hex)((0, import_agent_qa_schema8.canonicalJson)(projection.payload));
|
|
1919
|
+
return projectionDigest === String(row.payload_digest) ? projection : authoritative;
|
|
1920
|
+
} catch {
|
|
1921
|
+
return authoritative;
|
|
1922
|
+
}
|
|
1923
|
+
},
|
|
1924
|
+
async dumpState() {
|
|
1925
|
+
return {
|
|
1926
|
+
schemaVersion: AGENT_JOB_STORE_USER_VERSION,
|
|
1927
|
+
epoch: currentEpoch(),
|
|
1928
|
+
// The same ordered read the logical dump uses, so the typed projection and the
|
|
1929
|
+
// equality instrument cannot disagree about which rows exist or in what order.
|
|
1930
|
+
jobs: selectOrderedRows(db, "jobs").rows.map(dumpRow)
|
|
1931
|
+
};
|
|
1932
|
+
},
|
|
1933
|
+
async listNotifications() {
|
|
1934
|
+
return selectNotifications.all().map((row) => ({
|
|
1935
|
+
runId: String(row.run_id),
|
|
1936
|
+
payload: (0, import_agent_qa_schema8.guardRunNotification)(JSON.parse(String(row.payload))),
|
|
1937
|
+
createdAt: String(row.created_at)
|
|
1938
|
+
}));
|
|
1939
|
+
},
|
|
1940
|
+
async logicalDump() {
|
|
1941
|
+
return createLogicalDump({
|
|
1942
|
+
db,
|
|
1943
|
+
schemaVersion: AGENT_JOB_STORE_USER_VERSION,
|
|
1944
|
+
checkpointsRoot: checkpoints.checkpointsRoot
|
|
1945
|
+
});
|
|
1946
|
+
},
|
|
1947
|
+
async dumpLogicalState() {
|
|
1948
|
+
return canonicalLogicalDump(
|
|
1949
|
+
createLogicalDump({
|
|
1950
|
+
db,
|
|
1951
|
+
schemaVersion: AGENT_JOB_STORE_USER_VERSION,
|
|
1952
|
+
checkpointsRoot: checkpoints.checkpointsRoot
|
|
1953
|
+
})
|
|
1954
|
+
);
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
|
|
1959
|
+
// src/store/post-processing-reader.ts
|
|
1960
|
+
var CurationSnapshotReadError = class extends Error {
|
|
1961
|
+
name = "CurationSnapshotReadError";
|
|
1962
|
+
constructor() {
|
|
1963
|
+
super("The recorded curation checkpoint is invalid; its snapshot cannot be read.");
|
|
1964
|
+
}
|
|
1965
|
+
};
|
|
1966
|
+
function recordOf(value) {
|
|
1967
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1968
|
+
throw new CurationSnapshotReadError();
|
|
1969
|
+
}
|
|
1970
|
+
return value;
|
|
1971
|
+
}
|
|
1972
|
+
function readCurationSnapshot(db, root, ids, runId) {
|
|
1973
|
+
const checkpointId = "bundle-committed";
|
|
1974
|
+
const row = db.prepare(`SELECT payload_json, payload_digest, attempt FROM job_checkpoints
|
|
1975
|
+
WHERE job_id = ? AND checkpoint_id = ? ORDER BY attempt DESC LIMIT 1`).get(String(ids.curateMemory), checkpointId);
|
|
1976
|
+
if (row === void 0) return void 0;
|
|
1977
|
+
try {
|
|
1978
|
+
const authoritative = JSON.parse(String(row.payload_json));
|
|
1979
|
+
if ((0, import_agent_qa_schema9.sha256hex)((0, import_agent_qa_schema9.canonicalJson)(authoritative)) !== row.payload_digest) {
|
|
1980
|
+
throw new CurationSnapshotReadError();
|
|
1981
|
+
}
|
|
1982
|
+
let payload = authoritative;
|
|
1983
|
+
try {
|
|
1984
|
+
const cached = readJobCheckpointEnvelope({ root }, String(ids.curateMemory), checkpointId);
|
|
1985
|
+
if (cached.found && (0, import_agent_qa_schema9.sha256hex)((0, import_agent_qa_schema9.canonicalJson)(cached.envelope.payload)) === row.payload_digest) {
|
|
1986
|
+
payload = cached.envelope.payload;
|
|
1987
|
+
}
|
|
1988
|
+
} catch {
|
|
1989
|
+
}
|
|
1990
|
+
const checkpoint = recordOf(payload);
|
|
1991
|
+
if (checkpoint.jobId !== ids.curateMemory || checkpoint.checkpointId !== checkpointId || checkpoint.attempt !== Number(row.attempt) || !Number.isInteger(checkpoint.attempt) || Number(checkpoint.attempt) < 1 || typeof checkpoint.recordedAt !== "string" || checkpoint.recordedAt.length === 0) throw new CurationSnapshotReadError();
|
|
1992
|
+
const value = recordOf(checkpoint.value);
|
|
1993
|
+
if (value.curationSnapshot === void 0) return void 0;
|
|
1994
|
+
const job = db.prepare("SELECT kind, idempotency_key, payload, attempts FROM jobs WHERE id = ?").get(String(ids.curateMemory));
|
|
1995
|
+
if (job === void 0 || job.kind !== "curate-memory" || job.idempotency_key !== `${ids.analyzeRun}->curate-memory` || Number(job.attempts) < Number(checkpoint.attempt) || recordOf(JSON.parse(String(job.payload))).runId !== runId || value.runId !== runId) throw new CurationSnapshotReadError();
|
|
1996
|
+
return import_agent_qa_schema9.CurationSnapshotSchema.parse(value.curationSnapshot);
|
|
1997
|
+
} catch {
|
|
1998
|
+
throw new CurationSnapshotReadError();
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
function toNumber2(value) {
|
|
2002
|
+
return typeof value === "bigint" ? Number(value) : Number(value);
|
|
2003
|
+
}
|
|
2004
|
+
function toRow(row) {
|
|
2005
|
+
if (row === void 0) return void 0;
|
|
2006
|
+
const conclusionKind = !(0, import_agent_qa_schema9.isJobKind)(String(row.kind)) && row.status === "failed" && row.last_failure_kind === "permanent" && row.last_failure_reason === QUARANTINED_UNREADABLE_KIND_REASON ? "unreadable_kind" : void 0;
|
|
2007
|
+
return {
|
|
2008
|
+
status: String(row.status),
|
|
2009
|
+
claimed: row.claimed_at !== null,
|
|
2010
|
+
attempts: toNumber2(row.attempts),
|
|
2011
|
+
createdAt: String(row.created_at),
|
|
2012
|
+
updatedAt: String(row.updated_at),
|
|
2013
|
+
...row.claimed_at === null ? {} : { claimedAt: String(row.claimed_at) },
|
|
2014
|
+
...row.started_at === null ? {} : { startedAt: String(row.started_at) },
|
|
2015
|
+
...conclusionKind === void 0 ? {} : { conclusionKind },
|
|
2016
|
+
...row.last_failure_kind === null ? {} : { lastFailureKind: String(row.last_failure_kind) },
|
|
2017
|
+
...row.last_failure_reason === null ? {} : { lastFailureReason: String(row.last_failure_reason) }
|
|
2018
|
+
};
|
|
2019
|
+
}
|
|
2020
|
+
function createPostProcessingReader(options) {
|
|
2021
|
+
const path = (0, import_node_path9.join)(options.agentQaRoot, AGENT_JOB_STORE_DB_FILE);
|
|
2022
|
+
return {
|
|
2023
|
+
idSource: options.idSource,
|
|
2024
|
+
async readChain(ids, runId) {
|
|
2025
|
+
if (!(0, import_node_fs8.existsSync)(path)) return void 0;
|
|
2026
|
+
const db = new import_node_sqlite3.DatabaseSync(path, {
|
|
2027
|
+
readOnly: true,
|
|
2028
|
+
timeout: AGENT_JOB_STORE_BUSY_TIMEOUT_MS
|
|
2029
|
+
});
|
|
2030
|
+
try {
|
|
2031
|
+
const stamped = toNumber2(db.prepare("PRAGMA user_version").get()?.user_version);
|
|
2032
|
+
if (stamped !== 0 && stamped !== AGENT_JOB_STORE_USER_VERSION) {
|
|
2033
|
+
throw new JobStoreSchemaVersionError(path, AGENT_JOB_STORE_USER_VERSION, stamped);
|
|
2034
|
+
}
|
|
2035
|
+
const selectJob = db.prepare(`
|
|
2036
|
+
SELECT kind, status, attempts, created_at, claimed_at, started_at, updated_at,
|
|
2037
|
+
last_failure_kind, last_failure_reason
|
|
2038
|
+
FROM jobs WHERE id = ?
|
|
2039
|
+
`);
|
|
2040
|
+
const notificationRow = db.prepare("SELECT payload FROM notifications WHERE run_id = ?").get(runId);
|
|
2041
|
+
const analyzeRun = toRow(
|
|
2042
|
+
selectJob.get(String(ids.analyzeRun))
|
|
2043
|
+
);
|
|
2044
|
+
const curateMemory = toRow(
|
|
2045
|
+
selectJob.get(String(ids.curateMemory))
|
|
2046
|
+
);
|
|
2047
|
+
const proposeImprovements = toRow(
|
|
2048
|
+
selectJob.get(String(ids.proposeImprovements))
|
|
2049
|
+
);
|
|
2050
|
+
const notification = notificationRow === void 0 ? void 0 : (0, import_agent_qa_schema9.guardRunNotification)(JSON.parse(String(notificationRow.payload)));
|
|
2051
|
+
const curationSnapshot = readCurationSnapshot(db, options.agentQaRoot, ids, runId);
|
|
2052
|
+
return {
|
|
2053
|
+
...analyzeRun === void 0 ? {} : { analyzeRun },
|
|
2054
|
+
...curateMemory === void 0 ? {} : { curateMemory },
|
|
2055
|
+
...proposeImprovements === void 0 ? {} : { proposeImprovements },
|
|
2056
|
+
...notification === void 0 ? {} : { notification },
|
|
2057
|
+
...curationSnapshot === void 0 ? {} : { curationSnapshot }
|
|
2058
|
+
};
|
|
2059
|
+
} finally {
|
|
2060
|
+
db.close();
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
};
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// src/composition/compose-local-agent.ts
|
|
2067
|
+
var import_meta = {};
|
|
2068
|
+
var resolveHostRetrievalRung = import_agent_qa_knowledge_local.resolveRetrievalRung;
|
|
2069
|
+
var LOCAL_KNOWLEDGE_PROBES = Object.freeze({
|
|
2070
|
+
classifyIndexFace: import_agent_qa_knowledge_local.classifyIndexFace,
|
|
2071
|
+
foldRetrievalRung: import_agent_qa_knowledge_local.foldRetrievalRung,
|
|
2072
|
+
indexDbPath: import_agent_qa_knowledge_local.indexDbPath,
|
|
2073
|
+
readEmbedderWorkerRegistration: import_agent_qa_knowledge_local.readEmbedderWorkerRegistration,
|
|
2074
|
+
readWriterLease: import_agent_qa_knowledge_local.readWriterLease,
|
|
2075
|
+
resolveEmbedderCapability: import_agent_qa_knowledge_local.resolveEmbedderCapability
|
|
2076
|
+
});
|
|
2077
|
+
function resolveEmbedderWorkerEntryPath() {
|
|
2078
|
+
const requireBase = typeof __filename === "string" ? __filename : import_meta.url;
|
|
2079
|
+
const knowledgeEntry = (0, import_node_module.createRequire)(requireBase).resolve("@vostride/agent-qa-knowledge-local");
|
|
2080
|
+
return (0, import_node_path10.join)((0, import_node_path10.dirname)(knowledgeEntry), "embedder-worker", "worker-entry.js");
|
|
2081
|
+
}
|
|
2082
|
+
var LOCAL_DAEMON_CAPABILITIES = Object.freeze({
|
|
2083
|
+
acquireWriterLease: import_agent_qa_knowledge_local.acquireWriterLease,
|
|
2084
|
+
heartbeatLease: import_agent_qa_knowledge_local.heartbeatLease,
|
|
2085
|
+
releaseLease: import_agent_qa_knowledge_local.releaseLease,
|
|
2086
|
+
readWriterLease: import_agent_qa_knowledge_local.readWriterLease,
|
|
2087
|
+
deriveWriterLeaseFreshness: import_agent_qa_knowledge_local.deriveWriterLeaseFreshness,
|
|
2088
|
+
isLeaseStale: import_agent_qa_knowledge_local.isLeaseStale,
|
|
2089
|
+
countPendingCurationJobs: import_agent_qa_knowledge_local.countPendingCurationJobs,
|
|
2090
|
+
countRemainingCurationWork: import_agent_qa_knowledge_local.countRemainingCurationWork,
|
|
2091
|
+
inspectDrainableCurationWork: import_agent_qa_knowledge_local.inspectDrainableCurationWork,
|
|
2092
|
+
listQueuedCandidates: import_agent_qa_knowledge_local.listQueuedCandidates,
|
|
2093
|
+
acquireEmbedder: import_agent_qa_knowledge_local.acquireEmbedder,
|
|
2094
|
+
drainQueue: import_agent_qa_knowledge_local.drainQueue,
|
|
2095
|
+
runMaintenanceTail: import_agent_qa_knowledge_local.runMaintenanceTail,
|
|
2096
|
+
resolveEmbedderWorkerEntryPath,
|
|
2097
|
+
readEmbedderWorkerRegistration: import_agent_qa_knowledge_local.readEmbedderWorkerRegistration,
|
|
2098
|
+
embedderWorkerProcessIsAlive: import_agent_qa_knowledge_local.embedderWorkerProcessIsAlive,
|
|
2099
|
+
embedderWorkerSocketPath: import_agent_qa_knowledge_local.embedderWorkerSocketPath,
|
|
2100
|
+
probeEmbedderWorkerSocket: import_agent_qa_knowledge_local.probeEmbedderWorkerSocket,
|
|
2101
|
+
verifyEmbedderWorkerIncarnation: import_agent_qa_knowledge_local.verifyEmbedderWorkerIncarnation,
|
|
2102
|
+
resolveProcessIncarnation: import_agent_qa_knowledge_local.resolveProcessIncarnation,
|
|
2103
|
+
removeEmbedderWorkerRegistration: import_agent_qa_knowledge_local.removeEmbedderWorkerRegistration,
|
|
2104
|
+
resetEmbedderCapability: () => (0, import_agent_qa_knowledge_local.resetEmbedderCapability)(import_agent_qa_schema10.DEFAULT_EMBEDDING_PROFILE),
|
|
2105
|
+
pinLocalHeadlessExecutionContext: () => {
|
|
2106
|
+
(0, import_agent_qa_safety.setActiveExecutionContextResolver)(() => "local-headless");
|
|
2107
|
+
}
|
|
2108
|
+
});
|
|
2109
|
+
function receiptWriter(input) {
|
|
2110
|
+
return (0, import_agent_qa_knowledge_local.createLocalKnowledgeStore)({
|
|
2111
|
+
knowledgeRoot: input.knowledgeRoot,
|
|
2112
|
+
executionContext: input.executionContext,
|
|
2113
|
+
writerLeaseId: input.lease.leaseId,
|
|
2114
|
+
epoch: input.lease.epoch,
|
|
2115
|
+
...input.resolveTrustPriors === void 0 ? {} : { resolveTrustPriors: input.resolveTrustPriors },
|
|
2116
|
+
...input.resolvePromotionPosture === void 0 ? {} : { resolvePromotionPosture: input.resolvePromotionPosture },
|
|
2117
|
+
...input.sourceBuildOrdinal === void 0 ? {} : { sourceBuildOrdinal: input.sourceBuildOrdinal },
|
|
2118
|
+
...input.sourceRunId === void 0 ? {} : { sourceRunId: input.sourceRunId }
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
function recordGateReceipt(input) {
|
|
2122
|
+
return receiptWriter(input).recordGateReceipt(input.receipt);
|
|
2123
|
+
}
|
|
2124
|
+
function promoteWithReceipts(input) {
|
|
2125
|
+
return receiptWriter(input).promote(input.candidateId, input.receipts);
|
|
2126
|
+
}
|
|
2127
|
+
function readFeedbackRecord(input) {
|
|
2128
|
+
const matches = (0, import_agent_qa_knowledge_local.enumerateBundleDirs)(input.knowledgeRoot).flatMap((entry) => {
|
|
2129
|
+
const result = (0, import_agent_qa_knowledge_local.loadBundle)(entry.dir);
|
|
2130
|
+
return result.kind === "loaded" && result.bundle.manifest.id === input.recordId && (0, import_agent_qa_knowledge_local.verifyBundleIntegrity)(entry.dir).ok ? [result.bundle] : [];
|
|
2131
|
+
});
|
|
2132
|
+
if (matches.length !== 1) return void 0;
|
|
2133
|
+
const bundle = matches[0];
|
|
2134
|
+
const artifact = input.evidenceRef ?? bundle.artifactsIndex.artifacts[0];
|
|
2135
|
+
if (artifact === void 0) return void 0;
|
|
2136
|
+
return {
|
|
2137
|
+
recordId: input.recordId,
|
|
2138
|
+
kind: bundle.frontmatter.kind,
|
|
2139
|
+
...bundle.frontmatter.subtype === void 0 ? {} : { subtype: bundle.frontmatter.subtype },
|
|
2140
|
+
anchors: bundle.frontmatter.anchors.map((anchor) => anchor.raw),
|
|
2141
|
+
scope: bundle.frontmatter.scope,
|
|
2142
|
+
summary: bundle.summary,
|
|
2143
|
+
distilledEvidenceRef: { id: artifact.id, sha: artifact.sha },
|
|
2144
|
+
// This is the only admitted draft construction stage; user origin belongs on the delta.
|
|
2145
|
+
originStage: "run-curation"
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
var LOCAL_CURATION_CAPABILITIES = Object.freeze({
|
|
2149
|
+
readFeedbackRecord,
|
|
2150
|
+
appendMarkEvidence: import_agent_qa_knowledge_local.appendMarkEvidence,
|
|
2151
|
+
planMarkEvidence: import_reader5.planMarkEvidence,
|
|
2152
|
+
projectMarkEvidenceOutcome: import_reader5.projectMarkEvidenceOutcome,
|
|
2153
|
+
triageMark: import_agent_qa_knowledge_local.triageMark,
|
|
2154
|
+
servedIdsOn: import_agent_qa_knowledge_local.servedIdsOn,
|
|
2155
|
+
procedureReadOn: import_agent_qa_knowledge_local.procedureReadOn,
|
|
2156
|
+
applyFeedback: import_agent_qa_knowledge_local.applyFeedback,
|
|
2157
|
+
recordGateReceipt,
|
|
2158
|
+
promoteWithReceipts,
|
|
2159
|
+
openKnowledgeReader: import_agent_qa_knowledge_local.openKnowledgeReader,
|
|
2160
|
+
drainQueue: import_agent_qa_knowledge_local.drainQueue,
|
|
2161
|
+
foldRunOutcomes: import_agent_qa_knowledge_local.foldRunOutcomes,
|
|
2162
|
+
reconcileRunOutcomesProjection: import_agent_qa_knowledge_local.reconcileRunOutcomesProjection,
|
|
2163
|
+
enqueueCurationCandidateProjection: import_agent_qa_knowledge_local.enqueueCurationCandidateProjection,
|
|
2164
|
+
readCanonicalBundleAnchors: import_agent_qa_knowledge_local.readCanonicalBundleAnchors,
|
|
2165
|
+
readCanonicalBundleStateDigests: import_agent_qa_knowledge_local.readCanonicalBundleStateDigests,
|
|
2166
|
+
readCanaryPostureFacts: import_agent_qa_knowledge_local.canaryPostureFactsFor,
|
|
2167
|
+
readStandingGateReceipts: import_agent_qa_knowledge_local.standingReceiptDigestsFor,
|
|
2168
|
+
readLowerTrustShares: import_agent_qa_knowledge_local.lowerTrustSharesFor,
|
|
2169
|
+
runOutcomesProjectionVersion: import_agent_qa_knowledge_local.RUN_OUTCOMES_PROJECTION_VERSION,
|
|
2170
|
+
canaryOutcomeObservationId: import_reader5.canaryOutcomeObservationId,
|
|
2171
|
+
maintenance: Object.freeze({
|
|
2172
|
+
acquireWriterLease: import_agent_qa_knowledge_local.acquireWriterLease,
|
|
2173
|
+
refreshWriterLeaseAuthority: import_agent_qa_knowledge_local.refreshWriterLeaseAuthority,
|
|
2174
|
+
releaseLease: import_agent_qa_knowledge_local.releaseLease,
|
|
2175
|
+
rebuildIndex: import_agent_qa_knowledge_local.rebuildIndex,
|
|
2176
|
+
purgeRecord: import_agent_qa_knowledge_local.purgeRecord,
|
|
2177
|
+
resolvePurgeAffectedSet: import_agent_qa_knowledge_local.resolvePurgeAffectedSet,
|
|
2178
|
+
purgeAffectedSetFingerprint: import_agent_qa_knowledge_local.purgeAffectedSetFingerprint,
|
|
2179
|
+
rollbackCampaign: import_agent_qa_knowledge_local.rollbackCampaign,
|
|
2180
|
+
rollbackCurationJob: import_agent_qa_knowledge_local.rollbackCurationJob,
|
|
2181
|
+
resolveRollbackAffectedSet: import_agent_qa_knowledge_local.resolveRollbackAffectedSet,
|
|
2182
|
+
rollbackAffectedSetFingerprint: import_agent_qa_knowledge_local.rollbackAffectedSetFingerprint
|
|
2183
|
+
})
|
|
2184
|
+
});
|
|
2185
|
+
function isRecord(value) {
|
|
2186
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2187
|
+
}
|
|
2188
|
+
function isTargetName(value) {
|
|
2189
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
2190
|
+
}
|
|
2191
|
+
function recordedTargetsOf(artifact) {
|
|
2192
|
+
const source = isRecord(artifact) ? artifact.source : void 0;
|
|
2193
|
+
if (!isRecord(source)) return { status: "unknown" };
|
|
2194
|
+
if (source.kind === "test") {
|
|
2195
|
+
const target = isRecord(source.resolvedDefinition) ? source.resolvedDefinition.target : void 0;
|
|
2196
|
+
return isTargetName(target) ? { status: "recorded", names: [target] } : { status: "unknown" };
|
|
2197
|
+
}
|
|
2198
|
+
if (source.kind !== "suite" || !Array.isArray(source.members)) return { status: "unknown" };
|
|
2199
|
+
const names = /* @__PURE__ */ new Set();
|
|
2200
|
+
for (const member of source.members) {
|
|
2201
|
+
if (!isRecord(member)) return { status: "unknown" };
|
|
2202
|
+
if (member.target === void 0 || member.target === null) continue;
|
|
2203
|
+
if (!isTargetName(member.target)) return { status: "unknown" };
|
|
2204
|
+
names.add(member.target);
|
|
2205
|
+
}
|
|
2206
|
+
return names.size === 0 ? { status: "unknown" } : { status: "recorded", names: [...names].sort() };
|
|
2207
|
+
}
|
|
2208
|
+
function createRunsReader(agentQaRoot) {
|
|
2209
|
+
function enumerate() {
|
|
2210
|
+
const runsDir = (0, import_node_path10.join)(agentQaRoot, import_agent_qa_runner_local3.RUNS_SUBDIR);
|
|
2211
|
+
if (!(0, import_node_fs9.existsSync)(runsDir)) return [];
|
|
2212
|
+
const documents = [];
|
|
2213
|
+
for (const entry of (0, import_node_fs9.readdirSync)(runsDir, { withFileTypes: true })) {
|
|
2214
|
+
if (!entry.isDirectory()) continue;
|
|
2215
|
+
const handoffPath = (0, import_node_path10.join)(runsDir, entry.name, import_agent_qa_runner_local3.HANDOFF_FILE);
|
|
2216
|
+
if (!(0, import_node_fs9.existsSync)(handoffPath)) continue;
|
|
2217
|
+
try {
|
|
2218
|
+
documents.push((0, import_agent_qa_runner_local3.loadCompletedRunHandoff)(handoffPath));
|
|
2219
|
+
} catch {
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
return documents;
|
|
2224
|
+
}
|
|
2225
|
+
return {
|
|
2226
|
+
list() {
|
|
2227
|
+
return Promise.resolve(enumerate());
|
|
2228
|
+
},
|
|
2229
|
+
/**
|
|
2230
|
+
* Resolution runs over the ENUMERATED run documents and matches on the recorded `runId`,
|
|
2231
|
+
* never by joining the caller's string onto a path (T-384-08): a `runId` of `../../etc` can
|
|
2232
|
+
* therefore address nothing outside the runs directory, because it is never used to build a
|
|
2233
|
+
* path at all.
|
|
2234
|
+
*/
|
|
2235
|
+
get(runId) {
|
|
2236
|
+
return Promise.resolve(enumerate().find((document) => document.runId === runId));
|
|
2237
|
+
},
|
|
2238
|
+
/**
|
|
2239
|
+
* Deep replay of one run's `events.jsonl` through the SAME concrete store the v49 bus appends
|
|
2240
|
+
* with, so a replayed stream is byte-for-byte what the run emitted.
|
|
2241
|
+
*
|
|
2242
|
+
* Two cases are deliberately distinct. An absent or empty log is an ordinary empty replay —
|
|
2243
|
+
* nothing was recorded, so nothing was lost. A log whose earliest surviving seq is ABOVE the
|
|
2244
|
+
* requested resume point means events the caller asked for existed and are gone; that is
|
|
2245
|
+
* reported with the bus's own `ReplayIncompleteError` carrying the floor, never papered over
|
|
2246
|
+
* by returning the surviving suffix (D-07/D-08).
|
|
2247
|
+
*/
|
|
2248
|
+
readEvents(runId, fromSeq) {
|
|
2249
|
+
const recorded = [...new import_agent_qa_runner_local3.FsEventLog({ root: agentQaRoot, runId }).read(0)].sort((left, right) => left.seq - right.seq);
|
|
2250
|
+
if (recorded.length === 0) return Promise.resolve([]);
|
|
2251
|
+
const earliestAvailableSeq = recorded[0].seq;
|
|
2252
|
+
if (fromSeq < earliestAvailableSeq) {
|
|
2253
|
+
return Promise.reject(new import_agent_qa_runner4.ReplayIncompleteError(fromSeq, earliestAvailableSeq));
|
|
2254
|
+
}
|
|
2255
|
+
return Promise.resolve(recorded.filter((event) => event.seq >= fromSeq));
|
|
2256
|
+
},
|
|
2257
|
+
/**
|
|
2258
|
+
* One step's persisted evidence, read back out of the run's own handoff document
|
|
2259
|
+
* (387 KREV-02 / D-15).
|
|
2260
|
+
*
|
|
2261
|
+
* Through `loadCompletedRunHandoff` — the version guard — and NOT through a raw JSON read,
|
|
2262
|
+
* which is the load-bearing half. 387-02 gave the guard a present-implies-valid clause for
|
|
2263
|
+
* the retrieval trace, so a document whose trace is present but malformed is refused here
|
|
2264
|
+
* rather than reaching the why-verb looking well-formed.
|
|
2265
|
+
*
|
|
2266
|
+
* The ambiguity check is the never-guess law at this seam. `stepIndex` numbers steps per
|
|
2267
|
+
* MEMBER TEST, so a run with two tests has two step 0s; when the address does not name a
|
|
2268
|
+
* test and more than one candidate answers to it, this reports the candidates rather than
|
|
2269
|
+
* picking whichever test the document happened to list first. Picking would have produced a
|
|
2270
|
+
* confident, wrong answer to a question that was never asked — and a why-verb that does
|
|
2271
|
+
* that is worse than one that refuses, because its answer looks exactly like a right one.
|
|
2272
|
+
*/
|
|
2273
|
+
readStepEvidence(runId, address) {
|
|
2274
|
+
const document = enumerate().find((candidate) => candidate.runId === runId);
|
|
2275
|
+
if (document === void 0) return Promise.resolve({ kind: "run-not-recorded" });
|
|
2276
|
+
const candidates = document.tests.filter((test) => (address.testId === void 0 || test.testId === address.testId) && test.steps.some((step) => step.stepIndex === address.stepIndex));
|
|
2277
|
+
if (candidates.length === 0) return Promise.resolve({ kind: "step-not-found" });
|
|
2278
|
+
if (candidates.length > 1) {
|
|
2279
|
+
return Promise.resolve({
|
|
2280
|
+
kind: "ambiguous-step-address",
|
|
2281
|
+
// Sorted, so the refusal a surface prints is the same list twice running.
|
|
2282
|
+
testIds: candidates.map((test) => test.testId).sort()
|
|
2283
|
+
});
|
|
2284
|
+
}
|
|
2285
|
+
const evidence = candidates[0].steps.find((step) => step.stepIndex === address.stepIndex);
|
|
2286
|
+
return Promise.resolve(
|
|
2287
|
+
evidence === void 0 ? { kind: "step-not-found" } : { kind: "found", evidence }
|
|
2288
|
+
);
|
|
2289
|
+
},
|
|
2290
|
+
/**
|
|
2291
|
+
* Resolved over the ENUMERATED run directories (T-384-08, T-431-01): the caller's id is only
|
|
2292
|
+
* ever compared, never joined onto a path, and the directory must hold a completed handoff
|
|
2293
|
+
* recording that same id, so one run's snapshot can never answer for another. A snapshot that
|
|
2294
|
+
* is absent, unreadable or gone since `list()` answered is `unknown`, never an error.
|
|
2295
|
+
*/
|
|
2296
|
+
getRecordedTargets(runId) {
|
|
2297
|
+
const runsDir = (0, import_node_path10.join)(agentQaRoot, import_agent_qa_runner_local3.RUNS_SUBDIR);
|
|
2298
|
+
if (!(0, import_node_fs9.existsSync)(runsDir)) return Promise.resolve({ status: "unknown" });
|
|
2299
|
+
const entry = (0, import_node_fs9.readdirSync)(runsDir, { withFileTypes: true }).find((candidate) => candidate.isDirectory() && candidate.name === runId);
|
|
2300
|
+
if (entry === void 0) return Promise.resolve({ status: "unknown" });
|
|
2301
|
+
try {
|
|
2302
|
+
const runDir = (0, import_node_path10.join)(runsDir, entry.name);
|
|
2303
|
+
if ((0, import_agent_qa_runner_local3.loadCompletedRunHandoff)((0, import_node_path10.join)(runDir, import_agent_qa_runner_local3.HANDOFF_FILE)).runId !== runId) {
|
|
2304
|
+
return Promise.resolve({ status: "unknown" });
|
|
2305
|
+
}
|
|
2306
|
+
return Promise.resolve(
|
|
2307
|
+
recordedTargetsOf(JSON.parse((0, import_node_fs9.readFileSync)((0, import_node_path10.join)(runDir, import_agent_qa_runner_local3.ARTIFACT_FILE), "utf8")))
|
|
2308
|
+
);
|
|
2309
|
+
} catch {
|
|
2310
|
+
return Promise.resolve({ status: "unknown" });
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
};
|
|
2314
|
+
}
|
|
2315
|
+
function createKnowledgeReader(resolveKnowledgeRoot, sightMemoryEntities, resolvePromotionPosture, resolveTrustPriors) {
|
|
2316
|
+
return createKnowledgeReaderPort({
|
|
2317
|
+
resolveKnowledgeRoot,
|
|
2318
|
+
...resolveTrustPriors === void 0 ? {} : { resolveTrustPriors },
|
|
2319
|
+
...resolvePromotionPosture === void 0 ? {} : { resolvePromotionPosture },
|
|
2320
|
+
...sightMemoryEntities === void 0 ? {} : { sightMemoryEntities }
|
|
2321
|
+
});
|
|
2322
|
+
}
|
|
2323
|
+
function knowledgeChangeOutput(verb, operationId, result) {
|
|
2324
|
+
switch (result.outcome) {
|
|
2325
|
+
case "applied":
|
|
2326
|
+
return {
|
|
2327
|
+
outcome: "applied",
|
|
2328
|
+
verb,
|
|
2329
|
+
operationId,
|
|
2330
|
+
recordIds: [...result.recordIds],
|
|
2331
|
+
...result.forgetReceipt === void 0 ? {} : { forgetReceipt: result.forgetReceipt },
|
|
2332
|
+
...result.commitReceipt === void 0 ? {} : { commitReceipt: result.commitReceipt }
|
|
2333
|
+
};
|
|
2334
|
+
case "target-not-found":
|
|
2335
|
+
return { outcome: "target-not-found", verb, operationId, target: result.target };
|
|
2336
|
+
case "policy-materialize-refused":
|
|
2337
|
+
return { outcome: "refused", verb, operationId, reason: "policy-materialize-refused" };
|
|
2338
|
+
default:
|
|
2339
|
+
return result;
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
function createKnowledgeReview(input) {
|
|
2343
|
+
async function roots() {
|
|
2344
|
+
return { knowledgeRoot: await input.resolveKnowledgeRoot() };
|
|
2345
|
+
}
|
|
2346
|
+
let changeChain = Promise.resolve();
|
|
2347
|
+
function serialized(work) {
|
|
2348
|
+
const next = changeChain.then(work, work);
|
|
2349
|
+
changeChain = next.then(() => void 0, () => void 0);
|
|
2350
|
+
return next;
|
|
2351
|
+
}
|
|
2352
|
+
return {
|
|
2353
|
+
// 432-12 (D-04/D-05/D-09): one explicit change, applied in the call through the direct writer
|
|
2354
|
+
// (432-06). Nothing is filed: the change is committed under the lease, handed to a live
|
|
2355
|
+
// holder, or refused by type. A missing identity is minted here, once per call, and returned on
|
|
2356
|
+
// the receipt so the caller can retry the same change.
|
|
2357
|
+
change: (request) => serialized(async () => {
|
|
2358
|
+
const { knowledgeRoot } = await roots();
|
|
2359
|
+
const operationId = request.operationId ?? (0, import_node_crypto4.randomUUID)();
|
|
2360
|
+
const authority = input.resolveWriterAuthority?.();
|
|
2361
|
+
const result = await applyKnowledgeChange({
|
|
2362
|
+
agentQaRoot: input.agentQaRoot,
|
|
2363
|
+
knowledgeRoot,
|
|
2364
|
+
request: { verb: request.verb, input: request.input },
|
|
2365
|
+
operationId,
|
|
2366
|
+
// The op that made the change, stamped on Gate-3 receipts: `knowledge.accept`,
|
|
2367
|
+
// `knowledge.reject` or `knowledge.forget`, which is the verb's own op id.
|
|
2368
|
+
opId: `knowledge.${request.verb}`,
|
|
2369
|
+
executionContext: (input.resolveExecutionContext ?? import_agent_qa_safety.getActiveExecutionContext)(),
|
|
2370
|
+
...input.resolveTrustPriors === void 0 ? {} : { resolveTrustPriors: input.resolveTrustPriors },
|
|
2371
|
+
...input.resolvePromotionPosture === void 0 ? {} : { resolvePromotionPosture: input.resolvePromotionPosture },
|
|
2372
|
+
...input.knowledgeChangeDelegate === void 0 ? {} : { delegate: input.knowledgeChangeDelegate },
|
|
2373
|
+
...authority === void 0 ? {} : { writerAuthority: authority }
|
|
2374
|
+
});
|
|
2375
|
+
return knowledgeChangeOutput(request.verb, operationId, result);
|
|
2376
|
+
})
|
|
2377
|
+
// 2026-10-01 (432-17, D-05/D-22): `propose` and `apply` are deleted with the proposal inbox.
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
async function applyKnowledgeChange(input) {
|
|
2381
|
+
const posture = await input.resolvePromotionPosture?.();
|
|
2382
|
+
const ledger = (0, import_agent_qa_knowledge_local.openAddressingLedger)({
|
|
2383
|
+
path: (0, import_node_path10.join)(input.agentQaRoot, import_agent_qa_knowledge_local.ADDRESSING_LEDGER_DB_FILE)
|
|
2384
|
+
});
|
|
2385
|
+
try {
|
|
2386
|
+
return await (0, import_agent_qa_knowledge_local.applyExplicitReview)({
|
|
2387
|
+
agentQaRoot: input.agentQaRoot,
|
|
2388
|
+
knowledgeRoot: input.knowledgeRoot,
|
|
2389
|
+
request: input.request,
|
|
2390
|
+
operationId: input.operationId,
|
|
2391
|
+
opId: input.opId,
|
|
2392
|
+
executionContext: input.executionContext,
|
|
2393
|
+
resolveBundleShortId: (shortId) => ledger.lookupByShortId(shortId)?.machineId,
|
|
2394
|
+
...input.resolveTrustPriors === void 0 ? {} : { resolveTrustPriors: input.resolveTrustPriors },
|
|
2395
|
+
...posture === void 0 ? {} : { resolvePromotionPosture: posture },
|
|
2396
|
+
...input.delegate === void 0 ? {} : { delegate: input.delegate },
|
|
2397
|
+
...input.writerAuthority === void 0 ? {} : { writerAuthority: input.writerAuthority }
|
|
2398
|
+
});
|
|
2399
|
+
} finally {
|
|
2400
|
+
ledger.close();
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
function createEntityDirectory(input) {
|
|
2404
|
+
return {
|
|
2405
|
+
resolve(ref) {
|
|
2406
|
+
return withEntityResolutionDeps(input, (deps) => (0, import_agent_qa_knowledge_local.resolveEntityRef)(ref, deps));
|
|
2407
|
+
},
|
|
2408
|
+
get(ref) {
|
|
2409
|
+
return withEntityResolutionDeps(input, (deps) => (0, import_agent_qa_knowledge_local.getEntityDescriptor)(ref, deps));
|
|
2410
|
+
}
|
|
2411
|
+
};
|
|
2412
|
+
}
|
|
2413
|
+
function createMemorySightingHook(input) {
|
|
2414
|
+
return (bundleIds) => withEntityResolutionDeps(input, (deps) => {
|
|
2415
|
+
for (const bundleId of bundleIds) (0, import_agent_qa_knowledge_local.sightEntity)(bundleId, deps);
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
async function withEntityResolutionDeps(input, body) {
|
|
2419
|
+
const knowledgeRoot = await input.resolveKnowledgeRoot();
|
|
2420
|
+
const ledger = (0, import_agent_qa_knowledge_local.openAddressingLedger)({
|
|
2421
|
+
path: (0, import_node_path10.join)(input.agentQaRoot, import_agent_qa_knowledge_local.ADDRESSING_LEDGER_DB_FILE)
|
|
2422
|
+
});
|
|
2423
|
+
const entityIndex = (0, import_agent_qa_knowledge_local.openEntityIndex)({ path: (0, import_agent_qa_knowledge_local.entityIndexDbPath)(knowledgeRoot), knowledgeRoot });
|
|
2424
|
+
try {
|
|
2425
|
+
return await body({
|
|
2426
|
+
ledger,
|
|
2427
|
+
entityIndex,
|
|
2428
|
+
// One kind is wired by the tracer. 387-03 registers the remaining sources here and
|
|
2429
|
+
// neither the resolver nor the ops change — which is the property the tracer exists
|
|
2430
|
+
// to establish before anything expands.
|
|
2431
|
+
sources: [(0, import_agent_qa_knowledge_local.createMemorySightingSource)(() => knowledgeRoot)]
|
|
2432
|
+
});
|
|
2433
|
+
} finally {
|
|
2434
|
+
entityIndex.close();
|
|
2435
|
+
ledger.close();
|
|
2436
|
+
}
|
|
2437
|
+
}
|
|
2438
|
+
async function sweepEntityIndexProjection(input) {
|
|
2439
|
+
const knowledgeRoot = await input.resolveKnowledgeRoot();
|
|
2440
|
+
const ledger = (0, import_agent_qa_knowledge_local.openAddressingLedger)({
|
|
2441
|
+
path: (0, import_node_path10.join)(input.agentQaRoot, import_agent_qa_knowledge_local.ADDRESSING_LEDGER_DB_FILE)
|
|
2442
|
+
});
|
|
2443
|
+
const entityIndex = (0, import_agent_qa_knowledge_local.openEntityIndex)({ path: (0, import_agent_qa_knowledge_local.entityIndexDbPath)(knowledgeRoot), knowledgeRoot });
|
|
2444
|
+
try {
|
|
2445
|
+
const report = (0, import_agent_qa_knowledge_local.sweepEntityIndex)(entityIndex, (0, import_agent_qa_knowledge_local.describeAllEntities)({
|
|
2446
|
+
ledger,
|
|
2447
|
+
sources: [(0, import_agent_qa_knowledge_local.createMemorySightingSource)(() => knowledgeRoot)]
|
|
2448
|
+
}));
|
|
2449
|
+
let embedded = 0;
|
|
2450
|
+
const acquire = input.acquireEmbedderCapability ?? import_agent_qa_knowledge_local.acquireEmbedder;
|
|
2451
|
+
const capability = await acquire({
|
|
2452
|
+
preferredProfile: import_agent_qa_schema10.DEFAULT_EMBEDDING_PROFILE,
|
|
2453
|
+
allowDownload: false,
|
|
2454
|
+
agentQaRoot: input.agentQaRoot
|
|
2455
|
+
});
|
|
2456
|
+
if (capability.kind === "ready") {
|
|
2457
|
+
embedded = await (0, import_agent_qa_knowledge_local.embedPendingEntities)(
|
|
2458
|
+
entityIndex,
|
|
2459
|
+
capability.embedder,
|
|
2460
|
+
capability.profile
|
|
2461
|
+
);
|
|
2462
|
+
}
|
|
2463
|
+
return { ...report, embedded };
|
|
2464
|
+
} finally {
|
|
2465
|
+
entityIndex.close();
|
|
2466
|
+
ledger.close();
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
var RunProducedNoEventsError = class extends Error {
|
|
2470
|
+
constructor(runId) {
|
|
2471
|
+
super(
|
|
2472
|
+
`The host reported run ${runId}, but no run events were recorded for it. A run that started at all writes runStarted before anything else, so an empty event log means no run took place under that id.`
|
|
2473
|
+
);
|
|
2474
|
+
this.runId = runId;
|
|
2475
|
+
this.name = "RunProducedNoEventsError";
|
|
2476
|
+
}
|
|
2477
|
+
};
|
|
2478
|
+
function createLocalRunInvocation(input) {
|
|
2479
|
+
return {
|
|
2480
|
+
async start(request) {
|
|
2481
|
+
const started = await input.execution.execute(request);
|
|
2482
|
+
const { eventLog } = buildRunPorts({
|
|
2483
|
+
configDir: (0, import_node_path10.dirname)(input.agentQaRoot),
|
|
2484
|
+
runId: String(started.runId)
|
|
2485
|
+
});
|
|
2486
|
+
if (eventLog.read(0).length === 0) {
|
|
2487
|
+
throw new RunProducedNoEventsError(String(started.runId));
|
|
2488
|
+
}
|
|
2489
|
+
return started;
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
var EMPTY_FINGERPRINT_SNAPSHOT = {
|
|
2494
|
+
union: /* @__PURE__ */ new Set(),
|
|
2495
|
+
covered: [],
|
|
2496
|
+
elements: /* @__PURE__ */ new Map()
|
|
2497
|
+
};
|
|
2498
|
+
function draftIdentityKey(identity) {
|
|
2499
|
+
return `${identity.filePath}\0${identity.testName}`;
|
|
2500
|
+
}
|
|
2501
|
+
function readSuiteFingerprints(knowledgeRoot, product, draft) {
|
|
2502
|
+
const path = (0, import_agent_qa_knowledge_local.indexDbPath)(knowledgeRoot);
|
|
2503
|
+
if (!(0, import_node_fs9.existsSync)(path)) return EMPTY_FINGERPRINT_SNAPSHOT;
|
|
2504
|
+
const db = new import_node_sqlite4.DatabaseSync(path, { readOnly: true });
|
|
2505
|
+
try {
|
|
2506
|
+
const index = (0, import_agent_qa_knowledge_local.createSuiteFingerprintIndex)(db, { product });
|
|
2507
|
+
const draftKey = draftIdentityKey(draft);
|
|
2508
|
+
const covered = index.coveredTests().filter((identity) => draftIdentityKey(identity) !== draftKey);
|
|
2509
|
+
const elements = /* @__PURE__ */ new Map();
|
|
2510
|
+
const union = /* @__PURE__ */ new Set();
|
|
2511
|
+
for (const identity of covered) {
|
|
2512
|
+
const own = index.elementsForTest(identity.filePath, identity.testName);
|
|
2513
|
+
elements.set(draftIdentityKey(identity), own);
|
|
2514
|
+
for (const element of own) union.add(element);
|
|
2515
|
+
}
|
|
2516
|
+
return { union, covered, elements };
|
|
2517
|
+
} catch (error) {
|
|
2518
|
+
if (error instanceof Error && error.message.includes("no such table")) {
|
|
2519
|
+
return EMPTY_FINGERPRINT_SNAPSHOT;
|
|
2520
|
+
}
|
|
2521
|
+
throw error;
|
|
2522
|
+
} finally {
|
|
2523
|
+
db.close();
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
function createDraftEvaluation(input) {
|
|
2527
|
+
return {
|
|
2528
|
+
resolveDraft(draftPath) {
|
|
2529
|
+
return input.authoring.parseDraft(draftPath);
|
|
2530
|
+
},
|
|
2531
|
+
async openEvaluation(draft) {
|
|
2532
|
+
const knowledgeRoot = await input.knowledgeRoot();
|
|
2533
|
+
const draftIdentity = {
|
|
2534
|
+
filePath: draft.filePath,
|
|
2535
|
+
testName: draft.testName
|
|
2536
|
+
};
|
|
2537
|
+
const draftKey = draftIdentityKey(draftIdentity);
|
|
2538
|
+
const textCorpus = (await input.authoring.suiteTextCorpus()).filter(
|
|
2539
|
+
(entry) => draftIdentityKey(entry) !== draftKey
|
|
2540
|
+
);
|
|
2541
|
+
let observedProduct;
|
|
2542
|
+
let snapshot;
|
|
2543
|
+
const fingerprints = () => {
|
|
2544
|
+
if (snapshot !== void 0) return snapshot;
|
|
2545
|
+
snapshot = observedProduct === void 0 ? EMPTY_FINGERPRINT_SNAPSHOT : readSuiteFingerprints(knowledgeRoot, observedProduct, draftIdentity);
|
|
2546
|
+
return snapshot;
|
|
2547
|
+
};
|
|
2548
|
+
return {
|
|
2549
|
+
runs: {
|
|
2550
|
+
async start(request) {
|
|
2551
|
+
const acknowledgement = await input.startGateRun(request);
|
|
2552
|
+
observedProduct ??= (0, import_agent_qa_runner_local3.slugBranch)(acknowledgement.scope.key);
|
|
2553
|
+
return acknowledgement;
|
|
2554
|
+
},
|
|
2555
|
+
get: (runId) => input.readRun(runId)
|
|
2556
|
+
},
|
|
2557
|
+
fingerprints: {
|
|
2558
|
+
elementUnion: () => fingerprints().union,
|
|
2559
|
+
coveredTests: () => fingerprints().covered,
|
|
2560
|
+
elementsForTest: (filePath, testName) => fingerprints().elements.get(draftIdentityKey({ filePath, testName })) ?? []
|
|
2561
|
+
},
|
|
2562
|
+
textCorpus,
|
|
2563
|
+
// D-19: the gitignored per-draft receipt store, bound over `agentQaRoot` beside the
|
|
2564
|
+
// proposals inbox and for its reason — a receipt is machine-local working state under the
|
|
2565
|
+
// runtime root, not a committed artifact a pull request has any business showing.
|
|
2566
|
+
receipts: (0, import_agent_qa_knowledge_local.createDraftReceiptsStore)({ resolveAgentQaRoot: () => input.agentQaRoot }),
|
|
2567
|
+
// D-14's live arm (394-17, step c). `product` is the SAME `observedProduct` gate 3's index
|
|
2568
|
+
// is opened against — read off the first gate-run acknowledgment, never derived from
|
|
2569
|
+
// configuration — so the stability read and the suite index can never be asking about two
|
|
2570
|
+
// different applications. Before any run is acknowledged it is `undefined` and the
|
|
2571
|
+
// consultation is skipped; D-04 forbids short-circuiting, so a flip cannot be observed
|
|
2572
|
+
// until at least one run has been.
|
|
2573
|
+
suiteStability: {
|
|
2574
|
+
port: input.authoringFunnel,
|
|
2575
|
+
product: () => observedProduct
|
|
2576
|
+
}
|
|
2577
|
+
};
|
|
2578
|
+
}
|
|
2579
|
+
};
|
|
2580
|
+
}
|
|
2581
|
+
var AUTHORING_GATE_RUN_CALLER = Object.freeze({
|
|
2582
|
+
surface: "authoring",
|
|
2583
|
+
origin: "agent"
|
|
2584
|
+
});
|
|
2585
|
+
var AUTHORING_LOOP_VERIFY_CALLER = Object.freeze({
|
|
2586
|
+
surface: "authoring",
|
|
2587
|
+
origin: "agent"
|
|
2588
|
+
});
|
|
2589
|
+
var SYSTEM_CLOCK = Object.freeze({
|
|
2590
|
+
now: () => (/* @__PURE__ */ new Date()).toISOString()
|
|
2591
|
+
});
|
|
2592
|
+
var digestJobIdSource = {
|
|
2593
|
+
jobIdFor: (kind, idempotencyKey) => (0, import_agent_qa_schema10.asJobId)(`job_${(0, import_agent_qa_schema10.sha256hex16)(`${kind} ${idempotencyKey}`)}`)
|
|
2594
|
+
};
|
|
2595
|
+
var DIGEST_JOB_ID_SOURCE = Object.freeze(digestJobIdSource);
|
|
2596
|
+
var LocalAgentClosedError = class extends Error {
|
|
2597
|
+
constructor(agentQaRoot) {
|
|
2598
|
+
super(
|
|
2599
|
+
`The local agent composition for ${agentQaRoot} is closed. A closed composition cannot reopen its job store; create a new composition with fresh lifecycle ownership instead.`
|
|
2600
|
+
);
|
|
2601
|
+
this.agentQaRoot = agentQaRoot;
|
|
2602
|
+
}
|
|
2603
|
+
name = "LocalAgentClosedError";
|
|
2604
|
+
};
|
|
2605
|
+
var UNCLAIMABLE_BY_PROSE_POSTURE = Object.freeze({
|
|
2606
|
+
// Frozen to the same depth it is read at (WR-11). The shallow freeze every sibling table
|
|
2607
|
+
// here uses is not enough for this one: the VALUES are the arrays the queue claims
|
|
2608
|
+
// against, so an unfrozen inner array is a writable disclosure guard reachable from any
|
|
2609
|
+
// module that can import the table.
|
|
2610
|
+
"masker-active": Object.freeze([]),
|
|
2611
|
+
"no-masker": Object.freeze(["curate-memory"])
|
|
2612
|
+
});
|
|
2613
|
+
function readProjectedTestHistory(knowledgeRoot, query) {
|
|
2614
|
+
const path = (0, import_agent_qa_knowledge_local.indexDbPath)(knowledgeRoot);
|
|
2615
|
+
if (!(0, import_node_fs9.existsSync)(path)) return void 0;
|
|
2616
|
+
const db = new import_node_sqlite4.DatabaseSync(path, { readOnly: true });
|
|
2617
|
+
try {
|
|
2618
|
+
return (0, import_agent_qa_knowledge_local.readTestOutcomeHistory)(db, {
|
|
2619
|
+
product: query.product,
|
|
2620
|
+
filePath: query.filePath,
|
|
2621
|
+
testName: query.testName
|
|
2622
|
+
});
|
|
2623
|
+
} catch (error) {
|
|
2624
|
+
if (error instanceof Error && error.message.includes("no such table")) return void 0;
|
|
2625
|
+
throw error;
|
|
2626
|
+
} finally {
|
|
2627
|
+
db.close();
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
function readProjectedRecurrence(knowledgeRoot, query) {
|
|
2631
|
+
const path = (0, import_agent_qa_knowledge_local.indexDbPath)(knowledgeRoot);
|
|
2632
|
+
if (!(0, import_node_fs9.existsSync)(path)) return void 0;
|
|
2633
|
+
const db = new import_node_sqlite4.DatabaseSync(path, { readOnly: true });
|
|
2634
|
+
try {
|
|
2635
|
+
const history = (0, import_agent_qa_knowledge_local.readTestOutcomeHistory)(db, {
|
|
2636
|
+
product: query.product,
|
|
2637
|
+
filePath: query.filePath,
|
|
2638
|
+
testName: query.testName
|
|
2639
|
+
});
|
|
2640
|
+
if (history === void 0) return void 0;
|
|
2641
|
+
const currentBuildOrdinal = history.lastBuildOrdinal ?? history.lastOutcomes.reduce((max, entry) => Math.max(max, entry.buildOrdinal), 0);
|
|
2642
|
+
if (query.fingerprint === void 0) return (0, import_agent_qa_knowledge_local.failureRecurrence)(history, currentBuildOrdinal);
|
|
2643
|
+
const windowed = (0, import_agent_qa_knowledge_local.fingerprintRecurrence)(
|
|
2644
|
+
db,
|
|
2645
|
+
{ product: query.product, fingerprint: query.fingerprint },
|
|
2646
|
+
currentBuildOrdinal
|
|
2647
|
+
);
|
|
2648
|
+
return { kth: windowed.occurrenceOrdinals, windowBuilds: windowed.windowOrdinals };
|
|
2649
|
+
} catch (error) {
|
|
2650
|
+
if (error instanceof Error && error.message.includes("no such table")) return void 0;
|
|
2651
|
+
throw error;
|
|
2652
|
+
} finally {
|
|
2653
|
+
db.close();
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
var LADDER_STATE_FOR_ENTRY = {
|
|
2657
|
+
quarantined: "quarantined",
|
|
2658
|
+
expired: "expired",
|
|
2659
|
+
released: void 0
|
|
2660
|
+
};
|
|
2661
|
+
function readQuarantineLadder(knowledgeRoot, identity, now, recurrence) {
|
|
2662
|
+
const entries = (0, import_agent_qa_knowledge_local.foldQuarantineLedgerState)((0, import_agent_qa_knowledge_local.readQuarantineLedger)(knowledgeRoot).events, now);
|
|
2663
|
+
const entry = entries.find((candidate) => candidate.product === identity.product && candidate.filePath === identity.filePath && candidate.testName === identity.testName);
|
|
2664
|
+
const state = entry === void 0 ? void 0 : LADDER_STATE_FOR_ENTRY[entry.state];
|
|
2665
|
+
if (entry !== void 0 && state !== void 0) return { state, expiresAt: entry.expiresAt };
|
|
2666
|
+
if (recurrence.kth >= import_agent_qa_knowledge_local.QUARANTINE_FLAG_FLOOR) return { state: "flagged" };
|
|
2667
|
+
return void 0;
|
|
2668
|
+
}
|
|
2669
|
+
function createProjectionRecurrenceProvider(resolveKnowledgeRoot, now) {
|
|
2670
|
+
let root;
|
|
2671
|
+
let resolving;
|
|
2672
|
+
function beginResolve() {
|
|
2673
|
+
if (resolving !== void 0) return resolving;
|
|
2674
|
+
let answer;
|
|
2675
|
+
try {
|
|
2676
|
+
answer = resolveKnowledgeRoot();
|
|
2677
|
+
} catch {
|
|
2678
|
+
return void 0;
|
|
2679
|
+
}
|
|
2680
|
+
if (typeof answer === "string") {
|
|
2681
|
+
root = answer;
|
|
2682
|
+
return void 0;
|
|
2683
|
+
}
|
|
2684
|
+
resolving = answer.then(
|
|
2685
|
+
(value) => {
|
|
2686
|
+
root = value;
|
|
2687
|
+
},
|
|
2688
|
+
() => {
|
|
2689
|
+
resolving = void 0;
|
|
2690
|
+
}
|
|
2691
|
+
);
|
|
2692
|
+
return resolving;
|
|
2693
|
+
}
|
|
2694
|
+
function rootNow() {
|
|
2695
|
+
if (root !== void 0) return root;
|
|
2696
|
+
beginResolve();
|
|
2697
|
+
return root;
|
|
2698
|
+
}
|
|
2699
|
+
return {
|
|
2700
|
+
prime() {
|
|
2701
|
+
if (root !== void 0) return Promise.resolve();
|
|
2702
|
+
return beginResolve() ?? Promise.resolve();
|
|
2703
|
+
},
|
|
2704
|
+
provider: {
|
|
2705
|
+
failureRecurrence(query) {
|
|
2706
|
+
const knowledgeRoot = rootNow();
|
|
2707
|
+
if (knowledgeRoot === void 0) return void 0;
|
|
2708
|
+
const recurrence = readProjectedRecurrence(knowledgeRoot, query);
|
|
2709
|
+
if (recurrence === void 0) return void 0;
|
|
2710
|
+
const ladder = readQuarantineLadder(knowledgeRoot, query, now(), recurrence);
|
|
2711
|
+
return ladder === void 0 ? recurrence : { ...recurrence, ladder };
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
function createAnalysisArtifactReader(agentQaRoot) {
|
|
2717
|
+
return {
|
|
2718
|
+
getArtifact(runId) {
|
|
2719
|
+
const path = (0, import_node_path10.join)(agentQaRoot, import_agent_qa_runner_local3.RUNS_SUBDIR, (0, import_agent_qa_runner_local3.slugBranch)(runId), import_agent_qa_schema10.ANALYSIS_FILE);
|
|
2720
|
+
let bytes;
|
|
2721
|
+
try {
|
|
2722
|
+
bytes = (0, import_node_fs9.readFileSync)(path, "utf8");
|
|
2723
|
+
} catch (error) {
|
|
2724
|
+
if (error.code === "ENOENT") return void 0;
|
|
2725
|
+
throw error;
|
|
2726
|
+
}
|
|
2727
|
+
const admission = (0, import_agent_qa_schema10.admitAnalysisArtifactForRun)(
|
|
2728
|
+
(0, import_agent_qa_schema10.guardRunAnalysisArtifact)(JSON.parse(bytes)),
|
|
2729
|
+
runId
|
|
2730
|
+
);
|
|
2731
|
+
return admission.kind === "admitted" ? admission.artifact : void 0;
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
}
|
|
2735
|
+
function postRunCapabilities(input, knowledgeRoot, analysisTokenBudget, jobStore, proposeGate1MaxPerJob) {
|
|
2736
|
+
const postRun = input.postRun;
|
|
2737
|
+
if (postRun === void 0) return void 0;
|
|
2738
|
+
return {
|
|
2739
|
+
// 422 D-14: ONE narrow read — this job's own durable Gate-1 charge, addressed by its own id,
|
|
2740
|
+
// through the authoritative `readCheckpoint` the store already publishes. The handler is
|
|
2741
|
+
// handed no store, no checkpoint reader and no path; the shape check is `agent`'s, beside
|
|
2742
|
+
// the shape it checks, so two hosts cannot disagree about what a readable charge is.
|
|
2743
|
+
...jobStore === void 0 ? {} : {
|
|
2744
|
+
readProposeGate1Allowance: async (jobId) => (0, import_agent_qa_agent4.parseProposeGate1Allowance)(
|
|
2745
|
+
await jobStore.readCheckpoint((0, import_agent_qa_schema10.asJobId)(jobId), import_agent_qa_agent4.PROPOSE_GATE1_ALLOWANCE_CHECKPOINT)
|
|
2746
|
+
)
|
|
2747
|
+
},
|
|
2748
|
+
loadCompletedRunHandoff: (runId) => (0, import_agent_qa_runner_local3.loadCompletedRunHandoff)(
|
|
2749
|
+
(0, import_node_path10.join)(input.agentQaRoot, import_agent_qa_runner_local3.RUNS_SUBDIR, (0, import_agent_qa_runner_local3.slugBranch)(runId), import_agent_qa_runner_local3.HANDOFF_FILE)
|
|
2750
|
+
),
|
|
2751
|
+
curateRun: postRun.curateRun,
|
|
2752
|
+
// 422 D-13: spread on the conditional idiom this file uses throughout, so an unbound port
|
|
2753
|
+
// is an ABSENT member rather than a present `undefined` — the handler decides on presence,
|
|
2754
|
+
// in one place, and a host that declined to bind cannot be mistaken for one that bound a
|
|
2755
|
+
// hole.
|
|
2756
|
+
...postRun.stageAdversarialProposal === void 0 ? {} : { stageAdversarialProposal: postRun.stageAdversarialProposal },
|
|
2757
|
+
// 422-09 (D-14): the Gate-1 leg's four ports and its bound, forwarded on the same
|
|
2758
|
+
// presence-spread idiom one line up. The handler's own `screenGate1` requires ALL FOUR before
|
|
2759
|
+
// it screens anything, so a host that binds three of them is a host that screens nothing —
|
|
2760
|
+
// which is why they are forwarded one by one rather than as a bundle a caller could
|
|
2761
|
+
// half-populate without noticing.
|
|
2762
|
+
...postRun.runReplayGate === void 0 ? {} : { runReplayGate: postRun.runReplayGate },
|
|
2763
|
+
...postRun.listBundles === void 0 ? {} : { listBundles: postRun.listBundles },
|
|
2764
|
+
...postRun.gateReceiptsFor === void 0 ? {} : { gateReceiptsFor: postRun.gateReceiptsFor },
|
|
2765
|
+
...postRun.readProposeCorpusPosture === void 0 ? {} : { readProposeCorpusPosture: postRun.readProposeCorpusPosture },
|
|
2766
|
+
...postRun.readMisdirectionRootPosture === void 0 ? {} : { readMisdirectionRootPosture: postRun.readMisdirectionRootPosture },
|
|
2767
|
+
...proposeGate1MaxPerJob === void 0 ? {} : { proposeGate1MaxPerJob },
|
|
2768
|
+
// 389 D-09: the artifact's path is DERIVED from the artifact's own runId through the
|
|
2769
|
+
// same `runs/<slug>/` law as the handoff read above — `slugBranch` is the traversal
|
|
2770
|
+
// guard (T-389-01), and a job payload never carries a path.
|
|
2771
|
+
persistAnalysis: (artifact) => writeAnalysisArtifact(
|
|
2772
|
+
(0, import_node_path10.join)(input.agentQaRoot, import_agent_qa_runner_local3.RUNS_SUBDIR, (0, import_agent_qa_runner_local3.slugBranch)(artifact.runId), import_agent_qa_schema10.ANALYSIS_FILE),
|
|
2773
|
+
artifact
|
|
2774
|
+
),
|
|
2775
|
+
// Plan 02's read side over the derived projection, keyed the projection's way —
|
|
2776
|
+
// (product, filePath, testName), the stable identity, NEVER a per-run explicit id.
|
|
2777
|
+
// The query arrives already keyed by the author callback; this binding only reads.
|
|
2778
|
+
readTestHistory: (query) => readProjectedTestHistory(knowledgeRoot, query),
|
|
2779
|
+
// D-02's sha-addressed screenshot bytes: the SAME CASS-02 store the runner writes.
|
|
2780
|
+
// `FsEvidenceArtifactStore.getBlob` validates the lowercase 64-hex shape BEFORE any
|
|
2781
|
+
// path use (T-389-14) and answers ENOENT as honest absence; the runId becomes a path
|
|
2782
|
+
// segment only through `slugBranch` inside the store's own constructor.
|
|
2783
|
+
//
|
|
2784
|
+
// 405-05: THROUGH THE SHARED RULE, not a lambda of its own. `runs.artifact` binds a port over
|
|
2785
|
+
// the same `lookupEvidenceBlob` below, and two lambdas over one store would be two answers to
|
|
2786
|
+
// where a run's blobs live — agreeing on the day the second is written and diverging the first
|
|
2787
|
+
// time either side changes a root. This job wants only the found bytes, so the three-arm
|
|
2788
|
+
// lookup collapses here; the SPLIT is what the op needs and what this caller does not.
|
|
2789
|
+
readEvidenceBlob: (runId, sha) => readEvidenceBlobBytes({ agentQaRoot: input.agentQaRoot }, runId, sha),
|
|
2790
|
+
// Host-resolved and passed through untouched (386 D-15): the budget derives from the
|
|
2791
|
+
// same profile resolution as the job model, and this composition adds no knob of its own.
|
|
2792
|
+
...analysisTokenBudget === void 0 ? {} : { analysisTokenBudget }
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2795
|
+
var CHAT_RESOLVER_CALLER = Object.freeze({
|
|
2796
|
+
surface: "chat",
|
|
2797
|
+
origin: "human"
|
|
2798
|
+
});
|
|
2799
|
+
var CHAT_PLANNER_CALLER = Object.freeze({
|
|
2800
|
+
surface: "chat",
|
|
2801
|
+
origin: "agent"
|
|
2802
|
+
});
|
|
2803
|
+
function chatToolsetIds(registry) {
|
|
2804
|
+
if (registry === void 0) return [];
|
|
2805
|
+
return registry.list(CHAT_RESOLVER_CALLER).map((declaration) => declaration.id).sort();
|
|
2806
|
+
}
|
|
2807
|
+
function chatToolsetHash(registry) {
|
|
2808
|
+
if (registry === void 0) return "unbound";
|
|
2809
|
+
return (0, import_agent_qa_schema10.sha256hex16)(chatToolsetIds(registry).join("\n"));
|
|
2810
|
+
}
|
|
2811
|
+
function projectChatMembers(value, rules, mask) {
|
|
2812
|
+
return Object.fromEntries(Object.entries(value).map(([key, member]) => {
|
|
2813
|
+
const rule = Object.hasOwn(rules, key) ? rules[key] : "tree";
|
|
2814
|
+
if (rule === "as-written") return [key, member];
|
|
2815
|
+
if (typeof rule === "function") return [key, rule(member, mask)];
|
|
2816
|
+
if (rule === "text" && typeof member === "string") return [key, mask(member)];
|
|
2817
|
+
return [key, (0, import_agent_qa_schema10.maskPayloadTree)(member, mask)];
|
|
2818
|
+
}));
|
|
2819
|
+
}
|
|
2820
|
+
var eachChatRecord = (rules) => (value, mask) => {
|
|
2821
|
+
const project = (entry) => isRecord(entry) ? projectChatMembers(entry, rules, mask) : (0, import_agent_qa_schema10.maskPayloadTree)(entry, mask);
|
|
2822
|
+
return Array.isArray(value) ? value.map(project) : project(value);
|
|
2823
|
+
};
|
|
2824
|
+
var CHAT_EVENT_BASE = {
|
|
2825
|
+
kind: "as-written",
|
|
2826
|
+
turnId: "as-written",
|
|
2827
|
+
at: "as-written"
|
|
2828
|
+
};
|
|
2829
|
+
var CHAT_CLAUSE_RECEIPT_RULES = {
|
|
2830
|
+
clause: "text",
|
|
2831
|
+
functionId: "as-written",
|
|
2832
|
+
origin: "as-written",
|
|
2833
|
+
status: "as-written",
|
|
2834
|
+
outcome: "text"
|
|
2835
|
+
};
|
|
2836
|
+
var CHAT_EVENT_RULES = {
|
|
2837
|
+
user_message: { ...CHAT_EVENT_BASE, text: "text" },
|
|
2838
|
+
stage_outcome: { ...CHAT_EVENT_BASE, stage: "as-written", outcome: "as-written" },
|
|
2839
|
+
clause_receipt: { ...CHAT_EVENT_BASE, ...CHAT_CLAUSE_RECEIPT_RULES },
|
|
2840
|
+
clause_result: { ...CHAT_EVENT_BASE, functionId: "as-written", payload: "tree" },
|
|
2841
|
+
approval_decision: {
|
|
2842
|
+
...CHAT_EVENT_BASE,
|
|
2843
|
+
actionClass: "as-written",
|
|
2844
|
+
decision: "as-written",
|
|
2845
|
+
source: "as-written"
|
|
2846
|
+
},
|
|
2847
|
+
mark_unhandled: { ...CHAT_EVENT_BASE, clause: "text", reason: "text" },
|
|
2848
|
+
model_change: { ...CHAT_EVENT_BASE, from: "text", to: "text", source: "as-written" },
|
|
2849
|
+
active_tools_change: {
|
|
2850
|
+
...CHAT_EVENT_BASE,
|
|
2851
|
+
addedIds: "as-written",
|
|
2852
|
+
removedIds: "as-written",
|
|
2853
|
+
toolsetHash: "as-written"
|
|
2854
|
+
},
|
|
2855
|
+
compaction: {
|
|
2856
|
+
...CHAT_EVENT_BASE,
|
|
2857
|
+
beforeTokens: "as-written",
|
|
2858
|
+
afterTokens: "as-written",
|
|
2859
|
+
outcome: "as-written"
|
|
2860
|
+
},
|
|
2861
|
+
aborted: { ...CHAT_EVENT_BASE, source: "as-written" },
|
|
2862
|
+
turn_complete: { ...CHAT_EVENT_BASE, status: "as-written" }
|
|
2863
|
+
};
|
|
2864
|
+
var CHAT_INVOKE_ANSWER_RULES = {
|
|
2865
|
+
payload: "tree",
|
|
2866
|
+
verdict: eachChatRecord({ status: "as-written", outcome: "text" }),
|
|
2867
|
+
foldLines: "tree"
|
|
2868
|
+
};
|
|
2869
|
+
var CHAT_TURN_RESULT_RULES = {
|
|
2870
|
+
sessionId: "as-written",
|
|
2871
|
+
turnId: "as-written",
|
|
2872
|
+
status: "as-written",
|
|
2873
|
+
answer: "text",
|
|
2874
|
+
receipts: eachChatRecord(CHAT_CLAUSE_RECEIPT_RULES),
|
|
2875
|
+
results: eachChatRecord({ functionId: "as-written", payload: "tree" }),
|
|
2876
|
+
unhandled: eachChatRecord({ clause: "text", reason: "text" }),
|
|
2877
|
+
refusedLineCount: "as-written"
|
|
2878
|
+
};
|
|
2879
|
+
function projectChatSessionEvent(event, mask) {
|
|
2880
|
+
const rules = Object.hasOwn(CHAT_EVENT_RULES, event.kind) ? CHAT_EVENT_RULES[event.kind] : CHAT_EVENT_BASE;
|
|
2881
|
+
return projectChatMembers(event, rules, mask);
|
|
2882
|
+
}
|
|
2883
|
+
function composeLocalAgent(input) {
|
|
2884
|
+
const clock = input.clock ?? SYSTEM_CLOCK;
|
|
2885
|
+
const entityResolutionInput = {
|
|
2886
|
+
agentQaRoot: input.agentQaRoot,
|
|
2887
|
+
resolveKnowledgeRoot: input.knowledgeRoot
|
|
2888
|
+
};
|
|
2889
|
+
const recurrence = createProjectionRecurrenceProvider(input.knowledgeRoot, () => clock.now());
|
|
2890
|
+
const runsReader = createRunsReader(input.agentQaRoot);
|
|
2891
|
+
let composedRegistry;
|
|
2892
|
+
const composedChatModelName = input.chatModel === void 0 ? "unbound" : input.chatModel.modelId;
|
|
2893
|
+
const chatScopes = /* @__PURE__ */ new Map();
|
|
2894
|
+
const chatScopeFor = (sessionId) => {
|
|
2895
|
+
const existing = chatScopes.get(sessionId);
|
|
2896
|
+
if (existing !== void 0) return existing;
|
|
2897
|
+
if (composedRegistry === void 0) {
|
|
2898
|
+
throw new TypeError("a chat session opened before its registry finished composing");
|
|
2899
|
+
}
|
|
2900
|
+
const scope = {
|
|
2901
|
+
// An UNBORN session has run no `/model`, so it opens under the model this host composed —
|
|
2902
|
+
// which is also what its header will say. A container seeded from another session's override
|
|
2903
|
+
// would make one conversation's verb the next conversation's opening posture.
|
|
2904
|
+
state: (0, import_agent_qa_agent4.createChatState)({ activeModel: composedChatModelName })
|
|
2905
|
+
// 432-13 (D-14): no pin holder. `/pin` left chat resolution with the op gate it tightened.
|
|
2906
|
+
};
|
|
2907
|
+
chatScopes.set(sessionId, scope);
|
|
2908
|
+
return scope;
|
|
2909
|
+
};
|
|
2910
|
+
const chatModelNameFor = (sessionId) => chatScopeFor(sessionId).state.read().activeModel;
|
|
2911
|
+
const chatModelsByName = /* @__PURE__ */ new Map();
|
|
2912
|
+
if (input.chatModel !== void 0) chatModelsByName.set(composedChatModelName, input.chatModel);
|
|
2913
|
+
const chatSessionLog = createJsonlSessionLog({
|
|
2914
|
+
root: input.agentQaRoot,
|
|
2915
|
+
now: () => clock.now(),
|
|
2916
|
+
// THE COMPOSED MODEL, not a session's override, and the difference is a leak this keying closes.
|
|
2917
|
+
// This thunk is read when a header is WRITTEN, which happens exactly once per session and only
|
|
2918
|
+
// for a session that has not existed until now — so no `/model` can have run in it. Answering
|
|
2919
|
+
// with a live override would stamp one conversation's switch onto the opening header of every
|
|
2920
|
+
// conversation opened after it. 441 GAP3-D4: written and listed through the declared mask.
|
|
2921
|
+
model: () => input.mask(composedChatModelName),
|
|
2922
|
+
toolsetHash: () => chatToolsetHash(composedRegistry)
|
|
2923
|
+
});
|
|
2924
|
+
const liveChatSessions = /* @__PURE__ */ new Map();
|
|
2925
|
+
const trackedChatSessionLog = {
|
|
2926
|
+
async open(sessionId) {
|
|
2927
|
+
const opened2 = await chatSessionLog.open(sessionId);
|
|
2928
|
+
const id = opened2.header.sessionId;
|
|
2929
|
+
const turnIds = /* @__PURE__ */ new Set();
|
|
2930
|
+
const tracked = {
|
|
2931
|
+
header: opened2.header,
|
|
2932
|
+
events: opened2.events,
|
|
2933
|
+
refusedLines: opened2.refusedLines,
|
|
2934
|
+
append: async (event) => {
|
|
2935
|
+
const projected = projectChatSessionEvent(event, input.mask);
|
|
2936
|
+
turnIds.add(projected.turnId);
|
|
2937
|
+
await opened2.append(projected);
|
|
2938
|
+
try {
|
|
2939
|
+
input.observeSessionEvent?.(id, projected);
|
|
2940
|
+
} catch {
|
|
2941
|
+
}
|
|
2942
|
+
},
|
|
2943
|
+
turnBoundary: () => opened2.turnBoundary(),
|
|
2944
|
+
close: async () => {
|
|
2945
|
+
liveChatSessions.delete(id);
|
|
2946
|
+
await opened2.close();
|
|
2947
|
+
}
|
|
2948
|
+
};
|
|
2949
|
+
liveChatSessions.set(id, { handle: tracked, turnIds });
|
|
2950
|
+
return tracked;
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
const chatSessionHoldingTurn = (turnId) => {
|
|
2954
|
+
for (const entry of liveChatSessions.values()) {
|
|
2955
|
+
if (entry.turnIds.has(turnId)) return entry.handle;
|
|
2956
|
+
}
|
|
2957
|
+
return void 0;
|
|
2958
|
+
};
|
|
2959
|
+
const plannerStages = /* @__PURE__ */ new Map();
|
|
2960
|
+
const chatPlanner = input.chatModel === void 0 ? void 0 : {
|
|
2961
|
+
plan: (request) => {
|
|
2962
|
+
if (composedRegistry === void 0) {
|
|
2963
|
+
throw new TypeError("a chat turn ran before its registry finished composing");
|
|
2964
|
+
}
|
|
2965
|
+
const activeName = chatModelNameFor(request.sessionId);
|
|
2966
|
+
let stage = plannerStages.get(activeName);
|
|
2967
|
+
if (stage === void 0) {
|
|
2968
|
+
stage = (0, import_agent_qa_agent4.createPlannerStage)({
|
|
2969
|
+
model: chatModelsByName.get(activeName) ?? input.chatModel,
|
|
2970
|
+
tools: (0, import_agent_qa_agent4.projectPlannerTools)(composedRegistry, CHAT_PLANNER_CALLER),
|
|
2971
|
+
// Read PER REQUEST — not per turn and not per stage (392 plan 13, CR-02). A stage
|
|
2972
|
+
// is shared by every session planning under the same model name, so a context
|
|
2973
|
+
// thunk taking no argument had exactly one conversation to answer with, and on
|
|
2974
|
+
// two interleaved turns it answered with whichever one published last. Taking the
|
|
2975
|
+
// requesting session's id makes "the conversation this request replays" a fact
|
|
2976
|
+
// about the call. The read is still LIVE, so a `/model` override still moves what
|
|
2977
|
+
// the prompt states without the stage being rebuilt, and the conversation block
|
|
2978
|
+
// still carries what compaction left rather than a second replay of the log.
|
|
2979
|
+
context: (contextRequest) => {
|
|
2980
|
+
const scope = chatScopeFor(contextRequest.sessionId).state.read();
|
|
2981
|
+
return { activeModel: scope.activeModel, conversation: scope.conversation };
|
|
2982
|
+
}
|
|
2983
|
+
});
|
|
2984
|
+
plannerStages.set(activeName, stage);
|
|
2985
|
+
}
|
|
2986
|
+
return stage.plan(request);
|
|
2987
|
+
}
|
|
2988
|
+
};
|
|
2989
|
+
const chatCompaction = input.chatAccounting === void 0 ? void 0 : (0, import_agent_qa_agent4.createChatCompactionHook)({
|
|
2990
|
+
accounting: input.chatAccounting,
|
|
2991
|
+
now: () => clock.now(),
|
|
2992
|
+
// 392-06's flagged consumer, wired: what compaction leaves IS the conversation the
|
|
2993
|
+
// prompt renders, and it lands in the live state on EVERY turn rather than only on a
|
|
2994
|
+
// triggered pass — a sink that fired only when a pass ran would leave the prompt holding
|
|
2995
|
+
// a stale conversation on every quiet turn. Into the SESSION'S OWN container (plan 13):
|
|
2996
|
+
// the id is the compacted turn's, so a pruned history cannot land in another
|
|
2997
|
+
// conversation however many turns are in flight.
|
|
2998
|
+
onConversation: (messages, sessionId) => {
|
|
2999
|
+
chatScopeFor(sessionId).state.setConversation(messages);
|
|
3000
|
+
}
|
|
3001
|
+
});
|
|
3002
|
+
const chatTurn = (0, import_agent_qa_agent4.createTurnEngine)({
|
|
3003
|
+
table: import_agent_qa_agent4.CHAT_RESOLVER_TABLE,
|
|
3004
|
+
// 392 plan 02, D-09: the real conjunction grammar, quote-aware. The tracer's
|
|
3005
|
+
// whole-utterance placeholder is gone; this is the seam filled, not widened.
|
|
3006
|
+
split: import_agent_qa_agent4.splitClauses,
|
|
3007
|
+
reconcile: input.chatReconcile ?? import_agent_qa_agent4.deterministicOverrideStrategy,
|
|
3008
|
+
sessionLog: trackedChatSessionLog,
|
|
3009
|
+
// RESEARCH Pitfall 1: per-STAGE minting, because origin is a per-call fact. A deterministic
|
|
3010
|
+
// table hit is a person typing and mints `human`; a planner-planned call mints `agent`, and the
|
|
3011
|
+
// turn's receipts record which.
|
|
3012
|
+
// 2026-10-01 (432-21, D-12): NOT STAMPED WITH THE SESSION ANY MORE. The session id rode the
|
|
3013
|
+
// caller for the gate's deny overlay (plan 13, W-03) and the overlay is deleted (432-13/18/19);
|
|
3014
|
+
// nothing reads `FunctionCaller.sessionId`. The session still reaches the ops that record it
|
|
3015
|
+
// through the turn's own session facts, not through the caller.
|
|
3016
|
+
mint: {
|
|
3017
|
+
forStage: (stage) => stage === "resolver" ? CHAT_RESOLVER_CALLER : CHAT_PLANNER_CALLER
|
|
3018
|
+
},
|
|
3019
|
+
// 399.1 D-01/D-03/D-05: the classified envelope, built HERE because this closure is the one
|
|
3020
|
+
// place in the system holding both the registry and the invoke call. The chat subtree cannot
|
|
3021
|
+
// reach a spec by construction (384 FUNC-03), so the verdict and the render fold have to arrive
|
|
3022
|
+
// as data on this return — which is what `ChatInvokeAnswer` is. `foldFunctionResult` owns the
|
|
3023
|
+
// fail-closed rule so both composition roots get the same one rather than a copy each.
|
|
3024
|
+
//
|
|
3025
|
+
// invokeFunction validates canonical output; it does not promise masking. Classify those
|
|
3026
|
+
// original bytes before projecting the answer into results[] and the durable clause_result.
|
|
3027
|
+
invoke: async (functionId, invokeInput, caller) => {
|
|
3028
|
+
if (composedRegistry === void 0) {
|
|
3029
|
+
throw new TypeError("a chat turn ran before its registry finished composing");
|
|
3030
|
+
}
|
|
3031
|
+
const payload = await (0, import_agent_qa_agent4.invokeFunction)(composedRegistry, functionId, invokeInput, caller);
|
|
3032
|
+
const fold = (0, import_agent_qa_agent4.foldFunctionResult)(composedRegistry, functionId, payload);
|
|
3033
|
+
return projectChatMembers({ payload, ...fold }, CHAT_INVOKE_ANSWER_RULES, input.mask);
|
|
3034
|
+
},
|
|
3035
|
+
...chatPlanner === void 0 ? {} : { planner: chatPlanner },
|
|
3036
|
+
...chatCompaction === void 0 ? {} : { compaction: chatCompaction },
|
|
3037
|
+
// 392 plan 08: the directive handlers, which is what turns the engine's
|
|
3038
|
+
// `directive recognised but no handler` refusal from the shipped behaviour into an
|
|
3039
|
+
// unreachable arm on this host. `/help` and `/quit` are REPL chrome and answer in prose;
|
|
3040
|
+
// `/model` changes what the session is and returns the EFFECT it had. (432-13: `/pin` is gone.)
|
|
3041
|
+
//
|
|
3042
|
+
// RESOLVED PER SESSION (plan 13, CR-02 / W-03). Each one acts on a conversation rather than the
|
|
3043
|
+
// project, so the handler set is built over the scope of the session running the turn:
|
|
3044
|
+
// `/model` moves that conversation's model and `/quit` asks that loop to end.
|
|
3045
|
+
directives: (sessionId) => ({
|
|
3046
|
+
// THE BATCH PRE-FLIGHT (392-14, E3/E3b). The engine asks this of EVERY directive of a turn
|
|
3047
|
+
// before it applies the first one, so a refused later directive leaves an earlier one
|
|
3048
|
+
// unapplied.
|
|
3049
|
+
//
|
|
3050
|
+
// IT APPLIES NOTHING. `/model` RESOLVES the model and caches it under its name — a cache is
|
|
3051
|
+
// not the session's state, and the session's own `activeModel` is left exactly where it was.
|
|
3052
|
+
// The resolution is also the only part of `/model` that can fail, so caching it here is what
|
|
3053
|
+
// makes the later apply unable to throw after an earlier directive has already been carried
|
|
3054
|
+
// out.
|
|
3055
|
+
validate: async (directive, argument) => {
|
|
3056
|
+
if (directive === "model" && argument !== void 0 && input.chatModelFor !== void 0) {
|
|
3057
|
+
chatModelsByName.set(argument, await input.chatModelFor(argument));
|
|
3058
|
+
}
|
|
3059
|
+
},
|
|
3060
|
+
model: async (name) => {
|
|
3061
|
+
const scope = chatScopeFor(sessionId);
|
|
3062
|
+
const from = scope.state.read().activeModel;
|
|
3063
|
+
if (input.chatModelFor !== void 0 && !chatModelsByName.has(name)) {
|
|
3064
|
+
chatModelsByName.set(name, await input.chatModelFor(name));
|
|
3065
|
+
}
|
|
3066
|
+
scope.state.setActiveModel(name);
|
|
3067
|
+
return { from, to: name, line: `model: ${from} \u2192 ${name}` };
|
|
3068
|
+
},
|
|
3069
|
+
help: () => import_agent_qa_agent4.CHAT_HELP_TEXT,
|
|
3070
|
+
quit: () => {
|
|
3071
|
+
chatScopeFor(sessionId).state.requestClose();
|
|
3072
|
+
return "closing this session";
|
|
3073
|
+
}
|
|
3074
|
+
}),
|
|
3075
|
+
state: (sessionId) => chatScopeFor(sessionId).state,
|
|
3076
|
+
// D-04: the configuration RE-READ, per turn and PER SESSION. A thunk rather than a captured
|
|
3077
|
+
// value, because the drift worth detecting is precisely the one that happens underneath a
|
|
3078
|
+
// running session — a model resolved differently by the host, or a build whose registry
|
|
3079
|
+
// reaches further than the one this session opened against. The engine folds the log for what
|
|
3080
|
+
// was last recorded and emits `model_change` / `active_tools_change` on the difference.
|
|
3081
|
+
//
|
|
3082
|
+
// THE MODEL IT REPORTS IS THE ASKING SESSION'S (plan 13). A composition-wide read would answer
|
|
3083
|
+
// one conversation's `/model` override for every conversation, and the per-turn diff would
|
|
3084
|
+
// then write a `model_change` into sessions nobody had switched.
|
|
3085
|
+
//
|
|
3086
|
+
// 441 GAP3-D4: masked, because the log it is compared with is masked on append.
|
|
3087
|
+
config: (sessionId) => {
|
|
3088
|
+
const ids = chatToolsetIds(composedRegistry);
|
|
3089
|
+
return {
|
|
3090
|
+
model: input.mask(chatModelNameFor(sessionId)),
|
|
3091
|
+
toolset: { hash: chatToolsetHash(composedRegistry), ids }
|
|
3092
|
+
};
|
|
3093
|
+
},
|
|
3094
|
+
// 395-08 D-14/D-15 · CFGM-05: the target vocabulary the PRE-DISPATCH write_test route reads.
|
|
3095
|
+
// Bound off the SAME host verb `authoringRead.configuredTargetNames` uses, so the seam that
|
|
3096
|
+
// decides whether to offer a scaffold and the loop that later checks what a model emitted are
|
|
3097
|
+
// reading one answer rather than two. A host that cannot name its targets binds nothing and the
|
|
3098
|
+
// route never runs, which leaves the shipped dispatch and the loop's own backstop exactly as
|
|
3099
|
+
// they were.
|
|
3100
|
+
// BOUND ONLY WHERE THE LOOP IS. A host that cannot author has nothing for this route to route,
|
|
3101
|
+
// and binding it anyway would answer an empty vocabulary — which the route reads as the
|
|
3102
|
+
// from-zero case and refuses, reporting the wrong cause for a host whose real problem is that
|
|
3103
|
+
// `authoring.draft.write` is unbound. Absent leaves the shipped dispatch untouched.
|
|
3104
|
+
...input.draftAuthoring !== void 0 && input.runExecution !== void 0 ? { configuredTargets: () => input.draftAuthoring.configuredTargetNames() } : {},
|
|
3105
|
+
// 395-13 D-14: where the scaffold proposal is rendered before the in-turn apply is invoked.
|
|
3106
|
+
// Conditionally spread on the exact idiom above, so a host that binds no surface leaves the
|
|
3107
|
+
// field ABSENT and the engine's own "renders nothing" posture is what answers — in one place.
|
|
3108
|
+
...input.chatPresent === void 0 ? {} : { present: input.chatPresent },
|
|
3109
|
+
now: () => clock.now(),
|
|
3110
|
+
mintTurnId: () => `turn-${(0, import_node_crypto4.randomUUID)()}`
|
|
3111
|
+
});
|
|
3112
|
+
const authoringLoopHost = input.draftAuthoring !== void 0 && input.runExecution !== void 0 ? input.draftAuthoring : void 0;
|
|
3113
|
+
const authoringFunnel = {
|
|
3114
|
+
receipts: (0, import_agent_qa_knowledge_local.createDraftReceiptsStore)({ resolveAgentQaRoot: () => input.agentQaRoot }),
|
|
3115
|
+
quarantine: {
|
|
3116
|
+
list: async () => await (0, import_agent_qa_knowledge_local.createQuarantineOperations)({
|
|
3117
|
+
resolveKnowledgeRoot: input.knowledgeRoot,
|
|
3118
|
+
now: () => clock.now(),
|
|
3119
|
+
mintIdentityPath: input.mintQuarantineIdentityPath,
|
|
3120
|
+
...input.resolveWriterAuthority === void 0 ? {} : { resolveWriterAuthority: input.resolveWriterAuthority }
|
|
3121
|
+
}).list()
|
|
3122
|
+
},
|
|
3123
|
+
// D-14's history half, over the run-outcome projection's own read side (394-17, step b).
|
|
3124
|
+
//
|
|
3125
|
+
// ONE CONNECTION PER VERB, opened read-only and closed in `finally`, on the shape
|
|
3126
|
+
// `readProjectedTestHistory` above already takes. An absent database, a store predating the
|
|
3127
|
+
// projection tables and an identity nothing has counted all answer EMPTY — which
|
|
3128
|
+
// `assessSuiteStability` turns into the honest `no-history` posture rather than into a clean
|
|
3129
|
+
// bill of health. Any other read failure propagates: a corrupt store is not "no history".
|
|
3130
|
+
//
|
|
3131
|
+
// THE OUTCOME WORDS. The durable window keeps one boolean per build (`last_outcomes_json`),
|
|
3132
|
+
// which is what the fold reduced each member's outcome to, so this spells it back with the two
|
|
3133
|
+
// words it was reduced FROM. Nothing downstream compares those strings against a member list —
|
|
3134
|
+
// `assessSuiteStability` counts DISTINCT values — so a flip in the window is a flip here, and
|
|
3135
|
+
// no vocabulary is invented beyond what the projection actually kept.
|
|
3136
|
+
outcomes: {
|
|
3137
|
+
testsIn: async (product, filePath) => {
|
|
3138
|
+
const knowledgeRoot = await input.knowledgeRoot();
|
|
3139
|
+
const path = (0, import_agent_qa_knowledge_local.indexDbPath)(knowledgeRoot);
|
|
3140
|
+
if (!(0, import_node_fs9.existsSync)(path)) return [];
|
|
3141
|
+
const db = new import_node_sqlite4.DatabaseSync(path, { readOnly: true });
|
|
3142
|
+
try {
|
|
3143
|
+
return (0, import_agent_qa_knowledge_local.listTestsInFile)(db, { product, filePath }).map((testName) => ({ filePath, testName }));
|
|
3144
|
+
} catch (error) {
|
|
3145
|
+
if (error instanceof Error && error.message.includes("no such table")) return [];
|
|
3146
|
+
throw error;
|
|
3147
|
+
} finally {
|
|
3148
|
+
db.close();
|
|
3149
|
+
}
|
|
3150
|
+
},
|
|
3151
|
+
outcomesFor: async (product, filePath, testName) => {
|
|
3152
|
+
const knowledgeRoot = await input.knowledgeRoot();
|
|
3153
|
+
const history = readProjectedTestHistory(knowledgeRoot, { product, filePath, testName });
|
|
3154
|
+
if (history === void 0) return [];
|
|
3155
|
+
return history.lastOutcomes.map((entry) => entry.passed ? "passed" : "failed");
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
};
|
|
3159
|
+
const registry = (0, import_agent_qa_agent4.createFunctionRegistry)({
|
|
3160
|
+
deps: {
|
|
3161
|
+
secretMask: input.mask,
|
|
3162
|
+
runs: {
|
|
3163
|
+
...runsReader,
|
|
3164
|
+
get: async (runId) => {
|
|
3165
|
+
await recurrence.prime();
|
|
3166
|
+
return runsReader.get(runId);
|
|
3167
|
+
}
|
|
3168
|
+
},
|
|
3169
|
+
// 387 D-01: this host's list verbs are live sighting sites. Every entity a listing,
|
|
3170
|
+
// an inspect, a diff or a show surfaces gets its permanent address the first time it
|
|
3171
|
+
// is surfaced — which is what "pre-existing entities get IDs the day they are first
|
|
3172
|
+
// referenced" means in practice, and it costs the creation paths nothing.
|
|
3173
|
+
knowledge: createKnowledgeReader(
|
|
3174
|
+
input.knowledgeRoot,
|
|
3175
|
+
createMemorySightingHook(entityResolutionInput),
|
|
3176
|
+
input.resolvePromotionPosture,
|
|
3177
|
+
input.resolveTrustPriors
|
|
3178
|
+
),
|
|
3179
|
+
config: input.config,
|
|
3180
|
+
entities: createEntityDirectory(entityResolutionInput),
|
|
3181
|
+
// 387-08 KREV-01: the WRITE half, and it is present in every local composition because a
|
|
3182
|
+
// local host owns the knowledge root it serves from. It stays an OPTIONAL dep so a host
|
|
3183
|
+
// that is genuinely read-only — a listing, a remote surface — reports the four mutating
|
|
3184
|
+
// verbs as `unbound` BY NAME rather than as a hole in the catalogue.
|
|
3185
|
+
knowledgeReview: createKnowledgeReview({
|
|
3186
|
+
...input.resolveTrustPriors === void 0 ? {} : { resolveTrustPriors: input.resolveTrustPriors },
|
|
3187
|
+
...input.resolvePromotionPosture === void 0 ? {} : { resolvePromotionPosture: input.resolvePromotionPosture },
|
|
3188
|
+
agentQaRoot: input.agentQaRoot,
|
|
3189
|
+
resolveKnowledgeRoot: input.knowledgeRoot,
|
|
3190
|
+
...input.knowledgeChangeDelegate === void 0 ? {} : { knowledgeChangeDelegate: input.knowledgeChangeDelegate },
|
|
3191
|
+
...input.resolveWriterAuthority === void 0 ? {} : { resolveWriterAuthority: input.resolveWriterAuthority },
|
|
3192
|
+
// 432-12: the host's own context, so a direct change is recorded under the context it was
|
|
3193
|
+
// actually made in. (432-39: the registry no longer receives it; this is its one reader.)
|
|
3194
|
+
...input.resolveExecutionContext === void 0 ? {} : { resolveExecutionContext: input.resolveExecutionContext }
|
|
3195
|
+
}),
|
|
3196
|
+
// 388 D-14: chip truth, on EVERY local host and not gated on `postRun`. The two are
|
|
3197
|
+
// different questions and conflating them would be the bug: `postRun` says whether this
|
|
3198
|
+
// host can DO the chain's work, while this says whether it can SEE what the chain did —
|
|
3199
|
+
// and a host that never drains anything (a status query, a listing, the CLI after a
|
|
3200
|
+
// handed-off tail) is exactly the caller that most needs to read durable state.
|
|
3201
|
+
//
|
|
3202
|
+
// Given the SAME id source the queue below is built with, so the rows projected are the
|
|
3203
|
+
// rows enqueued. It opens nothing here: the reader constructs no handle until it is asked
|
|
3204
|
+
// a question, and never one that could create the database (Pitfall 4).
|
|
3205
|
+
postProcessing: createPostProcessingReader({
|
|
3206
|
+
agentQaRoot: input.agentQaRoot,
|
|
3207
|
+
idSource: input.idSource ?? DIGEST_JOB_ID_SOURCE
|
|
3208
|
+
}),
|
|
3209
|
+
// 405 D-03: the run-detail projection read, conditionally spread on the house rule — an
|
|
3210
|
+
// absent port must be an ABSENT field and not a present `undefined` one, because the
|
|
3211
|
+
// registry reads exactly this record to decide what the capability LISTS as. Unlike
|
|
3212
|
+
// `postProcessing` above it is NOT present on every local host: chip truth is readable from
|
|
3213
|
+
// `agentQaRoot` alone, but a run's projected step tree lives in a dashboard database that
|
|
3214
|
+
// only the dashboard process holds.
|
|
3215
|
+
// 423 D-04: runs.evidence shares this reader with runs.detail and the blob reader
|
|
3216
|
+
// below. The registry requires both for evidence without narrowing detail availability.
|
|
3217
|
+
...input.runDetail === void 0 ? {} : { runDetail: input.runDetail },
|
|
3218
|
+
// 405-05 D-12/D-13: the run-scoped evidence-blob read, on EVERY local host and NOT
|
|
3219
|
+
// conditionally spread — unlike `runDetail` one line up, which needs a dashboard database
|
|
3220
|
+
// this host may not hold, every local composition has a runtime root and therefore has the
|
|
3221
|
+
// run directories these blobs live in. It is the SAME reader the analysis job's
|
|
3222
|
+
// `readEvidenceBlob` is built over, so "resolve this ref" and "diet this evidence" cannot
|
|
3223
|
+
// disagree about where a blob is.
|
|
3224
|
+
evidenceBlobs: createEvidenceBlobReader({ agentQaRoot: input.agentQaRoot }),
|
|
3225
|
+
// Present iff this host can execute a run. The registry reads the SAME record to
|
|
3226
|
+
// decide what `runs.start` lists as, so the declaration and the dispatch cannot
|
|
3227
|
+
// disagree about whether this composition can run it (D-16).
|
|
3228
|
+
...input.runExecution === void 0 ? {} : {
|
|
3229
|
+
runInvocation: createLocalRunInvocation({
|
|
3230
|
+
agentQaRoot: input.agentQaRoot,
|
|
3231
|
+
execution: input.runExecution
|
|
3232
|
+
})
|
|
3233
|
+
},
|
|
3234
|
+
// 389 D-10/D-11: the artifact read `analysis.get` binds through, and the D-08
|
|
3235
|
+
// recurrence join the one renderer consults. Both are bindable from `agentQaRoot`
|
|
3236
|
+
// and the knowledge root alone, so every local composition carries them — a host
|
|
3237
|
+
// with no artifact on disk serves the floor, and a host with no recorded history
|
|
3238
|
+
// renders the explicit no-history clause; neither is a refusal (plan 05's law).
|
|
3239
|
+
analysis: createAnalysisArtifactReader(input.agentQaRoot),
|
|
3240
|
+
recurrence: recurrence.provider,
|
|
3241
|
+
// 398 D-09 (dated 2026-08-17): the catalog port `agent.explain` binds through, handed
|
|
3242
|
+
// straight through from the host. Conditionally spread rather than assigned, on the
|
|
3243
|
+
// `runInvocation` precedent: an absent port must be an ABSENT field and not a present
|
|
3244
|
+
// `undefined` one, because the registry reads exactly this record to decide what the
|
|
3245
|
+
// capability LISTS as — so the declaration and the dispatch cannot disagree about whether
|
|
3246
|
+
// this composition can explain itself (D-16).
|
|
3247
|
+
...input.capabilityCatalog === void 0 ? {} : { capabilityCatalog: input.capabilityCatalog },
|
|
3248
|
+
// 403 D-03: the live-view mint seam, conditionally spread on the same rule — an absent port
|
|
3249
|
+
// must be an ABSENT field, so `runs.liveurl` declares `unbound` where no origin is served.
|
|
3250
|
+
...input.liveView === void 0 ? {} : { liveView: input.liveView },
|
|
3251
|
+
// 390 D-12/D-13: the three quarantine verbs, bound HERE and therefore on BOTH hosts —
|
|
3252
|
+
// the CLI tail and 386's daemon compose the same root, which is the whole point of
|
|
3253
|
+
// 385 D-15 (a second binding site is a second answer to "what is quarantined").
|
|
3254
|
+
//
|
|
3255
|
+
// Present in every local composition for the same reason the knowledge WRITE half is: a
|
|
3256
|
+
// local host owns the committed knowledge root the ledger lives at. It stays an OPTIONAL
|
|
3257
|
+
// dep so a genuinely read-only or remote composition reports all three `unbound` BY NAME
|
|
3258
|
+
// rather than answering an empty list about a ledger it has never been able to open.
|
|
3259
|
+
//
|
|
3260
|
+
// The port takes the root as a FACTORY (the CLI resolves it asynchronously) and the clock
|
|
3261
|
+
// as an argument, so this composition supplies both and the implementation reads neither
|
|
3262
|
+
// of its own accord. Nothing opens here: the first byte is read when a verb is invoked.
|
|
3263
|
+
//
|
|
3264
|
+
// 390-09 / CR-03: the SAME `resolveWriterAuthority` the apply path consumes three bindings
|
|
3265
|
+
// above, and it is what lands each host on the correct lease arm with zero daemon changes.
|
|
3266
|
+
// The daemon supplies it (`cli/src/daemon/host.ts:1839`) because it acquired the lease at
|
|
3267
|
+
// startup and holds it for its lifetime, so the verbs ride that authority instead of asking
|
|
3268
|
+
// for a lease the process cannot be granted twice; the CLI in-process host supplies nothing,
|
|
3269
|
+
// so the verbs acquire and release per call. Conditionally spread like its neighbour rather
|
|
3270
|
+
// than assigned through: an absent authority must be an ABSENT field, not a present
|
|
3271
|
+
// `undefined` one, so the implementation's `?.()` arm distinguishes the two hosts.
|
|
3272
|
+
//
|
|
3273
|
+
// 390-12 / D-01: the identity mint is handed straight through, REQUIRED and unspread. The
|
|
3274
|
+
// verbs key the committed ledger on `filePath` and every reader compares those bytes with
|
|
3275
|
+
// `===`, so a host that supplied nothing would be back to storing whatever a human typed —
|
|
3276
|
+
// the round-2 regression. Unlike the authority above there is no host without a checkout
|
|
3277
|
+
// root, so the absent case is a compile error rather than a second posture.
|
|
3278
|
+
// 394-07 D-12 / WTST-05: the honesty funnel's reads, bound HERE and therefore on BOTH hosts,
|
|
3279
|
+
// which is the whole point of 385 D-15 — a second binding site would be a second answer to
|
|
3280
|
+
// "how many drafts did this session keep", and that number is the one WTST-05 exists to keep
|
|
3281
|
+
// honest.
|
|
3282
|
+
//
|
|
3283
|
+
// UNCONDITIONAL, unlike the loop's two halves above it, and the asymmetry is the point: the
|
|
3284
|
+
// funnel is a READ over records this host owns by owning `agentQaRoot`, so every local
|
|
3285
|
+
// composition can answer it whether or not it can author anything. An empty receipts
|
|
3286
|
+
// directory and an absent one are the same answer, and nothing opens here — the first byte
|
|
3287
|
+
// is read when the verb is invoked.
|
|
3288
|
+
//
|
|
3289
|
+
// THE RECEIPTS PORT IS THE READ HALF ONLY. The evaluator one binding below is handed the
|
|
3290
|
+
// same store's `put`; this bundle names `list` and nothing else, so the projection is
|
|
3291
|
+
// structurally unable to write the records it reports on. Two narrow mirrors over one store
|
|
3292
|
+
// rather than one wide port.
|
|
3293
|
+
//
|
|
3294
|
+
// 2026-10-01 (432-17, D-06): the bundle no longer reads the proposals inbox. 2026-10-02
|
|
3295
|
+
// (quick 261002-ctr): the terminal stage is `writeEligible`, read off the receipts this
|
|
3296
|
+
// bundle lists (see `projectAuthoringFunnel`).
|
|
3297
|
+
//
|
|
3298
|
+
// `outcomes` IS BOUND (394-17, step b): the port's two verbs now LEAD with `product`, so the
|
|
3299
|
+
// binding reads the run-outcome projection on its own primary key instead of guessing a
|
|
3300
|
+
// partition or serving every product's history under one file path.
|
|
3301
|
+
//
|
|
3302
|
+
// ASSEMBLED ABOVE rather than inline, because the gate-2 flip path is handed the SAME
|
|
3303
|
+
// bundle: one composition-level answer to "what does this project know about this suite",
|
|
3304
|
+
// reached by the funnel read op and by the evaluator's consultation alike.
|
|
3305
|
+
authoringFunnel,
|
|
3306
|
+
// 2026-10-01 (432-17, D-05/D-22): the proposals inbox (391 PROP-03) is not bound — it is deleted
|
|
3307
|
+
// with its ops. Old files under `<agentQaRoot>/proposals/` are never opened again.
|
|
3308
|
+
...input.feedback === void 0 ? {} : { feedback: input.feedback },
|
|
3309
|
+
...input.feedbackReads === void 0 ? {} : { feedbackReads: input.feedbackReads },
|
|
3310
|
+
quarantine: (0, import_agent_qa_knowledge_local.createQuarantineOperations)({
|
|
3311
|
+
resolveKnowledgeRoot: input.knowledgeRoot,
|
|
3312
|
+
now: () => clock.now(),
|
|
3313
|
+
mintIdentityPath: input.mintQuarantineIdentityPath,
|
|
3314
|
+
...input.resolveWriterAuthority === void 0 ? {} : { resolveWriterAuthority: input.resolveWriterAuthority }
|
|
3315
|
+
}),
|
|
3316
|
+
// 392 D-24: the turn engine assembled above. Bound HERE and therefore on BOTH hosts, so
|
|
3317
|
+
// `agent.ask` is a real capability wherever the composition root runs — and unbound nowhere,
|
|
3318
|
+
// which is what lets 397's parity suite read it off the registry with no chat-specific case.
|
|
3319
|
+
chatTurn: {
|
|
3320
|
+
// The engine also echoes authored clause text on receipts and refusal answers. Mask
|
|
3321
|
+
// the complete outward projection after execution, never the message it dispatches.
|
|
3322
|
+
runTurn: async (turnInput) => projectChatMembers(await chatTurn.runTurn(turnInput), CHAT_TURN_RESULT_RULES, input.mask)
|
|
3323
|
+
},
|
|
3324
|
+
// 393 D-01 / D-15: the keep-gate evaluator's two host halves, bound HERE and therefore on
|
|
3325
|
+
// BOTH hosts — the CLI and 386's daemon compose this same root, which is the whole point of
|
|
3326
|
+
// 385 D-15 (a second binding site would be a second answer to "is this draft worth keeping",
|
|
3327
|
+
// and the two would disagree about a decision that gets written into a durable receipt).
|
|
3328
|
+
//
|
|
3329
|
+
// Conditionally spread on BOTH halves, because a capability that can parse but cannot run is
|
|
3330
|
+
// not a degraded evaluator — it is a host that must say `unbound` by name. `runs.start` is
|
|
3331
|
+
// dispatched through the registry this very call is about to produce, which is the same
|
|
3332
|
+
// deferred reference the chat engine takes and is safe for the same reason: no op can run
|
|
3333
|
+
// before the registry it is registered in has been built.
|
|
3334
|
+
...input.draftAuthoring === void 0 || input.runExecution === void 0 ? {} : {
|
|
3335
|
+
draftEvaluation: createDraftEvaluation({
|
|
3336
|
+
authoring: input.draftAuthoring,
|
|
3337
|
+
agentQaRoot: input.agentQaRoot,
|
|
3338
|
+
knowledgeRoot: input.knowledgeRoot,
|
|
3339
|
+
authoringFunnel,
|
|
3340
|
+
readRun: (runId) => runsReader.get(runId),
|
|
3341
|
+
startGateRun: async (request) => {
|
|
3342
|
+
if (composedRegistry === void 0) {
|
|
3343
|
+
throw new TypeError("a gate-run started before its registry finished composing");
|
|
3344
|
+
}
|
|
3345
|
+
return await (0, import_agent_qa_agent4.invokeFunction)(
|
|
3346
|
+
composedRegistry,
|
|
3347
|
+
import_agent_qa_agent4.RUNS_START_FUNCTION_ID,
|
|
3348
|
+
request,
|
|
3349
|
+
AUTHORING_GATE_RUN_CALLER
|
|
3350
|
+
);
|
|
3351
|
+
}
|
|
3352
|
+
})
|
|
3353
|
+
},
|
|
3354
|
+
// 394 D-01 / WTST-01: the authoring LOOP's two halves, bound HERE and therefore on BOTH
|
|
3355
|
+
// hosts, under the SAME condition the evaluator above is bound under — the loop's stage 4
|
|
3356
|
+
// dispatches `authoring.draft.verify`, so a host that cannot verify a draft cannot author
|
|
3357
|
+
// one either, and a third answer to "can this host author" would be a third thing to keep
|
|
3358
|
+
// in step.
|
|
3359
|
+
//
|
|
3360
|
+
// THE READ HALF IS `getAuthoringBrief`'S FIRST PRODUCTION CALLER. The port has been
|
|
3361
|
+
// implemented and unbound since v51 (`knowledge-local/src/serving/authoring-reader.ts`),
|
|
3362
|
+
// which means every field of its contract is crossing a real seam for the first time here;
|
|
3363
|
+
// a mismatch surfaces at this binding rather than in a surface. The reader is built PER
|
|
3364
|
+
// CALL over a freshly resolved knowledge root, on the standing-approval reader's stated
|
|
3365
|
+
// reasoning — the CLI resolves its root asynchronously and per call, and a root captured
|
|
3366
|
+
// once would serve a brief out of a knowledge tree the invocation is not serving from. No
|
|
3367
|
+
// `product` is bound: the composition does not own one, and the SCOPE arrives on the op's
|
|
3368
|
+
// input, which is the honest half of the pair the reader filters on.
|
|
3369
|
+
//
|
|
3370
|
+
// `suiteTextCorpus` is the SAME host walk gate 3's degraded lane already consumes, reached
|
|
3371
|
+
// here for stage 2's pre-authoring screen. One walk, one notion of what a suite test's text
|
|
3372
|
+
// is; a second enumeration would let the screen and the gate disagree about the same pair.
|
|
3373
|
+
//
|
|
3374
|
+
// THE WRITE HALF bundles the model, the shelf and the verify dispatch, because none of the
|
|
3375
|
+
// three is independently bindable: a host with a model and no shelf cannot author, one with
|
|
3376
|
+
// a shelf and no model has nothing to author with, and one that can do both but cannot
|
|
3377
|
+
// verify would produce a test and have nothing to say about whether it is worth keeping.
|
|
3378
|
+
// The model is a FACTORY, resolved at most once and only when a draft is actually compiled,
|
|
3379
|
+
// the same idiom `openJobs` uses — binding this capability must not construct a client for a
|
|
3380
|
+
// host that never authors.
|
|
3381
|
+
//
|
|
3382
|
+
// WHY THE ROUTER (plan 05) WILL READ `DraftRunPortLike.get`'s `CompletedRunHandoff` AND NOT
|
|
3383
|
+
// `runs.get`'s PROJECTION. D-08 routes on run EVIDENCE — step terminations, planning
|
|
3384
|
+
// failure, cycle flags, the per-step sub-action record — and `runs.get` projects a
|
|
3385
|
+
// `RunDetail` in which none of that survives (`stepCount` is its only step-shaped field).
|
|
3386
|
+
// This is `createDraftEvaluation`'s own posture, stated one layer out: the op is reached for
|
|
3387
|
+
// what an op owns, and the reader is read for evidence no projection carries. A router built
|
|
3388
|
+
// on the projection would have to infer a mechanics failure from a member verdict, and
|
|
3389
|
+
// inferring an app defect out of a bare member verdict is precisely the cardinal error D-08
|
|
3390
|
+
// exists to make unreachable.
|
|
3391
|
+
//
|
|
3392
|
+
// A NOTE FOR WHOEVER EDITS THIS PARAGRAPH. The arch dependency scan reads import specifiers
|
|
3393
|
+
// out of this file's RAW BYTES, comments included, so any prose shaped like an import's
|
|
3394
|
+
// trailing clause — the keyword, then a quoted phrase — is reported as an undeclared
|
|
3395
|
+
// external dependency of this package. Two drafts of this comment tripped it, once on a
|
|
3396
|
+
// sentence about verdicts and once on the sentence explaining the first. Keep quoted phrases
|
|
3397
|
+
// away from that keyword here.
|
|
3398
|
+
...authoringLoopHost === void 0 ? {} : {
|
|
3399
|
+
authoringRead: {
|
|
3400
|
+
async getAuthoringBrief(intent, scope) {
|
|
3401
|
+
const knowledgeRoot = await input.knowledgeRoot();
|
|
3402
|
+
return (0, import_agent_qa_knowledge_local.createAuthoringReader)({ knowledgeRoot }).getAuthoringBrief(intent, scope);
|
|
3403
|
+
},
|
|
3404
|
+
suiteTextCorpus: () => authoringLoopHost.suiteTextCorpus(),
|
|
3405
|
+
// 394-33 · WR-07: the `target` vocabulary stage 3's compile prompt serves. Bound off the
|
|
3406
|
+
// SAME port the brief and the corpus come from, so a host that can run the loop's read
|
|
3407
|
+
// stages can name its own targets by construction — see the port's own doc.
|
|
3408
|
+
configuredTargetNames: () => authoringLoopHost.configuredTargetNames()
|
|
3409
|
+
},
|
|
3410
|
+
// 394-26 · WINDOWS 110: the scope an intent-only call is authored at, bound HERE and so on
|
|
3411
|
+
// BOTH hosts, off the same port the read and compile halves come from. Its own deps key
|
|
3412
|
+
// rather than a third verb on `authoringRead`, because deriving a scope needs the resolved
|
|
3413
|
+
// config and the workspace while the brief needs a knowledge index — a host can have one
|
|
3414
|
+
// without the other, and the op refuses on THIS key by name when a scope-less call arrives
|
|
3415
|
+
// at a composition that never bound it.
|
|
3416
|
+
authoringScope: {
|
|
3417
|
+
deriveDefault: () => authoringLoopHost.deriveDefaultScope()
|
|
3418
|
+
},
|
|
3419
|
+
draftShelf: {
|
|
3420
|
+
model: async () => await input.model(),
|
|
3421
|
+
write: (contents) => authoringLoopHost.stageDraft(contents),
|
|
3422
|
+
// 394-06 D-09/D-20: the ONE RFC-6902 applier, reached at the host because it needs the
|
|
3423
|
+
// yaml CST and neither `agent` nor this package may depend on `core`. Same seam,
|
|
3424
|
+
// same reason and same shape as `stageDraft` right above it.
|
|
3425
|
+
applyPatches: (draft, patchSet) => authoringLoopHost.applyPatchesToDraft(draft, patchSet),
|
|
3426
|
+
// 432 D-06/D-07: the direct writer the loop's final write and the edit op reach. Same seam
|
|
3427
|
+
// and same reason as the two verbs above: the boundary check needs `core`.
|
|
3428
|
+
publish: (request) => authoringLoopHost.publishDraft(request),
|
|
3429
|
+
// 394-06: stage 5's evidence reader and the repair arm's probe runner. The reader is
|
|
3430
|
+
// the same `runsReader` the gate evaluator is bound to — a second reader would be a
|
|
3431
|
+
// second answer to what one run recorded — and the starter dispatches the REGISTERED
|
|
3432
|
+
// `runs.start` on the gate-run caller, so a probe is marker-stamped exactly as a gate
|
|
3433
|
+
// run is and is excluded from outcomes for the same reason.
|
|
3434
|
+
runs: {
|
|
3435
|
+
start: async (request) => {
|
|
3436
|
+
if (composedRegistry === void 0) {
|
|
3437
|
+
throw new TypeError("a repair probe started before its registry finished composing");
|
|
3438
|
+
}
|
|
3439
|
+
return await (0, import_agent_qa_agent4.invokeFunction)(
|
|
3440
|
+
composedRegistry,
|
|
3441
|
+
import_agent_qa_agent4.RUNS_START_FUNCTION_ID,
|
|
3442
|
+
request,
|
|
3443
|
+
AUTHORING_GATE_RUN_CALLER
|
|
3444
|
+
);
|
|
3445
|
+
},
|
|
3446
|
+
get: (runId) => runsReader.get(runId)
|
|
3447
|
+
},
|
|
3448
|
+
// 2026-10-01 (432-18/432-19, D-15): no dispatch-scoped grant exists. The authoring loop
|
|
3449
|
+
// runs its dispatch directly, and no gate run it starts consults any approval.
|
|
3450
|
+
verifyDraft: async (verifyInput) => {
|
|
3451
|
+
if (composedRegistry === void 0) {
|
|
3452
|
+
throw new TypeError("a draft was verified before its registry finished composing");
|
|
3453
|
+
}
|
|
3454
|
+
return await (0, import_agent_qa_agent4.invokeFunction)(
|
|
3455
|
+
composedRegistry,
|
|
3456
|
+
import_agent_qa_agent4.AUTHORING_DRAFT_VERIFY_FUNCTION_ID,
|
|
3457
|
+
verifyInput,
|
|
3458
|
+
AUTHORING_LOOP_VERIFY_CALLER
|
|
3459
|
+
);
|
|
3460
|
+
}
|
|
3461
|
+
}
|
|
3462
|
+
// 2026-10-01 (432-17, D-06): no authoring proposal writer is bound; an authored test is
|
|
3463
|
+
// written in its own call through the shelf's direct writer.
|
|
3464
|
+
},
|
|
3465
|
+
// 432-16 (D-07): no test-draft or config commit port and no undo source is bound — agent-qa
|
|
3466
|
+
// makes no git commits. The final authored test is written through `draftShelf.publish`.
|
|
3467
|
+
//
|
|
3468
|
+
// 395-01 D-01/D-03/D-17 · CFGM-01/03/04: the config editor's host half, each spread on its
|
|
3469
|
+
// OWN condition rather than as one bundle, so a host missing one reports that one `unbound`.
|
|
3470
|
+
...input.configFiles === void 0 ? {} : { configFiles: input.configFiles },
|
|
3471
|
+
...input.configValidate === void 0 ? {} : { configValidate: input.configValidate },
|
|
3472
|
+
...input.scaffoldContent === void 0 ? {} : { scaffoldContent: input.scaffoldContent },
|
|
3473
|
+
...input.configWrite === void 0 ? {} : { configWrite: input.configWrite }
|
|
3474
|
+
// 2026-10-14 (441-39, D-15): a config change is not scanned for secrets, so no scan is bound.
|
|
3475
|
+
// 2026-10-01 (432-17, D-05): no config proposal filer is bound; the six config ops write in
|
|
3476
|
+
// the call through `configWrite`.
|
|
3477
|
+
}
|
|
3478
|
+
// 2026-10-15 (441-28, D-15): no audit sink; the guardrail audit went with the guardrails.
|
|
3479
|
+
// 432-13 (D-14/D-15): no standing-approval reader, confirm port or session deny overlay reaches
|
|
3480
|
+
// the registry — `invokeFunction` asks nobody. 432-18 deleted the grant reader with the ledger.
|
|
3481
|
+
// 2026-10-02 (432-39, D-12): the host's execution-context answer is no longer forwarded here.
|
|
3482
|
+
// The registry's option was retired and unread since 432-13 and is deleted; the same answer
|
|
3483
|
+
// still reaches the knowledge review above, which records a direct change under it.
|
|
3484
|
+
});
|
|
3485
|
+
composedRegistry = registry;
|
|
3486
|
+
let opened;
|
|
3487
|
+
let opening;
|
|
3488
|
+
let closing;
|
|
3489
|
+
let lifecycleGeneration = 0;
|
|
3490
|
+
let closed = false;
|
|
3491
|
+
async function openJobs(openGeneration) {
|
|
3492
|
+
const model = await input.model();
|
|
3493
|
+
const knowledgeRoot = await input.knowledgeRoot();
|
|
3494
|
+
const analysisTokenBudget = input.analysisTokenBudget === void 0 ? void 0 : await input.analysisTokenBudget();
|
|
3495
|
+
const proposeGate1MaxPerJob = input.postRun?.proposeGate1MaxPerJob === void 0 ? void 0 : await input.postRun.proposeGate1MaxPerJob();
|
|
3496
|
+
const store = createSqliteJobStore({
|
|
3497
|
+
path: (0, import_node_path10.join)(input.agentQaRoot, AGENT_JOB_STORE_DB_FILE),
|
|
3498
|
+
clock,
|
|
3499
|
+
checkpoints: createJobCheckpointStore({ root: input.agentQaRoot })
|
|
3500
|
+
});
|
|
3501
|
+
let jobs;
|
|
3502
|
+
try {
|
|
3503
|
+
const kinds = input.jobKinds ?? (0, import_agent_qa_agent4.createRealJobKindRegistry)(
|
|
3504
|
+
postRunCapabilities(input, knowledgeRoot, analysisTokenBudget, store, proposeGate1MaxPerJob)
|
|
3505
|
+
);
|
|
3506
|
+
const idSource = input.idSource ?? DIGEST_JOB_ID_SOURCE;
|
|
3507
|
+
const unclaimableKinds = UNCLAIMABLE_BY_PROSE_POSTURE[input.postRun?.candidateProse ?? "no-masker"];
|
|
3508
|
+
jobs = {
|
|
3509
|
+
queue: (0, import_agent_qa_agent4.createAgentJobQueue)({
|
|
3510
|
+
store,
|
|
3511
|
+
registry: kinds,
|
|
3512
|
+
clock,
|
|
3513
|
+
idSource,
|
|
3514
|
+
model,
|
|
3515
|
+
...unclaimableKinds.length === 0 ? {} : { unclaimableKinds }
|
|
3516
|
+
}),
|
|
3517
|
+
store,
|
|
3518
|
+
kinds,
|
|
3519
|
+
model,
|
|
3520
|
+
idSource,
|
|
3521
|
+
unclaimableKinds
|
|
3522
|
+
};
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
store.close();
|
|
3525
|
+
throw error;
|
|
3526
|
+
}
|
|
3527
|
+
if (closed || openGeneration !== lifecycleGeneration) {
|
|
3528
|
+
store.close();
|
|
3529
|
+
throw new LocalAgentClosedError(input.agentQaRoot);
|
|
3530
|
+
}
|
|
3531
|
+
opened = jobs;
|
|
3532
|
+
return jobs;
|
|
3533
|
+
}
|
|
3534
|
+
return {
|
|
3535
|
+
registry,
|
|
3536
|
+
chat: {
|
|
3537
|
+
stateFor: (sessionId) => chatScopes.get(sessionId)?.state.read(),
|
|
3538
|
+
async abortActiveTurn() {
|
|
3539
|
+
for (const [sessionId, entry] of liveChatSessions) {
|
|
3540
|
+
const turnId = chatScopes.get(sessionId)?.state.read().activeTurnId;
|
|
3541
|
+
if (turnId === void 0) continue;
|
|
3542
|
+
if (!entry.turnIds.has(turnId)) continue;
|
|
3543
|
+
await entry.handle.append({
|
|
3544
|
+
kind: "aborted",
|
|
3545
|
+
turnId,
|
|
3546
|
+
at: clock.now(),
|
|
3547
|
+
source: "explicit"
|
|
3548
|
+
});
|
|
3549
|
+
await entry.handle.turnBoundary();
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
},
|
|
3553
|
+
curation: LOCAL_CURATION_CAPABILITIES,
|
|
3554
|
+
clock,
|
|
3555
|
+
jobs() {
|
|
3556
|
+
if (closed) return Promise.reject(new LocalAgentClosedError(input.agentQaRoot));
|
|
3557
|
+
opening ??= openJobs(lifecycleGeneration);
|
|
3558
|
+
return opening;
|
|
3559
|
+
},
|
|
3560
|
+
close() {
|
|
3561
|
+
if (closing !== void 0) return closing;
|
|
3562
|
+
closed = true;
|
|
3563
|
+
lifecycleGeneration += 1;
|
|
3564
|
+
const pending = opening;
|
|
3565
|
+
const current = opened;
|
|
3566
|
+
opened = void 0;
|
|
3567
|
+
current?.store.close();
|
|
3568
|
+
closing = (async () => {
|
|
3569
|
+
if (pending !== void 0) {
|
|
3570
|
+
try {
|
|
3571
|
+
const late = await pending;
|
|
3572
|
+
if (late !== current) late.store.close();
|
|
3573
|
+
} catch {
|
|
3574
|
+
}
|
|
3575
|
+
}
|
|
3576
|
+
opening = void 0;
|
|
3577
|
+
})();
|
|
3578
|
+
return closing;
|
|
3579
|
+
}
|
|
3580
|
+
};
|
|
3581
|
+
}
|
|
3582
|
+
|
|
3583
|
+
// src/composition/build-ordinals.ts
|
|
3584
|
+
var import_agent_qa_runner_local4 = require("@vostride/agent-qa-runner-local");
|
|
3585
|
+
var import_agent_qa_runner_local5 = require("@vostride/agent-qa-runner-local");
|
|
3586
|
+
|
|
3587
|
+
// src/composition/compose-job-recovery.ts
|
|
3588
|
+
var import_node_path11 = require("node:path");
|
|
3589
|
+
var import_agent_qa_agent5 = require("@vostride/agent-qa-agent");
|
|
3590
|
+
var JOB_RECOVERY_MAX_RESUMED_JOBS = 64;
|
|
3591
|
+
function composeJobRecovery(input) {
|
|
3592
|
+
const store = createSqliteJobStore({
|
|
3593
|
+
path: (0, import_node_path11.join)(input.agentQaRoot, AGENT_JOB_STORE_DB_FILE),
|
|
3594
|
+
clock: input.clock,
|
|
3595
|
+
checkpoints: createJobCheckpointStore({ root: input.agentQaRoot })
|
|
3596
|
+
});
|
|
3597
|
+
const kinds = input.jobKinds ?? (0, import_agent_qa_agent5.createRealJobKindRegistry)();
|
|
3598
|
+
const queue = (0, import_agent_qa_agent5.createAgentJobQueue)({
|
|
3599
|
+
store,
|
|
3600
|
+
registry: kinds,
|
|
3601
|
+
clock: input.clock,
|
|
3602
|
+
idSource: input.idSource,
|
|
3603
|
+
model: input.model
|
|
3604
|
+
});
|
|
3605
|
+
return {
|
|
3606
|
+
store,
|
|
3607
|
+
kinds,
|
|
3608
|
+
queue,
|
|
3609
|
+
recoverAbandonedClaims(fence) {
|
|
3610
|
+
return queue.recoverAbandonedClaims({ fence });
|
|
3611
|
+
},
|
|
3612
|
+
async resumePending(fence, maxJobs = JOB_RECOVERY_MAX_RESUMED_JOBS) {
|
|
3613
|
+
const outcomes = [];
|
|
3614
|
+
for (; ; ) {
|
|
3615
|
+
if (outcomes.length >= maxJobs) {
|
|
3616
|
+
throw new Error(
|
|
3617
|
+
`agent job recovery ran ${maxJobs} jobs without emptying the agent job queue; the bound exists so a chain that keeps enqueuing successors cannot turn recovery into an unbounded drain, and stopping early without saying so would report success over work left behind`
|
|
3618
|
+
);
|
|
3619
|
+
}
|
|
3620
|
+
const outcome = await queue.runNextJob({ fence });
|
|
3621
|
+
if (!outcome.ran) return outcomes;
|
|
3622
|
+
outcomes.push(outcome);
|
|
3623
|
+
}
|
|
3624
|
+
},
|
|
3625
|
+
close() {
|
|
3626
|
+
store.close();
|
|
3627
|
+
}
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
// src/session/memory-session-log.ts
|
|
3632
|
+
var import_agent_qa_agent6 = require("@vostride/agent-qa-agent");
|
|
3633
|
+
var import_agent_qa_schema11 = require("@vostride/agent-qa-schema");
|
|
3634
|
+
function createMemorySessionLog(options) {
|
|
3635
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
3636
|
+
const random = options.random ?? Math.random;
|
|
3637
|
+
const liveSessions = /* @__PURE__ */ new Set();
|
|
3638
|
+
function handleFor(id, opened) {
|
|
3639
|
+
let live = true;
|
|
3640
|
+
const requireLive = (verb) => {
|
|
3641
|
+
if (!live) {
|
|
3642
|
+
throw new TypeError(
|
|
3643
|
+
`a chat session ${verb} cannot go through a closed handle for ${id}. Open the session again to write to it.`
|
|
3644
|
+
);
|
|
3645
|
+
}
|
|
3646
|
+
};
|
|
3647
|
+
liveSessions.add(id);
|
|
3648
|
+
return {
|
|
3649
|
+
header: opened.header,
|
|
3650
|
+
events: opened.events,
|
|
3651
|
+
refusedLines: opened.refusedLines,
|
|
3652
|
+
append(event) {
|
|
3653
|
+
requireLive("event");
|
|
3654
|
+
sessions.get(id).events.push(event);
|
|
3655
|
+
return Promise.resolve();
|
|
3656
|
+
},
|
|
3657
|
+
turnBoundary() {
|
|
3658
|
+
requireLive("turn boundary");
|
|
3659
|
+
return Promise.resolve();
|
|
3660
|
+
},
|
|
3661
|
+
close() {
|
|
3662
|
+
if (live) {
|
|
3663
|
+
live = false;
|
|
3664
|
+
liveSessions.delete(id);
|
|
3665
|
+
}
|
|
3666
|
+
return Promise.resolve();
|
|
3667
|
+
}
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
return {
|
|
3671
|
+
open(sessionId) {
|
|
3672
|
+
if (sessionId !== void 0) {
|
|
3673
|
+
const existing = sessions.get(sessionId);
|
|
3674
|
+
if (existing === void 0) throw new UnknownChatSessionError(sessionId);
|
|
3675
|
+
if (liveSessions.has(sessionId)) throw new ChatSessionBusyError(sessionId);
|
|
3676
|
+
const repair = (0, import_agent_qa_agent6.repairSession)(
|
|
3677
|
+
existing.events.map((event, index) => ({
|
|
3678
|
+
lineNumber: index + 2,
|
|
3679
|
+
text: JSON.stringify(event)
|
|
3680
|
+
})),
|
|
3681
|
+
{ declaredVersion: existing.header.schemaVersion, now: options.now }
|
|
3682
|
+
);
|
|
3683
|
+
existing.events.push(...repair.synthesized);
|
|
3684
|
+
return Promise.resolve(
|
|
3685
|
+
handleFor(sessionId, {
|
|
3686
|
+
header: existing.header,
|
|
3687
|
+
events: [...existing.events],
|
|
3688
|
+
// Always empty here, and that is the honest answer rather than a placeholder: this
|
|
3689
|
+
// twin stores typed records, so no line of it can be damaged in the way a file's can.
|
|
3690
|
+
refusedLines: repair.refusedLines
|
|
3691
|
+
})
|
|
3692
|
+
);
|
|
3693
|
+
}
|
|
3694
|
+
const createdAt = options.now();
|
|
3695
|
+
const id = mintChatSessionId(createdAt, random);
|
|
3696
|
+
const header = {
|
|
3697
|
+
schemaVersion: import_agent_qa_schema11.CURRENT_CHAT_SESSION_SCHEMA_VERSION,
|
|
3698
|
+
sessionId: id,
|
|
3699
|
+
createdAt,
|
|
3700
|
+
model: options.model(),
|
|
3701
|
+
toolsetHash: options.toolsetHash()
|
|
3702
|
+
};
|
|
3703
|
+
sessions.set(id, { header, events: [] });
|
|
3704
|
+
return Promise.resolve(handleFor(id, { header, events: [], refusedLines: [] }));
|
|
3705
|
+
},
|
|
3706
|
+
snapshot(sessionId) {
|
|
3707
|
+
const found = sessions.get(sessionId);
|
|
3708
|
+
return found === void 0 ? void 0 : { header: found.header, events: [...found.events], refusedLines: [] };
|
|
3709
|
+
}
|
|
3710
|
+
};
|
|
3711
|
+
}
|
|
3712
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
3713
|
+
0 && (module.exports = {
|
|
3714
|
+
ALLOW_PRODUCTION_OPT_IN,
|
|
3715
|
+
BRIEF_CEILING,
|
|
3716
|
+
BRIEF_FRACTION,
|
|
3717
|
+
BuildOrdinalReservationError,
|
|
3718
|
+
CAUTION_FRACTION,
|
|
3719
|
+
CHAT_PLANNER_CALLER,
|
|
3720
|
+
CHAT_RESOLVER_CALLER,
|
|
3721
|
+
CHAT_SESSIONS_SUBDIR,
|
|
3722
|
+
ChatSessionBusyError,
|
|
3723
|
+
ChatSessionHeaderError,
|
|
3724
|
+
DIGEST_JOB_ID_SOURCE,
|
|
3725
|
+
DRAIN_BUDGET_MS,
|
|
3726
|
+
EMBEDDER_WORKER_REGISTRATION_FILE,
|
|
3727
|
+
EMBEDDER_WORKER_START_TIMEOUT_MS,
|
|
3728
|
+
EMBEDDER_WORKER_TERM_GRACE_MS,
|
|
3729
|
+
ENTITY_INDEX_SWEEP_INTERVAL_MS,
|
|
3730
|
+
EmbedderWorkerTimeoutError,
|
|
3731
|
+
GLOBAL_RECORD_CAP,
|
|
3732
|
+
InvalidChatSessionIdError,
|
|
3733
|
+
JOB_RECOVERY_MAX_RESUMED_JOBS,
|
|
3734
|
+
LOCAL_CURATION_CAPABILITIES,
|
|
3735
|
+
LOCAL_DAEMON_CAPABILITIES,
|
|
3736
|
+
LOCAL_KNOWLEDGE_PROBES,
|
|
3737
|
+
LocalAgentClosedError,
|
|
3738
|
+
MIN_BRIEF,
|
|
3739
|
+
MIN_CAUTION,
|
|
3740
|
+
MIN_MEMORY_TOOL_CONTEXT_WINDOW,
|
|
3741
|
+
QUARANTINE_CLASS_DISPOSITIONS,
|
|
3742
|
+
QUARANTINE_FLOOR_ELIGIBILITY,
|
|
3743
|
+
READ_BODY_FRACTION,
|
|
3744
|
+
REFUSED_RUN_REQUEST_FIELDS,
|
|
3745
|
+
RunDeniedError,
|
|
3746
|
+
RunIdAlreadyInUseError,
|
|
3747
|
+
RunIdNotMintedError,
|
|
3748
|
+
RunProducedNoEventsError,
|
|
3749
|
+
RunRequestFieldRefusedError,
|
|
3750
|
+
SUITE_LIFECYCLE_LANE,
|
|
3751
|
+
SYSTEM_CLOCK,
|
|
3752
|
+
SuiteBoundaryWhileLiveError,
|
|
3753
|
+
UNCLAIMABLE_BY_PROSE_POSTURE,
|
|
3754
|
+
UnknownChatSessionError,
|
|
3755
|
+
WEAK_MODEL_CAP_SCALE,
|
|
3756
|
+
applyKnowledgeChange,
|
|
3757
|
+
buildMemberRunConfig,
|
|
3758
|
+
buildProductionRefusal,
|
|
3759
|
+
buildRunPorts,
|
|
3760
|
+
buildScreenshotCompressor,
|
|
3761
|
+
buildSuiteMemberExecutor,
|
|
3762
|
+
composeJobRecovery,
|
|
3763
|
+
composeLocalAgent,
|
|
3764
|
+
computeKnowledgeBudgets,
|
|
3765
|
+
computeRunConfigDigests,
|
|
3766
|
+
constructKnowledgeReader,
|
|
3767
|
+
createEvidenceBlobReader,
|
|
3768
|
+
createFrozenKnowledgeReader,
|
|
3769
|
+
createJsonlSessionLog,
|
|
3770
|
+
createKnowledgeReader,
|
|
3771
|
+
createKnowledgeReview,
|
|
3772
|
+
createLocalRunInvocation,
|
|
3773
|
+
createMemorySessionLog,
|
|
3774
|
+
createRunInvocationCore,
|
|
3775
|
+
createRunsReader,
|
|
3776
|
+
createSuiteLifecycle,
|
|
3777
|
+
latestBuildOrdinal,
|
|
3778
|
+
listSessions,
|
|
3779
|
+
lookupEvidenceBlob,
|
|
3780
|
+
mapVerdictToExitCode,
|
|
3781
|
+
memoryToolGate,
|
|
3782
|
+
mintChatSessionId,
|
|
3783
|
+
openRunKnowledgeComposition,
|
|
3784
|
+
readEvidenceBlobBytes,
|
|
3785
|
+
refuseProductionWithoutOptIn,
|
|
3786
|
+
refuseUnavailableRunId,
|
|
3787
|
+
registerAdapterLiveStream,
|
|
3788
|
+
reserveBuildOrdinalForProducts,
|
|
3789
|
+
resolveBuildTag,
|
|
3790
|
+
resolveBuildTagSetting,
|
|
3791
|
+
resolveHostRetrievalRung,
|
|
3792
|
+
resolveRunBuildOrdinal,
|
|
3793
|
+
resolveRunExecutionContext,
|
|
3794
|
+
resolveRunQuarantineSnapshot,
|
|
3795
|
+
runSafetyScope,
|
|
3796
|
+
scaledRecordCap,
|
|
3797
|
+
statEvidenceBlob,
|
|
3798
|
+
sweepEntityIndexProjection,
|
|
3799
|
+
toMemberTest
|
|
3800
|
+
});
|
|
3801
|
+
//# sourceMappingURL=index.cjs.map
|