cloving 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.md +145 -14
  2. package/dist/src/authority/commands.js +10 -2
  3. package/dist/src/authority/reconciliation.d.ts +2 -0
  4. package/dist/src/authority/reconciliation.js +10 -1
  5. package/dist/src/authority/transitions.d.ts +1 -1
  6. package/dist/src/authority/transitions.js +2 -0
  7. package/dist/src/broker/actor/authority-session.d.ts +2 -0
  8. package/dist/src/broker/actor/authority-session.js +3 -4
  9. package/dist/src/chats/analysis-batches.d.ts +24 -0
  10. package/dist/src/chats/analysis-batches.js +83 -0
  11. package/dist/src/chats/analysis-contract.d.ts +80 -0
  12. package/dist/src/chats/analysis-contract.js +16 -0
  13. package/dist/src/chats/analysis-data.d.ts +3 -0
  14. package/dist/src/chats/analysis-data.js +85 -0
  15. package/dist/src/chats/analysis-input.d.ts +12 -0
  16. package/dist/src/chats/analysis-input.js +83 -0
  17. package/dist/src/chats/analysis-links.d.ts +2 -0
  18. package/dist/src/chats/analysis-links.js +25 -0
  19. package/dist/src/chats/analysis-prompt.d.ts +10 -0
  20. package/dist/src/chats/analysis-prompt.js +16 -0
  21. package/dist/src/chats/analysis-proposal.d.ts +2 -0
  22. package/dist/src/chats/analysis-proposal.js +32 -0
  23. package/dist/src/chats/analysis-provenance.d.ts +4 -0
  24. package/dist/src/chats/analysis-provenance.js +99 -0
  25. package/dist/src/chats/analysis-shape.d.ts +3 -0
  26. package/dist/src/chats/analysis-shape.js +228 -0
  27. package/dist/src/chats/analysis.d.ts +3 -0
  28. package/dist/src/chats/analysis.js +42 -0
  29. package/dist/src/chats/corpus-input.d.ts +5 -0
  30. package/dist/src/chats/corpus-input.js +288 -0
  31. package/dist/src/chats/destination.js +3 -1
  32. package/dist/src/chats/import.d.ts +8 -6
  33. package/dist/src/chats/import.js +195 -23
  34. package/dist/src/chats/input-file.d.ts +3 -0
  35. package/dist/src/chats/input-file.js +32 -0
  36. package/dist/src/chats/normalize.d.ts +11 -2
  37. package/dist/src/chats/normalize.js +18 -2
  38. package/dist/src/chats/previous-import.d.ts +5 -0
  39. package/dist/src/chats/previous-import.js +34 -0
  40. package/dist/src/chats/reader.d.ts +7 -1
  41. package/dist/src/chats/reader.js +46 -15
  42. package/dist/src/chats/redact-import.d.ts +14 -0
  43. package/dist/src/chats/redact-import.js +141 -0
  44. package/dist/src/chats/redaction.d.ts +41 -0
  45. package/dist/src/chats/redaction.js +161 -0
  46. package/dist/src/chats/revision-input.d.ts +3 -0
  47. package/dist/src/chats/revision-input.js +180 -0
  48. package/dist/src/chats/revision.d.ts +48 -0
  49. package/dist/src/chats/revision.js +94 -0
  50. package/dist/src/chats/selection.d.ts +25 -0
  51. package/dist/src/chats/selection.js +109 -0
  52. package/dist/src/chats/source.d.ts +23 -0
  53. package/dist/src/chats/source.js +135 -0
  54. package/dist/src/chats/structure.d.ts +57 -0
  55. package/dist/src/chats/structure.js +216 -0
  56. package/dist/src/cli/arguments.d.ts +8 -0
  57. package/dist/src/cli/arguments.js +94 -13
  58. package/dist/src/cli/client.d.ts +1 -0
  59. package/dist/src/cli/client.js +38 -1
  60. package/dist/src/cli/commands.d.ts +3 -0
  61. package/dist/src/cli/commands.js +29 -11
  62. package/dist/src/cli/main.d.ts +2 -1
  63. package/dist/src/cli/main.js +61 -14
  64. package/dist/src/cli/render.d.ts +2 -0
  65. package/dist/src/cli/render.js +70 -2
  66. package/dist/src/cli/usage-render.d.ts +3 -0
  67. package/dist/src/cli/usage-render.js +28 -0
  68. package/dist/src/cli/usage.d.ts +3 -2
  69. package/dist/src/cli/usage.js +38 -6
  70. package/dist/src/cli/watch-actions.d.ts +10 -0
  71. package/dist/src/cli/watch-actions.js +58 -0
  72. package/dist/src/cli/watch-board-view.d.ts +32 -0
  73. package/dist/src/cli/watch-board-view.js +88 -15
  74. package/dist/src/cli/watch-board.js +6 -1
  75. package/dist/src/cli/watch-card-client.d.ts +1 -0
  76. package/dist/src/cli/watch-card-client.js +44 -0
  77. package/dist/src/cli/watch-card-forms.d.ts +2 -0
  78. package/dist/src/cli/watch-card-forms.js +33 -0
  79. package/dist/src/cli/watch-card-reply.d.ts +5 -0
  80. package/dist/src/cli/watch-card-reply.js +34 -0
  81. package/dist/src/cli/watch-card-view.d.ts +40 -0
  82. package/dist/src/cli/watch-card-view.js +86 -0
  83. package/dist/src/cli/watch-client.d.ts +1 -0
  84. package/dist/src/cli/watch-client.js +118 -0
  85. package/dist/src/cli/watch-model.js +31 -14
  86. package/dist/src/cli/watch-page.d.ts +1 -1
  87. package/dist/src/cli/watch-page.js +71 -85
  88. package/dist/src/cli/watch-pr-board.d.ts +3 -0
  89. package/dist/src/cli/watch-pr-board.js +26 -0
  90. package/dist/src/cli/watch-pr-command.d.ts +15 -0
  91. package/dist/src/cli/watch-pr-command.js +37 -0
  92. package/dist/src/cli/watch-pr-identity.d.ts +20 -0
  93. package/dist/src/cli/watch-pr-identity.js +62 -0
  94. package/dist/src/cli/watch-pr-observer.d.ts +19 -0
  95. package/dist/src/cli/watch-pr-observer.js +108 -0
  96. package/dist/src/cli/watch-pr-settings.d.ts +8 -0
  97. package/dist/src/cli/watch-pr-settings.js +47 -0
  98. package/dist/src/cli/watch-reads.js +4 -0
  99. package/dist/src/cli/watch-render.d.ts +16 -2
  100. package/dist/src/cli/watch-render.js +10 -3
  101. package/dist/src/cli/watch-run-input.d.ts +14 -0
  102. package/dist/src/cli/watch-run-input.js +49 -0
  103. package/dist/src/cli/watch-server.d.ts +3 -0
  104. package/dist/src/cli/watch-server.js +44 -6
  105. package/dist/src/cli/watch-start-view.d.ts +18 -0
  106. package/dist/src/cli/watch-start-view.js +77 -0
  107. package/dist/src/cli/watch-turn.d.ts +1 -0
  108. package/dist/src/cli/watch-turn.js +7 -2
  109. package/dist/src/cli/watch-usage.d.ts +6 -0
  110. package/dist/src/cli/watch-usage.js +65 -0
  111. package/dist/src/cli/watch-view.d.ts +15 -0
  112. package/dist/src/cli/watch-view.js +61 -17
  113. package/dist/src/cli.js +48 -6
  114. package/dist/src/daemon/analysis-launch.d.ts +5 -0
  115. package/dist/src/daemon/analysis-launch.js +81 -0
  116. package/dist/src/daemon/analysis-token.d.ts +6 -0
  117. package/dist/src/daemon/analysis-token.js +88 -0
  118. package/dist/src/daemon/analysis-worker.d.ts +22 -0
  119. package/dist/src/daemon/analysis-worker.js +219 -0
  120. package/dist/src/daemon/chats-files.d.ts +15 -0
  121. package/dist/src/daemon/chats-files.js +113 -0
  122. package/dist/src/daemon/chats-worker.d.ts +12 -0
  123. package/dist/src/daemon/chats-worker.js +200 -0
  124. package/dist/src/daemon/claude-turn.d.ts +10 -0
  125. package/dist/src/daemon/claude-turn.js +96 -15
  126. package/dist/src/daemon/claude-worker.d.ts +4 -1
  127. package/dist/src/daemon/claude-worker.js +99 -47
  128. package/dist/src/daemon/codex-stream.js +16 -0
  129. package/dist/src/daemon/daemon.d.ts +3 -0
  130. package/dist/src/daemon/daemon.js +16 -7
  131. package/dist/src/daemon/github-methods.js +4 -0
  132. package/dist/src/daemon/github-ticket-status.d.ts +3 -0
  133. package/dist/src/daemon/github-ticket-status.js +268 -0
  134. package/dist/src/daemon/issue-queue.d.ts +1 -0
  135. package/dist/src/daemon/issue-queue.js +115 -24
  136. package/dist/src/daemon/operation-journal.d.ts +1 -0
  137. package/dist/src/daemon/operation-journal.js +4 -2
  138. package/dist/src/daemon/retrying-turn.d.ts +9 -0
  139. package/dist/src/daemon/retrying-turn.js +185 -0
  140. package/dist/src/host/world-reconciliation.d.ts +2 -0
  141. package/dist/src/host/world-reconciliation.js +6 -0
  142. package/dist/src/host/world-runs.js +9 -1
  143. package/dist/src/service/run-handlers.js +15 -23
  144. package/dist/src/service/worker-totals.d.ts +3 -0
  145. package/dist/src/service/worker-totals.js +28 -0
  146. package/dist/src/ticket-status.d.ts +8 -0
  147. package/dist/src/ticket-status.js +45 -0
  148. package/dist/src/token-prices.d.ts +19 -0
  149. package/dist/src/token-prices.js +76 -0
  150. package/dist/src/worker-usage.d.ts +22 -0
  151. package/dist/src/worker-usage.js +44 -0
  152. package/dist/src/workflow-candidate.d.ts +29 -0
  153. package/dist/src/workflow-candidate.js +59 -0
  154. package/dist/src/workflow-contracts.d.ts +2 -0
  155. package/dist/src/workflow-contracts.js +27 -5
  156. package/dist/src/workflow-declaration.d.ts +1 -1
  157. package/dist/src/workflow-declaration.js +3 -1
  158. package/dist/src/workflows.d.ts +1 -0
  159. package/dist/src/workflows.js +1 -0
  160. package/dist/tools/self-host-processes.js +7 -2
  161. package/dist/tools/self-host-workflows.d.ts +1 -1
  162. package/dist/tools/self-host-workflows.js +2 -2
  163. package/dist/tools/self-host.js +6 -6
  164. package/dist/tools/self-update-host.d.ts +7 -0
  165. package/dist/tools/self-update-host.js +48 -1
  166. package/dist/tools/self-update.d.ts +1 -0
  167. package/dist/tools/self-update.js +85 -13
  168. package/dist/workflows/analyze-chats.d.ts +10 -0
  169. package/dist/workflows/analyze-chats.declaration.json +9 -0
  170. package/dist/workflows/analyze-chats.js +73 -0
  171. package/dist/workflows/develop-ticket.d.ts +1 -7
  172. package/dist/workflows/develop-ticket.js +42 -141
  173. package/dist/workflows/issue-labels.d.ts +3 -1
  174. package/dist/workflows/issue-labels.js +34 -0
  175. package/dist/workflows/ticket-prompts.d.ts +35 -0
  176. package/dist/workflows/ticket-prompts.js +133 -0
  177. package/package.json +5 -3
