cloving 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +121 -21
- package/assets/watch-fonts/fira-code-OFL.md +93 -0
- package/assets/watch-fonts/fira-code-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/fira-code-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-OFL.md +93 -0
- package/assets/watch-fonts/heebo-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-700-normal.woff2 +0 -0
- package/assets/watch-fonts/heebo-latin-800-normal.woff2 +0 -0
- package/assets/watch-fonts/manifest.json +112 -0
- package/assets/watch-fonts/space-grotesk-OFL.md +93 -0
- package/assets/watch-fonts/space-grotesk-latin-400-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-500-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-600-normal.woff2 +0 -0
- package/assets/watch-fonts/space-grotesk-latin-700-normal.woff2 +0 -0
- package/dist/src/authority/commands.d.ts +1 -0
- package/dist/src/authority/commands.js +25 -4
- package/dist/src/authority/dispatch-decision.d.ts +1 -0
- package/dist/src/authority/dispatch-decision.js +7 -4
- package/dist/src/authority/events.js +2 -2
- package/dist/src/authority/handoff-chain.d.ts +24 -0
- package/dist/src/authority/handoff-chain.js +46 -0
- package/dist/src/authority/reconciliation.d.ts +1 -0
- package/dist/src/authority/reconciliation.js +18 -11
- package/dist/src/authority/transitions.d.ts +1 -1
- package/dist/src/authority/transitions.js +3 -0
- package/dist/src/broker/actor/authority-queries.d.ts +4 -0
- package/dist/src/broker/actor/authority-queries.js +51 -2
- package/dist/src/broker/actor/delivery-intents.d.ts +1 -0
- package/dist/src/broker/actor/effect-delivery.js +9 -0
- package/dist/src/broker/authority-admission.js +13 -5
- package/dist/src/chats/analysis-batches.d.ts +18 -0
- package/dist/src/chats/analysis-batches.js +15 -1
- package/dist/src/chats/analysis-checkpoint.d.ts +29 -0
- package/dist/src/chats/analysis-checkpoint.js +31 -0
- package/dist/src/chats/analysis-input.d.ts +7 -1
- package/dist/src/chats/analysis-input.js +20 -6
- package/dist/src/chats/analysis-merge.d.ts +16 -0
- package/dist/src/chats/analysis-merge.js +134 -0
- package/dist/src/chats/analysis-prompt.d.ts +8 -0
- package/dist/src/chats/analysis-prompt.js +27 -1
- package/dist/src/chats/analysis-proposal.d.ts +2 -0
- package/dist/src/chats/analysis-proposal.js +9 -0
- package/dist/src/chats/analysis-shape.d.ts +1 -0
- package/dist/src/chats/analysis-shape.js +11 -4
- package/dist/src/chats/reference-mapping.d.ts +16 -0
- package/dist/src/chats/reference-mapping.js +153 -0
- package/dist/src/chats/workflow-generation-projection.d.ts +8 -0
- package/dist/src/chats/workflow-generation-projection.js +75 -0
- package/dist/src/chats/workflow-generation-source.d.ts +25 -0
- package/dist/src/chats/workflow-generation-source.js +124 -0
- package/dist/src/chats/workflow-generation-text.d.ts +23 -0
- package/dist/src/chats/workflow-generation-text.js +74 -0
- package/dist/src/chats/workflow-generation.d.ts +69 -0
- package/dist/src/chats/workflow-generation.js +142 -0
- package/dist/src/cli/render.d.ts +1 -0
- package/dist/src/cli/render.js +60 -13
- package/dist/src/cli/usage.js +5 -2
- package/dist/src/cli/watch-board-view.d.ts +12 -1
- package/dist/src/cli/watch-board-view.js +57 -11
- package/dist/src/cli/watch-board.d.ts +7 -1
- package/dist/src/cli/watch-board.js +42 -10
- package/dist/src/cli/watch-client.d.ts +1 -1
- package/dist/src/cli/watch-client.js +7 -3
- package/dist/src/cli/watch-model.d.ts +10 -4
- package/dist/src/cli/watch-model.js +35 -58
- package/dist/src/cli/watch-page.js +21 -4
- package/dist/src/cli/watch-phases.js +3 -3
- package/dist/src/cli/watch-pr-command.d.ts +5 -2
- package/dist/src/cli/watch-pr-command.js +41 -4
- package/dist/src/cli/watch-pr-identity.d.ts +14 -2
- package/dist/src/cli/watch-pr-identity.js +25 -0
- package/dist/src/cli/watch-pr-observer.d.ts +1 -0
- package/dist/src/cli/watch-pr-observer.js +48 -6
- package/dist/src/cli/watch-pull-request.d.ts +10 -0
- package/dist/src/cli/watch-pull-request.js +130 -0
- package/dist/src/cli/watch-reads.d.ts +7 -1
- package/dist/src/cli/watch-reads.js +42 -20
- package/dist/src/cli/watch-render.d.ts +14 -1
- package/dist/src/cli/watch-render.js +2 -1
- package/dist/src/cli/watch-server.js +33 -2
- package/dist/src/cli/watch-turn.js +1 -1
- package/dist/src/cli/watch-view.d.ts +7 -0
- package/dist/src/cli/watch-view.js +51 -3
- package/dist/src/daemon/analysis-worker.d.ts +0 -2
- package/dist/src/daemon/analysis-worker.js +71 -19
- package/dist/src/daemon/chats-batch-binding.d.ts +23 -0
- package/dist/src/daemon/chats-batch-binding.js +99 -0
- package/dist/src/daemon/chats-capture.d.ts +18 -0
- package/dist/src/daemon/chats-capture.js +194 -0
- package/dist/src/daemon/chats-checkpoints.d.ts +41 -0
- package/dist/src/daemon/chats-checkpoints.js +186 -0
- package/dist/src/daemon/chats-files.d.ts +29 -2
- package/dist/src/daemon/chats-files.js +142 -3
- package/dist/src/daemon/chats-follow-up.d.ts +38 -0
- package/dist/src/daemon/chats-follow-up.js +180 -0
- package/dist/src/daemon/chats-question.d.ts +15 -0
- package/dist/src/daemon/chats-question.js +93 -0
- package/dist/src/daemon/chats-worker.d.ts +1 -0
- package/dist/src/daemon/chats-worker.js +52 -51
- package/dist/src/daemon/claude-turn.d.ts +2 -1
- package/dist/src/daemon/claude-turn.js +1 -1
- package/dist/src/daemon/claude-worker.d.ts +0 -2
- package/dist/src/daemon/claude-worker.js +6 -11
- package/dist/src/daemon/daemon.d.ts +5 -1
- package/dist/src/daemon/daemon.js +38 -10
- package/dist/src/daemon/delivery-router.js +6 -0
- package/dist/src/daemon/github-calls.d.ts +4 -0
- package/dist/src/daemon/github-calls.js +16 -7
- package/dist/src/daemon/github-ci.d.ts +6 -0
- package/dist/src/daemon/github-ci.js +170 -0
- package/dist/src/daemon/github-merge-call.d.ts +14 -0
- package/dist/src/daemon/github-merge-call.js +73 -0
- package/dist/src/daemon/github-merge.d.ts +3 -5
- package/dist/src/daemon/github-merge.js +52 -95
- package/dist/src/daemon/github-methods.d.ts +28 -0
- package/dist/src/daemon/github-methods.js +80 -16
- package/dist/src/daemon/github-operation.d.ts +2 -0
- package/dist/src/daemon/github-retry.d.ts +6 -0
- package/dist/src/daemon/github-retry.js +49 -0
- package/dist/src/daemon/github-ticket-status.js +27 -1
- package/dist/src/daemon/github-worker.js +2 -1
- package/dist/src/daemon/handoff-worker.d.ts +10 -0
- package/dist/src/daemon/handoff-worker.js +232 -0
- package/dist/src/daemon/issue-queue.d.ts +1 -0
- package/dist/src/daemon/issue-queue.js +94 -42
- package/dist/src/daemon/notify-journal.d.ts +18 -0
- package/dist/src/daemon/notify-journal.js +108 -0
- package/dist/src/daemon/notify-message.js +2 -1
- package/dist/src/daemon/notify.js +176 -37
- package/dist/src/daemon/retrying-turn.d.ts +3 -0
- package/dist/src/daemon/retrying-turn.js +55 -9
- package/dist/src/daemon/ticket-dependency-waits.d.ts +18 -0
- package/dist/src/daemon/ticket-dependency-waits.js +60 -0
- package/dist/src/daemon/ticket-registration.d.ts +8 -4
- package/dist/src/daemon/ticket-registration.js +76 -22
- package/dist/src/daemon/ticket-source.d.ts +4 -1
- package/dist/src/daemon/ticket-source.js +8 -2
- package/dist/src/decision-visibility.d.ts +2 -0
- package/dist/src/decision-visibility.js +10 -0
- package/dist/src/execution-pin.d.ts +4 -0
- package/dist/src/execution-pin.js +23 -2
- package/dist/src/host/start-delivery.d.ts +2 -1
- package/dist/src/host/start-delivery.js +1 -1
- package/dist/src/host/step-bridge-host.d.ts +1 -0
- package/dist/src/host/step-bridge-host.js +16 -0
- package/dist/src/host/world-reconciliation.d.ts +1 -0
- package/dist/src/host/world-reconciliation.js +3 -0
- package/dist/src/host/world-runs.js +5 -0
- package/dist/src/issue-identity.d.ts +6 -0
- package/dist/src/issue-identity.js +23 -0
- package/dist/src/service/queue-runs.d.ts +3 -0
- package/dist/src/service/queue-runs.js +39 -0
- package/dist/src/service/registry.d.ts +8 -2
- package/dist/src/service/registry.js +20 -8
- package/dist/src/service/replies.d.ts +2 -0
- package/dist/src/service/run-handlers.js +44 -12
- package/dist/src/service/service.d.ts +3 -0
- package/dist/src/service/service.js +3 -1
- package/dist/src/service/submission-handler.d.ts +1 -0
- package/dist/src/service/submission-handler.js +92 -13
- package/dist/src/service/worker-totals.js +25 -1
- package/dist/src/step-bridge.d.ts +9 -0
- package/dist/src/steps/handoff.d.ts +2 -0
- package/dist/src/steps/handoff.js +13 -0
- package/dist/src/ticket-dependencies.d.ts +1 -0
- package/dist/src/ticket-dependencies.js +15 -0
- package/dist/src/ticket-status.js +7 -4
- package/dist/src/token-prices.d.ts +2 -0
- package/dist/src/token-prices.js +41 -9
- package/dist/src/worker-usage.d.ts +9 -0
- package/dist/src/worker-usage.js +1 -1
- package/dist/src/workflow-admission.d.ts +1 -0
- package/dist/src/workflow-candidate.js +1 -0
- package/dist/src/workflow-contracts.d.ts +2 -0
- package/dist/src/workflow-contracts.js +8 -3
- package/dist/src/workflow-declaration.d.ts +11 -0
- package/dist/src/workflow-declaration.js +71 -15
- package/dist/src/workflow-discovery.d.ts +18 -0
- package/dist/src/workflow-discovery.js +33 -0
- package/dist/src/workflow-refusals.d.ts +6 -0
- package/dist/src/workflow-refusals.js +39 -0
- package/dist/src/workflows.d.ts +2 -1
- package/dist/src/workflows.js +1 -0
- package/dist/tools/admit-workflow.d.ts +13 -0
- package/dist/tools/admit-workflow.js +94 -0
- package/dist/tools/self-host-project.d.ts +17 -0
- package/dist/tools/self-host-project.js +177 -0
- package/dist/tools/self-host.js +27 -81
- package/dist/tools/workflow-bundle-dependencies.d.ts +5 -0
- package/dist/tools/workflow-bundle-dependencies.js +93 -0
- package/dist/tools/workflow-bundles.d.ts +20 -0
- package/dist/tools/workflow-bundles.js +173 -0
- package/dist/tools/workflow-candidate-counts.d.ts +8 -0
- package/dist/tools/workflow-candidate-counts.js +251 -0
- package/dist/tools/workflow-candidate-program.d.ts +11 -0
- package/dist/tools/workflow-candidate-program.js +78 -0
- package/dist/tools/workflow-candidate-source.d.ts +6 -0
- package/dist/tools/workflow-candidate-source.js +107 -0
- package/dist/tools/workflow-candidate-syntax.d.ts +15 -0
- package/dist/tools/workflow-candidate-syntax.js +219 -0
- package/dist/tools/workflow-folder-capture.d.ts +17 -0
- package/dist/tools/workflow-folder-capture.js +216 -0
- package/dist/tools/workflow-folders.d.ts +33 -0
- package/dist/tools/workflow-folders.js +150 -0
- package/dist/tools/workflow-source-effects.d.ts +10 -0
- package/dist/tools/workflow-source-effects.js +213 -0
- package/dist/workflows/analyze-chats.d.ts +6 -1
- package/dist/workflows/analyze-chats.declaration.json +14 -1
- package/dist/workflows/analyze-chats.js +188 -11
- package/dist/workflows/develop-prd.js +71 -3
- package/dist/workflows/develop-ticket.js +5 -0
- package/dist/workflows/ticket-prompts.js +12 -2
- package/package.json +21 -3
|
@@ -1,8 +1,50 @@
|
|
|
1
1
|
import { lstat } from "node:fs/promises";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { record } from "../protocol.js";
|
|
3
5
|
import { openInput } from "../chats/input-file.js";
|
|
4
6
|
import { createPrivateFile, validateDestination, } from "../chats/destination.js";
|
|
5
7
|
import { analysisDocumentBytes, } from "../chats/analysis-input.js";
|
|
8
|
+
import { analysisCaptureBytes } from "../chats/analysis-batches.js";
|
|
9
|
+
export async function readAnalysisCorpus(path, options = {}) {
|
|
10
|
+
const handle = await openInput(path);
|
|
11
|
+
let output;
|
|
12
|
+
try {
|
|
13
|
+
if (options.permissions !== undefined &&
|
|
14
|
+
((await handle.stat()).mode & 0o777) !== options.permissions)
|
|
15
|
+
throw new Error("analysis-output-failed");
|
|
16
|
+
if (options.output !== undefined)
|
|
17
|
+
output = await createPrivateFile(options.output);
|
|
18
|
+
const decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
|
19
|
+
const buffer = Buffer.alloc(65_536);
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let size = 0;
|
|
22
|
+
for (;;) {
|
|
23
|
+
const { bytesRead } = await handle.read({
|
|
24
|
+
buffer,
|
|
25
|
+
length: Math.min(buffer.length, analysisCaptureBytes + 1 - size),
|
|
26
|
+
});
|
|
27
|
+
if (bytesRead === 0)
|
|
28
|
+
break;
|
|
29
|
+
size += bytesRead;
|
|
30
|
+
if (size > analysisCaptureBytes)
|
|
31
|
+
throw new Error("analysis-too-large");
|
|
32
|
+
if (output !== undefined)
|
|
33
|
+
await output.writeFile(buffer.subarray(0, bytesRead));
|
|
34
|
+
chunks.push(decoder.decode(buffer.subarray(0, bytesRead), { stream: true }));
|
|
35
|
+
}
|
|
36
|
+
chunks.push(decoder.decode());
|
|
37
|
+
return chunks.join("");
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
try {
|
|
41
|
+
await handle.close();
|
|
42
|
+
}
|
|
43
|
+
finally {
|
|
44
|
+
await output?.close();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
6
48
|
export async function analysisDirectory(runtime, importId) {
|
|
7
49
|
if (process.platform !== "darwin")
|
|
8
50
|
throw new Error("platform-unsupported");
|
|
@@ -48,10 +90,13 @@ export async function boundedAnalysisFile(path, options = {}) {
|
|
|
48
90
|
await handle.close();
|
|
49
91
|
}
|
|
50
92
|
}
|
|
51
|
-
export async function readAnalysisSnapshot(directory,
|
|
93
|
+
export async function readAnalysisSnapshot(directory, input) {
|
|
94
|
+
const { importId, ...options } = input;
|
|
52
95
|
const required = async (name) => {
|
|
53
96
|
try {
|
|
54
|
-
return
|
|
97
|
+
return name === "redacted.jsonl"
|
|
98
|
+
? await readAnalysisCorpus(join(directory, name), options)
|
|
99
|
+
: await boundedAnalysisFile(join(directory, name), options);
|
|
55
100
|
}
|
|
56
101
|
catch (error) {
|
|
57
102
|
if (Reflect.get(Object(error), "code") === "ENOENT")
|
|
@@ -63,7 +108,7 @@ export async function readAnalysisSnapshot(directory, importId) {
|
|
|
63
108
|
importId,
|
|
64
109
|
redaction: await required("redaction.json"),
|
|
65
110
|
corpus: await required("redacted.jsonl"),
|
|
66
|
-
structure: await boundedAnalysisFile(join(directory, "structure.json")),
|
|
111
|
+
structure: await boundedAnalysisFile(join(directory, "structure.json"), options),
|
|
67
112
|
};
|
|
68
113
|
}
|
|
69
114
|
export async function matchingPrivateFile(path, text) {
|
|
@@ -85,7 +130,50 @@ export async function matchingPrivateFile(path, text) {
|
|
|
85
130
|
await handle.close();
|
|
86
131
|
}
|
|
87
132
|
}
|
|
133
|
+
async function requireAbsent(directory, names) {
|
|
134
|
+
for (const name of names) {
|
|
135
|
+
const exists = await lstat(join(directory, name)).catch((error) => {
|
|
136
|
+
if (error instanceof Error &&
|
|
137
|
+
"code" in error &&
|
|
138
|
+
error.code === "ENOENT")
|
|
139
|
+
return undefined;
|
|
140
|
+
throw error;
|
|
141
|
+
});
|
|
142
|
+
if (exists !== undefined)
|
|
143
|
+
throw new Error("analysis-output-failed");
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
function outputClaim(directory, output) {
|
|
147
|
+
return {
|
|
148
|
+
path: join(directory, `${"draft" in output ? "final-claim" : "claim"}-${output.cycleId}.json`),
|
|
149
|
+
text: JSON.stringify({
|
|
150
|
+
schemaVersion: 1,
|
|
151
|
+
kind: "reply" in output ? "draft" : "final",
|
|
152
|
+
digest: createHash("sha256")
|
|
153
|
+
.update(JSON.stringify(output))
|
|
154
|
+
.digest("hex"),
|
|
155
|
+
}) + "\n",
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
async function inspectOutputClaim(claim) {
|
|
159
|
+
const text = await boundedAnalysisFile(claim.path, {
|
|
160
|
+
permissions: 0o600,
|
|
161
|
+
}).catch((error) => {
|
|
162
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
163
|
+
return undefined;
|
|
164
|
+
throw error;
|
|
165
|
+
});
|
|
166
|
+
if (text !== undefined && text !== claim.text)
|
|
167
|
+
throw new Error("analysis-output-failed");
|
|
168
|
+
}
|
|
88
169
|
export async function inspectAnalysisFiles(directory, output) {
|
|
170
|
+
if (Buffer.byteLength(JSON.stringify(output)) > analysisDocumentBytes)
|
|
171
|
+
throw new Error("analysis-too-large");
|
|
172
|
+
await inspectOutputClaim(outputClaim(directory, output));
|
|
173
|
+
if (output.draft === undefined)
|
|
174
|
+
await requireAbsent(directory, [`draft-${output.cycleId}.json`]);
|
|
175
|
+
else if ((await inspectAnalysisDraftRecord(directory, output.draft)) === undefined)
|
|
176
|
+
throw new Error("analysis-output-failed");
|
|
89
177
|
const proposal = join(directory, `proposal-${output.cycleId}.txt`);
|
|
90
178
|
const analysis = join(directory, `analysis-${output.cycleId}.json`);
|
|
91
179
|
const marker = await boundedAnalysisFile(analysis, {
|
|
@@ -106,8 +194,59 @@ export async function inspectAnalysisFiles(directory, output) {
|
|
|
106
194
|
export async function saveAnalysisFiles(directory, output) {
|
|
107
195
|
if ((await inspectAnalysisFiles(directory, output)) === "complete")
|
|
108
196
|
return;
|
|
197
|
+
const claim = outputClaim(directory, output);
|
|
198
|
+
await matchingPrivateFile(claim.path, claim.text);
|
|
109
199
|
const proposal = join(directory, `proposal-${output.cycleId}.txt`);
|
|
110
200
|
const analysis = join(directory, `analysis-${output.cycleId}.json`);
|
|
111
201
|
await matchingPrivateFile(proposal, output.proposal);
|
|
112
202
|
await matchingPrivateFile(analysis, output.analysis);
|
|
113
203
|
}
|
|
204
|
+
function draftRecord(input, expiresAt) {
|
|
205
|
+
return {
|
|
206
|
+
schemaVersion: 1,
|
|
207
|
+
...input,
|
|
208
|
+
candidateDigest: createHash("sha256").update(input.reply).digest("hex"),
|
|
209
|
+
expiresAt,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
export async function inspectAnalysisDraft(directory, input) {
|
|
213
|
+
await requireAbsent(directory, [
|
|
214
|
+
`analysis-${input.cycleId}.json`,
|
|
215
|
+
`proposal-${input.cycleId}.txt`,
|
|
216
|
+
]);
|
|
217
|
+
return inspectAnalysisDraftRecord(directory, input);
|
|
218
|
+
}
|
|
219
|
+
export async function inspectAnalysisDraftRecord(directory, input) {
|
|
220
|
+
await inspectOutputClaim(outputClaim(directory, input));
|
|
221
|
+
const text = await boundedAnalysisFile(join(directory, `draft-${input.cycleId}.json`), { permissions: 0o600 }).catch((error) => {
|
|
222
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
223
|
+
return undefined;
|
|
224
|
+
throw error;
|
|
225
|
+
});
|
|
226
|
+
if (text === undefined)
|
|
227
|
+
return undefined;
|
|
228
|
+
const expiresAt = record(JSON.parse(text))?.["expiresAt"];
|
|
229
|
+
if (typeof expiresAt !== "string" ||
|
|
230
|
+
!Number.isFinite(Date.parse(expiresAt)) ||
|
|
231
|
+
new Date(expiresAt).toISOString() !== expiresAt)
|
|
232
|
+
throw new Error("analysis-output-failed");
|
|
233
|
+
const expected = draftRecord(input, expiresAt);
|
|
234
|
+
if (text !== JSON.stringify(expected) + "\n")
|
|
235
|
+
throw new Error("analysis-output-failed");
|
|
236
|
+
return { candidateDigest: expected.candidateDigest, expiresAt };
|
|
237
|
+
}
|
|
238
|
+
export async function saveAnalysisDraft(directory, saving) {
|
|
239
|
+
const { input, now } = saving;
|
|
240
|
+
const existing = await inspectAnalysisDraft(directory, input);
|
|
241
|
+
if (existing !== undefined)
|
|
242
|
+
return existing;
|
|
243
|
+
const claim = outputClaim(directory, input);
|
|
244
|
+
await matchingPrivateFile(claim.path, claim.text);
|
|
245
|
+
const expiresAt = new Date(Date.parse(now()) + 86_400_000).toISOString();
|
|
246
|
+
const draft = draftRecord(input, expiresAt);
|
|
247
|
+
const text = JSON.stringify(draft) + "\n";
|
|
248
|
+
if (Buffer.byteLength(text) > analysisDocumentBytes)
|
|
249
|
+
throw new Error("analysis-output-failed");
|
|
250
|
+
await matchingPrivateFile(join(directory, `draft-${input.cycleId}.json`), text);
|
|
251
|
+
return { candidateDigest: draft.candidateDigest, expiresAt };
|
|
252
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { AdapterOutcome, DeliveryIntent } from "../broker/actor/delivery-intents.ts";
|
|
2
|
+
import { type Fields } from "../protocol.ts";
|
|
3
|
+
import { type AnalysisSnapshot } from "../chats/analysis-input.ts";
|
|
4
|
+
import type { AnalysisQuestion } from "../chats/analysis-contract.ts";
|
|
5
|
+
import { analysisBatchData } from "../chats/analysis-batches.ts";
|
|
6
|
+
import { type AnalysisDraftInput } from "./chats-files.ts";
|
|
7
|
+
export type ChatsResult = ReturnType<typeof analysisBatchData> | ({
|
|
8
|
+
readonly kind: "analysis_batch";
|
|
9
|
+
} & ReturnType<typeof analysisBatchData>) | {
|
|
10
|
+
readonly kind: "analysis_partial";
|
|
11
|
+
readonly reason: string;
|
|
12
|
+
} | {
|
|
13
|
+
readonly kind: "analysis_draft";
|
|
14
|
+
readonly cycleId: string;
|
|
15
|
+
readonly candidateDigest: string;
|
|
16
|
+
readonly expiresAt: string;
|
|
17
|
+
readonly question: AnalysisQuestion;
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: "handoff_requested";
|
|
20
|
+
readonly cycleId: string;
|
|
21
|
+
readonly promotionId: string;
|
|
22
|
+
};
|
|
23
|
+
interface FollowUpSave {
|
|
24
|
+
readonly intent: DeliveryIntent;
|
|
25
|
+
readonly payload: Fields;
|
|
26
|
+
readonly mode: "deliver" | "reconcile";
|
|
27
|
+
readonly input: {
|
|
28
|
+
readonly cycleId: string;
|
|
29
|
+
readonly snapshot: AnalysisSnapshot;
|
|
30
|
+
readonly candidate: unknown;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export declare function storedAnalysisDraft(directory: string, call: FollowUpSave): Promise<AnalysisDraftInput | undefined>;
|
|
34
|
+
export declare function saveFollowUp(runtime: string, call: FollowUpSave): Promise<AdapterOutcome>;
|
|
35
|
+
export declare function saveAcceptedAnalysis(runtime: string, call: FollowUpSave & {
|
|
36
|
+
readonly draft?: AnalysisDraftInput;
|
|
37
|
+
}): Promise<AdapterOutcome>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { record } from "../protocol.js";
|
|
4
|
+
import { analysisFollowUp } from "../chats/analysis-prompt.js";
|
|
5
|
+
import { analysisEvidence, } from "../chats/analysis-input.js";
|
|
6
|
+
import { validateAnalysis } from "../chats/analysis.js";
|
|
7
|
+
import { analysisProposal } from "../chats/analysis-proposal.js";
|
|
8
|
+
import { analysisBatchData } from "../chats/analysis-batches.js";
|
|
9
|
+
import { finalizeAnalysis } from "./chats-checkpoints.js";
|
|
10
|
+
import { operationKey } from "./operation-journal.js";
|
|
11
|
+
import { analysisDirectory, boundedAnalysisFile, inspectAnalysisDraftRecord, inspectAnalysisFiles, saveAnalysisFiles, } from "./chats-files.js";
|
|
12
|
+
function storedDraftIdentity(stored, call) {
|
|
13
|
+
return (stored?.["runId"] === call.intent.runId &&
|
|
14
|
+
stored["cycleId"] === call.input.cycleId &&
|
|
15
|
+
typeof stored["operationId"] === "string" &&
|
|
16
|
+
typeof stored["inputDigest"] === "string" &&
|
|
17
|
+
typeof stored["reply"] === "string" &&
|
|
18
|
+
stored["snapshotDigest"] ===
|
|
19
|
+
createHash("sha256")
|
|
20
|
+
.update(JSON.stringify(call.input.snapshot))
|
|
21
|
+
.digest("hex"));
|
|
22
|
+
}
|
|
23
|
+
export async function storedAnalysisDraft(directory, call) {
|
|
24
|
+
const { cycleId } = call.input;
|
|
25
|
+
const text = await boundedAnalysisFile(join(directory, `draft-${cycleId}.json`), { permissions: 0o600 }).catch((error) => {
|
|
26
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
27
|
+
return undefined;
|
|
28
|
+
throw error;
|
|
29
|
+
});
|
|
30
|
+
if (text === undefined)
|
|
31
|
+
return undefined;
|
|
32
|
+
const stored = record(JSON.parse(text));
|
|
33
|
+
if (!storedDraftIdentity(stored, call))
|
|
34
|
+
throw new Error("analysis-follow-up-refused");
|
|
35
|
+
const draft = {
|
|
36
|
+
runId: call.intent.runId,
|
|
37
|
+
cycleId,
|
|
38
|
+
operationId: stored["operationId"],
|
|
39
|
+
inputDigest: stored["inputDigest"],
|
|
40
|
+
snapshotDigest: stored["snapshotDigest"],
|
|
41
|
+
reply: stored["reply"],
|
|
42
|
+
...("batch" in stored ? { batch: stored["batch"] } : {}),
|
|
43
|
+
};
|
|
44
|
+
if ((await inspectAnalysisDraftRecord(directory, draft)) === undefined)
|
|
45
|
+
throw new Error("analysis-follow-up-refused");
|
|
46
|
+
return draft;
|
|
47
|
+
}
|
|
48
|
+
async function boundDraft(directory, call) {
|
|
49
|
+
const followUp = analysisFollowUp(call.payload["followUp"]);
|
|
50
|
+
const draft = await storedAnalysisDraft(directory, call);
|
|
51
|
+
if (draft === undefined ||
|
|
52
|
+
JSON.stringify(draft.batch) !== JSON.stringify(call.payload["batch"]) ||
|
|
53
|
+
JSON.stringify(JSON.parse(draft.reply)) !== JSON.stringify(followUp.draft))
|
|
54
|
+
throw new Error("analysis-follow-up-refused");
|
|
55
|
+
return draft;
|
|
56
|
+
}
|
|
57
|
+
function refused(reason, codes = [reason]) {
|
|
58
|
+
return {
|
|
59
|
+
kind: "failed",
|
|
60
|
+
reason,
|
|
61
|
+
result: JSON.stringify({ kind: "blocked", codes }),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
export async function saveFollowUp(runtime, call) {
|
|
65
|
+
const { snapshot, candidate } = call.input;
|
|
66
|
+
try {
|
|
67
|
+
if ("batch" in call.payload &&
|
|
68
|
+
Object.hasOwn(record(candidate) ?? {}, "openQuestion"))
|
|
69
|
+
return refused("analysis-question-limit");
|
|
70
|
+
if (!("batch" in call.payload))
|
|
71
|
+
return await saveSnapshotFollowUp(runtime, call);
|
|
72
|
+
const directory = await analysisDirectory(runtime, snapshot.importId);
|
|
73
|
+
const draft = await boundDraft(directory, call);
|
|
74
|
+
return await saveAcceptedAnalysis(runtime, { ...call, draft });
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return refused("analysis-follow-up-refused");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async function saveSnapshotFollowUp(runtime, call) {
|
|
81
|
+
const { snapshot, cycleId, candidate } = call.input;
|
|
82
|
+
const validation = validateAnalysis(candidate, analysisEvidence(snapshot));
|
|
83
|
+
if (!validation.accepted)
|
|
84
|
+
return refused("analysis-follow-up-refused", validation.codes);
|
|
85
|
+
if (validation.result.openQuestion !== undefined)
|
|
86
|
+
return refused("analysis-question-limit");
|
|
87
|
+
const directory = await analysisDirectory(runtime, snapshot.importId);
|
|
88
|
+
const draft = await boundDraft(directory, call);
|
|
89
|
+
const output = {
|
|
90
|
+
cycleId,
|
|
91
|
+
draft,
|
|
92
|
+
analysis: JSON.stringify(validation.result) + "\n",
|
|
93
|
+
proposal: analysisProposal(validation.result),
|
|
94
|
+
};
|
|
95
|
+
if (call.mode === "reconcile" &&
|
|
96
|
+
(await inspectAnalysisFiles(directory, output)) === "absent")
|
|
97
|
+
return { kind: "absent" };
|
|
98
|
+
if (call.mode === "deliver")
|
|
99
|
+
await saveAnalysisFiles(directory, output);
|
|
100
|
+
return {
|
|
101
|
+
kind: "completed",
|
|
102
|
+
worker: "chats",
|
|
103
|
+
receipt: operationKey(call.intent.operationId),
|
|
104
|
+
result: JSON.stringify({
|
|
105
|
+
kind: "handoff_requested",
|
|
106
|
+
cycleId,
|
|
107
|
+
promotionId: `analysis:${snapshot.importId}:${cycleId}`,
|
|
108
|
+
}),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async function acceptedDraft(directory, call) {
|
|
112
|
+
const draft = await storedAnalysisDraft(directory, call);
|
|
113
|
+
if (draft === undefined)
|
|
114
|
+
return undefined;
|
|
115
|
+
const batch = record(call.payload["batch"]);
|
|
116
|
+
const prior = record(draft.batch);
|
|
117
|
+
if (prior === undefined ||
|
|
118
|
+
batch === undefined ||
|
|
119
|
+
Number(batch["batchIndex"]) === Number(prior["batchIndex"]))
|
|
120
|
+
throw new Error("analysis-follow-up-refused");
|
|
121
|
+
if (Number(batch["batchIndex"]) < Number(prior["batchIndex"]))
|
|
122
|
+
return undefined;
|
|
123
|
+
return draft;
|
|
124
|
+
}
|
|
125
|
+
export async function saveAcceptedAnalysis(runtime, call) {
|
|
126
|
+
const { snapshot, cycleId, candidate } = call.input;
|
|
127
|
+
const { importId } = snapshot;
|
|
128
|
+
try {
|
|
129
|
+
const directory = await analysisDirectory(runtime, importId);
|
|
130
|
+
const draft = call.draft ??
|
|
131
|
+
("batch" in call.payload
|
|
132
|
+
? await acceptedDraft(directory, call)
|
|
133
|
+
: undefined);
|
|
134
|
+
const saved = await finalizeAnalysis(directory, {
|
|
135
|
+
runId: call.intent.runId,
|
|
136
|
+
importId,
|
|
137
|
+
cycleId,
|
|
138
|
+
snapshot: "snapshot" in call.payload ? snapshot : undefined,
|
|
139
|
+
candidate,
|
|
140
|
+
mode: call.mode,
|
|
141
|
+
batch: call.payload["batch"],
|
|
142
|
+
...(draft === undefined ? {} : { draft }),
|
|
143
|
+
});
|
|
144
|
+
return savedOutcome(call, saved);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
throw new Error("analysis-output-failed");
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function savedOutcome(call, saved) {
|
|
151
|
+
const { cycleId, snapshot } = call.input;
|
|
152
|
+
const { importId } = snapshot;
|
|
153
|
+
if (saved.kind === "absent")
|
|
154
|
+
return { kind: "absent" };
|
|
155
|
+
if (saved.kind === "refused")
|
|
156
|
+
return refused(call.draft !== undefined &&
|
|
157
|
+
!saved.codes.includes("analysis-merge-conflict")
|
|
158
|
+
? "analysis-follow-up-refused"
|
|
159
|
+
: (saved.codes[0] ?? "analysis-input-refused"), saved.codes);
|
|
160
|
+
let result = {
|
|
161
|
+
kind: "handoff_requested",
|
|
162
|
+
cycleId,
|
|
163
|
+
promotionId: `analysis:${importId}:${cycleId}`,
|
|
164
|
+
};
|
|
165
|
+
if (saved.next?.kind === "refused")
|
|
166
|
+
result = { kind: "analysis_partial", reason: saved.next.code };
|
|
167
|
+
if (saved.next?.kind === "batch")
|
|
168
|
+
result = {
|
|
169
|
+
kind: "analysis_batch",
|
|
170
|
+
...analysisBatchData(saved.next, {
|
|
171
|
+
digest: saved.checkpoint.capturedDigest,
|
|
172
|
+
}),
|
|
173
|
+
};
|
|
174
|
+
return {
|
|
175
|
+
kind: "completed",
|
|
176
|
+
worker: "chats",
|
|
177
|
+
receipt: operationKey(call.intent.operationId),
|
|
178
|
+
result: JSON.stringify(result),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { AdapterOutcome, DeliveryIntent } from "../broker/actor/delivery-intents.ts";
|
|
2
|
+
import type { Fields } from "../protocol.ts";
|
|
3
|
+
import { type analysisSaveInput } from "../chats/analysis-input.ts";
|
|
4
|
+
interface QuestionSettings {
|
|
5
|
+
readonly runtimeDirectory: string;
|
|
6
|
+
readonly now?: () => string;
|
|
7
|
+
}
|
|
8
|
+
interface QuestionSave {
|
|
9
|
+
readonly intent: DeliveryIntent;
|
|
10
|
+
readonly mode: "deliver" | "reconcile";
|
|
11
|
+
readonly input: ReturnType<typeof analysisSaveInput>;
|
|
12
|
+
readonly payload: Fields;
|
|
13
|
+
}
|
|
14
|
+
export declare function saveQuestion(settings: QuestionSettings, call: QuestionSave): Promise<AdapterOutcome>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { validateAnalysis } from "../chats/analysis.js";
|
|
3
|
+
import { analysisEvidence, } from "../chats/analysis-input.js";
|
|
4
|
+
import { mergeAnalysisBatch } from "../chats/analysis-merge.js";
|
|
5
|
+
import { boundAnalysisBatch } from "./chats-batch-binding.js";
|
|
6
|
+
import { analysisDirectory, inspectAnalysisDraft, saveAnalysisDraft, } from "./chats-files.js";
|
|
7
|
+
import { operationKey } from "./operation-journal.js";
|
|
8
|
+
import { storedAnalysisDraft } from "./chats-follow-up.js";
|
|
9
|
+
async function validateQuestion(directory, call) {
|
|
10
|
+
const { input, payload, intent } = call;
|
|
11
|
+
if (!("batch" in payload))
|
|
12
|
+
return validateAnalysis(input.candidate, analysisEvidence(input.snapshot));
|
|
13
|
+
const bound = await boundAnalysisBatch(directory, {
|
|
14
|
+
runId: intent.runId,
|
|
15
|
+
importId: input.snapshot.importId,
|
|
16
|
+
snapshot: "snapshot" in payload ? input.snapshot : undefined,
|
|
17
|
+
batch: payload["batch"],
|
|
18
|
+
});
|
|
19
|
+
return mergeAnalysisBatch({
|
|
20
|
+
previousResult: bound.previous.result,
|
|
21
|
+
candidate: input.candidate,
|
|
22
|
+
evidence: analysisEvidence(bound.snapshot),
|
|
23
|
+
nextRecordIndex: bound.batch.nextRecordIndex,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function questionDraft(call) {
|
|
27
|
+
const { cycleId, snapshot, reply } = call.input;
|
|
28
|
+
return {
|
|
29
|
+
runId: call.intent.runId,
|
|
30
|
+
cycleId,
|
|
31
|
+
operationId: call.intent.operationId,
|
|
32
|
+
inputDigest: call.intent.inputDigest,
|
|
33
|
+
snapshotDigest: createHash("sha256")
|
|
34
|
+
.update(JSON.stringify(snapshot))
|
|
35
|
+
.digest("hex"),
|
|
36
|
+
reply,
|
|
37
|
+
...("batch" in call.payload ? { batch: call.payload["batch"] } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
async function additionalBatchQuestion(directory, call) {
|
|
41
|
+
const earlier = await storedAnalysisDraft(directory, call);
|
|
42
|
+
return (earlier !== undefined &&
|
|
43
|
+
"batch" in call.payload &&
|
|
44
|
+
earlier.operationId !== call.intent.operationId);
|
|
45
|
+
}
|
|
46
|
+
export async function saveQuestion(settings, call) {
|
|
47
|
+
try {
|
|
48
|
+
const { cycleId, snapshot } = call.input;
|
|
49
|
+
const directory = await analysisDirectory(settings.runtimeDirectory, snapshot.importId);
|
|
50
|
+
if (await additionalBatchQuestion(directory, call))
|
|
51
|
+
return {
|
|
52
|
+
kind: "failed",
|
|
53
|
+
reason: "analysis-question-limit",
|
|
54
|
+
result: JSON.stringify({
|
|
55
|
+
kind: "blocked",
|
|
56
|
+
codes: ["analysis-question-limit"],
|
|
57
|
+
}),
|
|
58
|
+
};
|
|
59
|
+
const validation = await validateQuestion(directory, call);
|
|
60
|
+
if (!validation.accepted)
|
|
61
|
+
return {
|
|
62
|
+
kind: "failed",
|
|
63
|
+
reason: validation.codes[0] ?? "analysis-input-refused",
|
|
64
|
+
result: JSON.stringify({ kind: "blocked", codes: validation.codes }),
|
|
65
|
+
};
|
|
66
|
+
const question = validation.result.openQuestion;
|
|
67
|
+
if (question === undefined)
|
|
68
|
+
throw new Error("analysis-input-refused");
|
|
69
|
+
const draft = questionDraft(call);
|
|
70
|
+
const metadata = call.mode === "reconcile"
|
|
71
|
+
? await inspectAnalysisDraft(directory, draft)
|
|
72
|
+
: await saveAnalysisDraft(directory, {
|
|
73
|
+
input: draft,
|
|
74
|
+
now: settings.now ?? (() => new Date().toISOString()),
|
|
75
|
+
});
|
|
76
|
+
if (metadata === undefined)
|
|
77
|
+
return { kind: "absent" };
|
|
78
|
+
return {
|
|
79
|
+
kind: "completed",
|
|
80
|
+
worker: "chats",
|
|
81
|
+
receipt: operationKey(call.intent.operationId),
|
|
82
|
+
result: JSON.stringify({
|
|
83
|
+
kind: "analysis_draft",
|
|
84
|
+
cycleId,
|
|
85
|
+
...metadata,
|
|
86
|
+
question,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
throw new Error("analysis-output-failed");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -4,6 +4,7 @@ export interface ChatsWorkerSettings {
|
|
|
4
4
|
readonly runtimeDirectory: string;
|
|
5
5
|
readonly document: (digest: string) => Promise<string | undefined>;
|
|
6
6
|
readonly workflowId: (runId: string) => Promise<string | undefined>;
|
|
7
|
+
readonly now?: () => string;
|
|
7
8
|
}
|
|
8
9
|
export interface ChatsWorker extends EffectDelivery {
|
|
9
10
|
readonly deliver: (intent: DeliveryIntent, signal: AbortSignal) => Promise<AdapterOutcome>;
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { saveQuestion } from "./chats-question.js";
|
|
2
|
+
import { saveAcceptedAnalysis, saveFollowUp, } from "./chats-follow-up.js";
|
|
1
3
|
import { record } from "../protocol.js";
|
|
2
4
|
import { validateAnalysis } from "../chats/analysis.js";
|
|
3
|
-
import { analysisDocumentBytes, analysisEvidence,
|
|
4
|
-
import {
|
|
5
|
+
import { analysisDocumentBytes, analysisEvidence, analysisSaveInput, validAnalysisImportId, } from "../chats/analysis-input.js";
|
|
6
|
+
import { analysisBatchData, prepareAnalysisBatch, } from "../chats/analysis-batches.js";
|
|
7
|
+
import { captureAnalysisSnapshot, readCapturedAnalysisSnapshot, findAnalysisCapture, } from "./chats-capture.js";
|
|
5
8
|
import { operationKey } from "./operation-journal.js";
|
|
6
|
-
import { analysisDirectory
|
|
9
|
+
import { analysisDirectory } from "./chats-files.js";
|
|
7
10
|
function completed(intent, result) {
|
|
8
11
|
return {
|
|
9
12
|
kind: "completed",
|
|
@@ -52,77 +55,75 @@ async function payloadOf(settings, intent) {
|
|
|
52
55
|
return payload;
|
|
53
56
|
}
|
|
54
57
|
async function read(settings, call) {
|
|
58
|
+
const { importId } = call;
|
|
55
59
|
let directory;
|
|
56
60
|
try {
|
|
57
|
-
directory = await analysisDirectory(settings.runtimeDirectory,
|
|
61
|
+
directory = await analysisDirectory(settings.runtimeDirectory, importId);
|
|
58
62
|
}
|
|
59
63
|
catch (error) {
|
|
60
64
|
if (Reflect.get(Object(error), "code") === "ENOENT")
|
|
61
65
|
throw new Error("redaction-required", { cause: error });
|
|
62
66
|
throw error;
|
|
63
67
|
}
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const
|
|
68
|
+
const capture = await captureAnalysisSnapshot(directory, {
|
|
69
|
+
importId,
|
|
70
|
+
runId: call.intent.runId,
|
|
71
|
+
});
|
|
72
|
+
const snapshot = await readCapturedAnalysisSnapshot(directory, capture);
|
|
73
|
+
const batch = prepareAnalysisBatch(snapshot, {
|
|
74
|
+
nextRecordIndex: 0,
|
|
75
|
+
completedBatchCount: 0,
|
|
76
|
+
previousResult: null,
|
|
77
|
+
});
|
|
78
|
+
if (batch.kind === "refused")
|
|
79
|
+
throw new Error(batch.code);
|
|
80
|
+
const result = analysisBatchData(batch, { digest: capture.digest, snapshot });
|
|
69
81
|
bounded(JSON.stringify(result));
|
|
70
82
|
return completed(call.intent, result);
|
|
71
83
|
}
|
|
72
|
-
function saveInput(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
catch {
|
|
86
|
-
throw new Error("analysis-invalid-json");
|
|
87
|
-
}
|
|
88
|
-
return { snapshot, cycleId, candidate };
|
|
84
|
+
async function saveInput(settings, call) {
|
|
85
|
+
if ("snapshot" in call.payload)
|
|
86
|
+
return analysisSaveInput(call.payload);
|
|
87
|
+
const importId = String(call.payload["importId"]);
|
|
88
|
+
const directory = await analysisDirectory(settings.runtimeDirectory, importId);
|
|
89
|
+
const capture = await findAnalysisCapture(directory, {
|
|
90
|
+
runId: call.intent.runId,
|
|
91
|
+
importId,
|
|
92
|
+
});
|
|
93
|
+
if (capture === undefined)
|
|
94
|
+
throw new Error("analysis-output-failed");
|
|
95
|
+
const snapshot = await readCapturedAnalysisSnapshot(directory, capture);
|
|
96
|
+
return analysisSaveInput({ ...call.payload, snapshot });
|
|
89
97
|
}
|
|
90
98
|
async function save(settings, call) {
|
|
91
|
-
const
|
|
92
|
-
const
|
|
99
|
+
const input = await saveInput(settings, call);
|
|
100
|
+
const { snapshot, candidate } = input;
|
|
101
|
+
if ("followUp" in call.payload)
|
|
102
|
+
return saveFollowUp(settings.runtimeDirectory, { ...call, input });
|
|
103
|
+
if ("batch" in call.payload &&
|
|
104
|
+
Object.hasOwn(record(candidate) ?? {}, "openQuestion"))
|
|
105
|
+
return saveQuestion(settings, { ...call, input });
|
|
106
|
+
const validation = "batch" in call.payload
|
|
107
|
+
? undefined
|
|
108
|
+
: validateAnalysis(candidate, analysisEvidence(snapshot));
|
|
109
|
+
if (validation === undefined)
|
|
110
|
+
return saveAcceptedAnalysis(settings.runtimeDirectory, { ...call, input });
|
|
93
111
|
if (!validation.accepted)
|
|
94
112
|
return refused(validation.codes[0] ?? "analysis-input-refused", validation.codes);
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const output = {
|
|
100
|
-
cycleId,
|
|
101
|
-
analysis: JSON.stringify(validation.result) + "\n",
|
|
102
|
-
proposal: analysisProposal(validation.result),
|
|
103
|
-
};
|
|
104
|
-
if (call.mode === "reconcile" &&
|
|
105
|
-
(await inspectAnalysisFiles(directory, output)) === "absent")
|
|
106
|
-
return { kind: "absent" };
|
|
107
|
-
if (call.mode === "deliver")
|
|
108
|
-
await saveAnalysisFiles(directory, output);
|
|
109
|
-
}
|
|
110
|
-
catch {
|
|
111
|
-
throw new Error("analysis-output-failed");
|
|
112
|
-
}
|
|
113
|
-
return completed(call.intent, {
|
|
114
|
-
kind: "handoff_requested",
|
|
115
|
-
cycleId,
|
|
116
|
-
promotionId: `analysis:${snapshot.importId}:${cycleId}`,
|
|
117
|
-
});
|
|
113
|
+
const question = validation.result.openQuestion;
|
|
114
|
+
if (question !== undefined)
|
|
115
|
+
return saveQuestion(settings, { ...call, input });
|
|
116
|
+
return saveAcceptedAnalysis(settings.runtimeDirectory, { ...call, input });
|
|
118
117
|
}
|
|
119
118
|
const reasons = new Set([
|
|
120
119
|
"analysis-too-large",
|
|
121
120
|
"redaction-required",
|
|
122
121
|
"invalid-import-id",
|
|
123
122
|
"analysis-invalid-json",
|
|
123
|
+
"analysis-follow-up-refused",
|
|
124
124
|
"analysis-input-refused",
|
|
125
125
|
"analysis-output-failed",
|
|
126
|
+
"analysis-batch-limit",
|
|
126
127
|
"unsupported-effect",
|
|
127
128
|
"workflow-not-allowed",
|
|
128
129
|
"payload-not-available",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { type WorkerUsage } from "../worker-usage.ts";
|
|
1
|
+
import { type WorkerUsage, type WorkerAttemptUsage } from "../worker-usage.ts";
|
|
2
2
|
export type Worker = "claude" | "codex";
|
|
3
3
|
export interface StreamResult {
|
|
4
|
+
readonly attempts?: readonly WorkerAttemptUsage[];
|
|
4
5
|
readonly subtype?: string;
|
|
5
6
|
readonly errorType?: string;
|
|
6
7
|
readonly model: string | null;
|