@@ -0,0 +1,113 @@
1
+ import { lstat } from "node:fs/promises";
2
+ import { dirname, join, resolve } from "node:path";
3
+ import { openInput } from "../chats/input-file.js";
4
+ import { createPrivateFile, validateDestination, } from "../chats/destination.js";
5
+ import { analysisDocumentBytes, } from "../chats/analysis-input.js";
6
+ export async function analysisDirectory(runtime, importId) {
7
+ if (process.platform !== "darwin")
8
+ throw new Error("platform-unsupported");
9
+ const directory = resolve(runtime, "chats", importId);
10
+ await checkedDirectory(directory);
11
+ return directory;
12
+ }
13
+ async function checkedDirectory(directory) {
14
+ try {
15
+ if (!(await lstat(directory)).isDirectory())
16
+ throw new Error("analysis-input-refused");
17
+ }
18
+ catch (error) {
19
+ if (Reflect.get(Object(error), "code") !== "ENOENT")
20
+ throw error;
21
+ const parent = dirname(directory);
22
+ if (parent === directory)
23
+ throw error;
24
+ await checkedDirectory(parent);
25
+ throw error;
26
+ }
27
+ await validateDestination(directory);
28
+ }
29
+ export async function boundedAnalysisFile(path, options = {}) {
30
+ const handle = await openInput(path);
31
+ try {
32
+ if (options.permissions !== undefined &&
33
+ ((await handle.stat()).mode & 0o777) !== options.permissions)
34
+ throw new Error("analysis-output-failed");
35
+ const buffer = Buffer.alloc(analysisDocumentBytes + 1);
36
+ let size = 0;
37
+ while (size < buffer.length) {
38
+ const { bytesRead } = await handle.read(buffer, size, buffer.length - size, null);
39
+ if (bytesRead === 0)
40
+ break;
41
+ size += bytesRead;
42
+ }
43
+ if (size > analysisDocumentBytes)
44
+ throw new Error("analysis-too-large");
45
+ return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, size));
46
+ }
47
+ finally {
48
+ await handle.close();
49
+ }
50
+ }
51
+ export async function readAnalysisSnapshot(directory, importId) {
52
+ const required = async (name) => {
53
+ try {
54
+ return await boundedAnalysisFile(join(directory, name));
55
+ }
56
+ catch (error) {
57
+ if (Reflect.get(Object(error), "code") === "ENOENT")
58
+ throw new Error("redaction-required", { cause: error });
59
+ throw error;
60
+ }
61
+ };
62
+ return {
63
+ importId,
64
+ redaction: await required("redaction.json"),
65
+ corpus: await required("redacted.jsonl"),
66
+ structure: await boundedAnalysisFile(join(directory, "structure.json")),
67
+ };
68
+ }
69
+ export async function matchingPrivateFile(path, text) {
70
+ try {
71
+ const existing = await boundedAnalysisFile(path, { permissions: 0o600 });
72
+ if (existing !== text)
73
+ throw new Error("analysis-output-failed");
74
+ return;
75
+ }
76
+ catch (error) {
77
+ if (Reflect.get(Object(error), "code") !== "ENOENT")
78
+ throw error;
79
+ }
80
+ const handle = await createPrivateFile(path);
81
+ try {
82
+ await handle.writeFile(text);
83
+ }
84
+ finally {
85
+ await handle.close();
86
+ }
87
+ }
88
+ export async function inspectAnalysisFiles(directory, output) {
89
+ const proposal = join(directory, `proposal-${output.cycleId}.txt`);
90
+ const analysis = join(directory, `analysis-${output.cycleId}.json`);
91
+ const marker = await boundedAnalysisFile(analysis, {
92
+ permissions: 0o600,
93
+ }).catch((error) => {
94
+ if (Reflect.get(Object(error), "code") === "ENOENT")
95
+ return undefined;
96
+ throw error;
97
+ });
98
+ if (marker === undefined)
99
+ return "absent";
100
+ if (marker !== output.analysis ||
101
+ (await boundedAnalysisFile(proposal, { permissions: 0o600 })) !==
102
+ output.proposal)
103
+ throw new Error("analysis-output-failed");
104
+ return "complete";
105
+ }
106
+ export async function saveAnalysisFiles(directory, output) {
107
+ if ((await inspectAnalysisFiles(directory, output)) === "complete")
108
+ return;
109
+ const proposal = join(directory, `proposal-${output.cycleId}.txt`);
110
+ const analysis = join(directory, `analysis-${output.cycleId}.json`);
111
+ await matchingPrivateFile(proposal, output.proposal);
112
+ await matchingPrivateFile(analysis, output.analysis);
113
+ }
@@ -0,0 +1,12 @@
1
+ import type { AdapterOutcome, DeliveryIntent } from "../broker/actor/delivery-intents.ts";
2
+ import type { EffectDelivery } from "./delivery-router.ts";
3
+ export interface ChatsWorkerSettings {
4
+ readonly runtimeDirectory: string;
5
+ readonly document: (digest: string) => Promise<string | undefined>;
6
+ readonly workflowId: (runId: string) => Promise<string | undefined>;
7
+ }
8
+ export interface ChatsWorker extends EffectDelivery {
9
+ readonly deliver: (intent: DeliveryIntent, signal: AbortSignal) => Promise<AdapterOutcome>;
10
+ readonly close: () => Promise<void>;
11
+ }
12
+ export declare function chatsWorker(settings: ChatsWorkerSettings): ChatsWorker;
@@ -0,0 +1,200 @@
1
+ import { record } from "../protocol.js";
2
+ import { validateAnalysis } from "../chats/analysis.js";
3
+ import { analysisDocumentBytes, analysisEvidence, analysisModelInput, isAnalysisSnapshot, validAnalysisImportId, } from "../chats/analysis-input.js";
4
+ import { analysisProposal } from "../chats/analysis-proposal.js";
5
+ import { operationKey } from "./operation-journal.js";
6
+ import { analysisDirectory, readAnalysisSnapshot, saveAnalysisFiles, inspectAnalysisFiles, } from "./chats-files.js";
7
+ function completed(intent, result) {
8
+ return {
9
+ kind: "completed",
10
+ worker: "chats",
11
+ receipt: operationKey(intent.operationId),
12
+ result: JSON.stringify(result),
13
+ };
14
+ }
15
+ function refused(code, codes = [code]) {
16
+ return {
17
+ kind: "failed",
18
+ reason: code,
19
+ result: JSON.stringify({ kind: "blocked", codes }),
20
+ };
21
+ }
22
+ function bounded(text) {
23
+ if (Buffer.byteLength(text) > analysisDocumentBytes)
24
+ throw new Error("analysis-too-large");
25
+ }
26
+ function ownIntent(intent) {
27
+ return (intent.kind === "protected-effect" &&
28
+ intent.capability === "chats" &&
29
+ ["read-analysis", "save-analysis"].includes(intent.method ?? "") &&
30
+ intent.payload !== undefined &&
31
+ intent.leaseGeneration !== undefined);
32
+ }
33
+ async function payloadOf(settings, intent) {
34
+ if (!ownIntent(intent) || intent.payload === undefined)
35
+ throw new Error("unsupported-effect");
36
+ if ((await settings.workflowId(intent.runId)) !== "analyze-chats")
37
+ throw new Error("workflow-not-allowed");
38
+ const text = await settings.document(intent.payload);
39
+ if (text === undefined)
40
+ throw new Error("payload-not-available");
41
+ bounded(text);
42
+ const payload = record(JSON.parse(text));
43
+ if (payload === undefined)
44
+ throw new Error("analysis-input-refused");
45
+ const importId = payload["importId"];
46
+ if (typeof importId !== "string" || importId === "")
47
+ throw new Error("analysis-input-refused");
48
+ if (!validAnalysisImportId(importId))
49
+ throw new Error("invalid-import-id");
50
+ if (intent.target !== importId)
51
+ throw new Error("analysis-input-refused");
52
+ return payload;
53
+ }
54
+ async function read(settings, call) {
55
+ let directory;
56
+ try {
57
+ directory = await analysisDirectory(settings.runtimeDirectory, call.importId);
58
+ }
59
+ catch (error) {
60
+ if (Reflect.get(Object(error), "code") === "ENOENT")
61
+ throw new Error("redaction-required", { cause: error });
62
+ throw error;
63
+ }
64
+ const snapshot = await readAnalysisSnapshot(directory, call.importId);
65
+ bounded(JSON.stringify(snapshot));
66
+ bounded(JSON.stringify({ prompt: analysisModelInput(snapshot) }));
67
+ analysisEvidence(snapshot);
68
+ const result = { snapshot };
69
+ bounded(JSON.stringify(result));
70
+ return completed(call.intent, result);
71
+ }
72
+ function saveInput(payload) {
73
+ const snapshot = payload["snapshot"];
74
+ const cycleId = payload["cycleId"];
75
+ if (!isAnalysisSnapshot(snapshot) ||
76
+ snapshot.importId !== payload["importId"] ||
77
+ typeof cycleId !== "string" ||
78
+ !/^[A-Za-z0-9_-]{1,128}$/.test(cycleId) ||
79
+ typeof payload["reply"] !== "string")
80
+ throw new Error("analysis-input-refused");
81
+ let candidate;
82
+ try {
83
+ candidate = JSON.parse(payload["reply"]);
84
+ }
85
+ catch {
86
+ throw new Error("analysis-invalid-json");
87
+ }
88
+ return { snapshot, cycleId, candidate };
89
+ }
90
+ async function save(settings, call) {
91
+ const { snapshot, cycleId, candidate } = saveInput(call.payload);
92
+ const validation = validateAnalysis(candidate, analysisEvidence(snapshot));
93
+ if (!validation.accepted)
94
+ return refused(validation.codes[0] ?? "analysis-input-refused", validation.codes);
95
+ if (validation.result.openQuestion !== undefined)
96
+ return refused("analysis-input-refused");
97
+ try {
98
+ const directory = await analysisDirectory(settings.runtimeDirectory, snapshot.importId);
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
+ });
118
+ }
119
+ const reasons = new Set([
120
+ "analysis-too-large",
121
+ "redaction-required",
122
+ "invalid-import-id",
123
+ "analysis-invalid-json",
124
+ "analysis-input-refused",
125
+ "analysis-output-failed",
126
+ "unsupported-effect",
127
+ "workflow-not-allowed",
128
+ "payload-not-available",
129
+ ]);
130
+ async function delivered(settings, call) {
131
+ const { intent, mode } = call;
132
+ try {
133
+ if (call.signal?.aborted)
134
+ return { kind: "stopped" };
135
+ const payload = await payloadOf(settings, intent);
136
+ if (call.signal?.aborted)
137
+ return { kind: "stopped" };
138
+ if (intent.method === "read-analysis" && mode === "reconcile")
139
+ return { kind: "absent" };
140
+ if (intent.method === "read-analysis")
141
+ return await read(settings, {
142
+ intent,
143
+ importId: String(payload["importId"]),
144
+ });
145
+ return await save(settings, { intent, payload, mode });
146
+ }
147
+ catch (error) {
148
+ const message = String(Reflect.get(Object(error), "message"));
149
+ return refused(reasons.has(message) ? message : "analysis-input-refused");
150
+ }
151
+ }
152
+ export function chatsWorker(settings) {
153
+ if (process.platform !== "darwin") {
154
+ const unsupported = () => Promise.resolve(refused("platform-unsupported"));
155
+ return {
156
+ deliver: unsupported,
157
+ reconcile: unsupported,
158
+ stop: unsupported,
159
+ close: () => Promise.resolve(),
160
+ };
161
+ }
162
+ const active = new Map();
163
+ const lifecycle = { closed: false };
164
+ const deliver = (intent, signal) => {
165
+ if (lifecycle.closed || signal.aborted)
166
+ return Promise.resolve({ kind: "stopped" });
167
+ const key = operationKey(JSON.stringify([intent.runId, intent.operationId, intent.inputDigest]));
168
+ const known = active.get(key);
169
+ if (known !== undefined)
170
+ return known;
171
+ const pending = delivered(settings, {
172
+ intent,
173
+ mode: "deliver",
174
+ signal,
175
+ }).finally(() => active.delete(key));
176
+ active.set(key, pending);
177
+ return pending;
178
+ };
179
+ const stop = async (intent) => {
180
+ if (!ownIntent(intent))
181
+ return refused("unsupported-effect");
182
+ if ((await settings.workflowId(intent.runId)) !== "analyze-chats")
183
+ return refused("workflow-not-allowed");
184
+ const key = operationKey(JSON.stringify([intent.runId, intent.operationId, intent.inputDigest]));
185
+ const pending = active.get(key);
186
+ if (pending !== undefined)
187
+ return await pending;
188
+ const result = await delivered(settings, { intent, mode: "reconcile" });
189
+ return result.kind === "absent" ? { kind: "stopped" } : result;
190
+ };
191
+ return {
192
+ deliver,
193
+ reconcile: (intent) => delivered(settings, { intent, mode: "reconcile" }),
194
+ stop,
195
+ close: async () => {
196
+ lifecycle.closed = true;
197
+ await Promise.all(active.values());
198
+ },
199
+ };
200
+ }
@@ -1,22 +1,31 @@
1
+ import { type WorkerUsage } from "../worker-usage.ts";
1
2
  export type Worker = "claude" | "codex";
2
3
  export interface StreamResult {
4
+ readonly subtype?: string;
5
+ readonly errorType?: string;
6
+ readonly model: string | null;
3
7
  readonly text: string;
4
8
  readonly failed: boolean;
5
9
  readonly costUsd: number | null;
10
+ readonly usage: WorkerUsage;
6
11
  readonly sessionId: string | null;
7
12
  readonly turns: number | null;
8
13
  }
9
14
  export interface TurnEnd {
15
+ readonly stopped?: boolean;
10
16
  readonly exitCode: number | null;
11
17
  readonly result: StreamResult | undefined;
12
18
  }
13
19
  export interface TurnStart {
20
+ readonly mode?: "analysis";
21
+ readonly analysisToken?: string;
14
22
  readonly worker?: Worker;
15
23
  readonly turnId?: string;
16
24
  readonly environment?: Readonly<Record<string, string>>;
17
25
  readonly command: readonly string[];
18
26
  readonly directory: string;
19
27
  readonly folder: string;
28
+ readonly activityFolder?: string;
20
29
  readonly prompt: string;
21
30
  }
22
31
  export interface RunningTurn {
@@ -24,5 +33,6 @@ export interface RunningTurn {
24
33
  readonly ended: Promise<TurnEnd>;
25
34
  readonly stop: (graceMilliseconds: number) => void;
26
35
  }
36
+ export declare function capacityRefusal(result: StreamResult | undefined): boolean;
27
37
  export declare function streamResult(line: string): StreamResult | undefined;
28
38
  export declare function startTurn(start: TurnStart): RunningTurn;
@@ -2,6 +2,9 @@ import { spawn } from "node:child_process";
2
2
  import { createWriteStream, mkdtempSync, rmSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  import { codexStream } from "./codex-stream.js";
5
+ import { priceOf, usageOf } from "../worker-usage.js";
6
+ import { commandModel } from "../token-prices.js";
7
+ import { analysisCommand, analysisEnvironment } from "./analysis-launch.js";
5
8
  function parsed(line) {
6
9
  try {
7
10
  const value = JSON.parse(line);
@@ -17,6 +20,13 @@ function numberOf(value) {
17
20
  function textOf(value) {
18
21
  return typeof value === "string" ? value : null;
19
22
  }
23
+ export function capacityRefusal(result) {
24
+ return (result?.failed === true &&
25
+ ["success", "error_during_execution"].includes(result.subtype ?? "error_during_execution") &&
26
+ (result.text ===
27
+ "Selected model is at capacity. Please try a different model." ||
28
+ result.errorType === "overloaded"));
29
+ }
20
30
  export function streamResult(line) {
21
31
  if (!line.includes('"result"'))
22
32
  return undefined;
@@ -25,9 +35,17 @@ export function streamResult(line) {
25
35
  if (field("type") !== "result")
26
36
  return undefined;
27
37
  return {
38
+ ...(typeof field("subtype") === "string"
39
+ ? { subtype: String(field("subtype")) }
40
+ : {}),
41
+ ...(typeof field("error") === "string"
42
+ ? { errorType: String(field("error")) }
43
+ : {}),
28
44
  text: textOf(field("result")) ?? "",
29
45
  failed: field("is_error") === true,
30
- costUsd: numberOf(field("total_cost_usd")),
46
+ costUsd: priceOf(field("total_cost_usd")),
47
+ usage: usageOf(field("usage")),
48
+ model: textOf(field("model")),
31
49
  sessionId: textOf(field("session_id")),
32
50
  turns: numberOf(field("num_turns")),
33
51
  };
@@ -38,6 +56,11 @@ function ended(stream) {
38
56
  });
39
57
  }
40
58
  function turnEnvironment(configuration, start) {
59
+ if (start.mode === "analysis")
60
+ return {
61
+ ...analysisEnvironment(start.folder),
62
+ CLAUDE_CODE_OAUTH_TOKEN: start.analysisToken,
63
+ };
41
64
  const excluded = new Set([
42
65
  "GH_TOKEN",
43
66
  "GITHUB_TOKEN",
@@ -56,7 +79,7 @@ function turnEnvironment(configuration, start) {
56
79
  }
57
80
  function spawnTurn(start) {
58
81
  const configuration = mkdtempSync(join(start.folder, "gh-config-"));
59
- const [executable = "", ...fixed] = start.command;
82
+ const [executable = "", ...fixed] = start.mode === "analysis" ? analysisCommand() : start.command;
60
83
  try {
61
84
  const child = spawn(executable, fixed, {
62
85
  cwd: start.directory,
@@ -70,43 +93,101 @@ function spawnTurn(start) {
70
93
  throw error;
71
94
  }
72
95
  }
96
+ function receivedLines(stream, lines) {
97
+ for (const line of lines) {
98
+ const event = parsed(line);
99
+ const field = (name) => Reflect.get(Object(event), name);
100
+ const type = field("type");
101
+ if (type === "assistant" && field("parent_tool_use_id") == null)
102
+ stream.errorType = textOf(field("error"));
103
+ if (type === "system" || type === "result")
104
+ stream.model = textOf(field("model")) ?? stream.model;
105
+ const result = streamResult(line);
106
+ if (result !== undefined) {
107
+ const errorType = result.errorType ?? stream.errorType;
108
+ stream.result = {
109
+ ...result,
110
+ ...(errorType === null ? {} : { errorType }),
111
+ };
112
+ }
113
+ }
114
+ }
115
+ function activityOutput(start) {
116
+ if (start.mode === "analysis")
117
+ return undefined;
118
+ const output = createWriteStream(join(start.activityFolder ?? start.folder, "turn.jsonl"), { flags: start.activityFolder === undefined ? "w" : "a" });
119
+ if (start.activityFolder !== undefined)
120
+ output.write("\n");
121
+ return output;
122
+ }
73
123
  function turnOutput(start, stdout) {
74
- const output = createWriteStream(join(start.folder, "turn.jsonl"));
124
+ const output = activityOutput(start);
75
125
  const codex = start.worker === "codex" ? codexStream() : undefined;
76
- const stream = { pending: "", result: undefined };
126
+ const stream = {
127
+ pending: "",
128
+ result: undefined,
129
+ model: null,
130
+ errorType: null,
131
+ };
132
+ const received = (lines) => {
133
+ receivedLines(stream, lines);
134
+ };
77
135
  const written = (lines) => {
78
136
  for (const line of lines)
79
- output.write(`${line}\n`);
80
- stream.result = lines.map(streamResult).findLast(Boolean) ?? stream.result;
137
+ output?.write(`${line}\n`);
138
+ received(lines);
81
139
  };
82
140
  stdout.setEncoding("utf8");
83
141
  stdout.on("data", (chunk) => {
84
142
  if (codex === undefined)
85
- output.write(chunk);
143
+ output?.write(chunk);
86
144
  const lines = `${stream.pending}${chunk}`.split("\n");
87
145
  stream.pending = lines.pop() ?? "";
88
146
  if (codex !== undefined) {
89
147
  written(lines.flatMap(codex.line));
90
148
  return;
91
149
  }
92
- stream.result = lines.map(streamResult).findLast(Boolean) ?? stream.result;
150
+ received(lines);
93
151
  });
94
152
  return async (exitCode) => {
95
153
  if (codex !== undefined) {
96
154
  written(codex.line(stream.pending));
97
155
  written([codex.result(exitCode)]);
98
156
  }
99
- await ended(output);
100
- return codex === undefined
101
- ? (streamResult(stream.pending) ?? stream.result)
102
- : stream.result;
157
+ if (output !== undefined)
158
+ await ended(output);
159
+ if (codex === undefined)
160
+ received([stream.pending]);
161
+ const result = stream.result;
162
+ return result === undefined
163
+ ? undefined
164
+ : {
165
+ ...result,
166
+ model: result.model ??
167
+ stream.model ??
168
+ commandModel(start.mode === "analysis" ? analysisCommand() : start.command),
169
+ };
170
+ };
171
+ }
172
+ function withoutAnalysisToken(start, result) {
173
+ const token = start.mode === "analysis" ? start.analysisToken : undefined;
174
+ if (token === undefined || result === undefined)
175
+ return result;
176
+ const redact = (value) => value === null ? null : value.replaceAll(token, "[redacted]");
177
+ return {
178
+ ...result,
179
+ text: redact(result.text) ?? "",
180
+ model: redact(result.model),
181
+ sessionId: redact(result.sessionId),
103
182
  };
104
183
  }
105
184
  export function startTurn(start) {
106
185
  const { child, configuration } = spawnTurn(start);
107
186
  const output = turnOutput(start, child.stdout);
108
- const errors = createWriteStream(join(start.folder, "turn.stderr.log"));
109
- child.stderr.pipe(errors);
187
+ if (start.mode === "analysis")
188
+ child.stderr.resume();
189
+ else
190
+ child.stderr.pipe(createWriteStream(join(start.folder, "turn.stderr.log")));
110
191
  child.stdin.on("error", () => undefined);
111
192
  child.stdin.end(start.prompt);
112
193
  const closed = new Promise((settle) => {
@@ -121,7 +202,7 @@ export function startTurn(start) {
121
202
  timers.forEach(clearTimeout);
122
203
  return {
123
204
  exitCode,
124
- result: await output(exitCode),
205
+ result: withoutAnalysisToken(start, await output(exitCode)),
125
206
  };
126
207
  })
127
208
  .finally(() => {
@@ -1,8 +1,11 @@
1
+ import { type TokenPrices } from "../token-prices.ts";
1
2
  import { type TurnProfiles } from "../turn-profile.ts";
2
3
  import type { Delivery } from "../broker/actor/delivery-intents.ts";
3
4
  import { type Worker } from "./claude-turn.ts";
5
+ import { type RetrySettings } from "./retrying-turn.ts";
4
6
  import { type WorkerReport } from "./operation-journal.ts";
5
- export interface ClaudeWorkerSettings {
7
+ export interface ClaudeWorkerSettings extends RetrySettings {
8
+ readonly tokenPrices?: TokenPrices;
6
9
  readonly worker?: Worker;
7
10
  readonly profiles?: TurnProfiles;
8
11
  readonly workflowId?: (runId: string) => Promise<string | undefined>;