@smartmemory/compose 0.3.6-beta → 0.3.8
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/.claude/skills/compose/SKILL.md +42 -88
- package/.compose-deps.json +1 -13
- package/README.md +72 -5
- package/bin/compose.js +754 -347
- package/bin/git-hooks/pre-push.template +29 -0
- package/bin/judgment-import.js +7 -0
- package/bin/judgment-migrate.js +387 -0
- package/contracts/comp-obs-contract.schema.json +9 -3
- package/contracts/feature-json.schema.json +5 -0
- package/contracts/fluid-record.schema.json +209 -0
- package/contracts/judgment-record.schema.json +425 -4
- package/contracts/lifecycle-backfill.schema.json +322 -0
- package/dist/assets/App-Z4MU-H_F.js +916 -0
- package/dist/assets/_baseUniq-ClWoCPFl.js +1 -0
- package/dist/assets/arc-DY26UIVo.js +1 -0
- package/dist/assets/architectureDiagram-Q4EWVU46-6Ggq4DqJ.js +36 -0
- package/dist/assets/blockDiagram-DXYQGD6D-CH3Ked0l.js +132 -0
- package/dist/assets/{browser-BSM23If2.js → browser-BWkrenen.js} +6 -6
- package/dist/assets/{c4Diagram-LMCZKHZV-DZf45Fbz.js → c4Diagram-AHTNJAMY-Bk8dYilu.js} +1 -1
- package/dist/assets/channel-SnZzzh7k.js +1 -0
- package/dist/assets/{chunk-JWPE2WC7-_7ujgd_Q.js → chunk-4BX2VUAB-BMR0XaAQ.js} +1 -1
- package/dist/assets/chunk-4TB4RGXK-JytR14a9.js +206 -0
- package/dist/assets/{chunk-XXDRQBXY-DfdVhbmA.js → chunk-55IACEB6-B4Q97BCP.js} +1 -1
- package/dist/assets/{chunk-VR4S4FIN-Dt9NZ67m.js → chunk-EDXVE4YY-R_qarkSf.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-BY4_PV5H.js → chunk-FMBD7UC4-C9s7KR9m.js} +1 -1
- package/dist/assets/chunk-OYMX7WX6-BySQzVxc.js +231 -0
- package/dist/assets/{chunk-2Q5K7J3B-Dn1spZYu.js → chunk-QZHKN3VN-DdpSYZsW.js} +1 -1
- package/dist/assets/{chunk-32BRIVSS-pURGrJDk.js → chunk-YZCP3GAM-iE_tzriw.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-CBu92dSH.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-CBu92dSH.js +1 -0
- package/dist/assets/clone-DgklGjHm.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-BieYif4o.js → cose-bilkent-S5V4N54A-BdlU6ZX_.js} +1 -1
- package/dist/assets/dagre-KV5264BT-Cp3F5KTn.js +4 -0
- package/dist/assets/diagram-5BDNPKRD-DiR6_2q_.js +10 -0
- package/dist/assets/diagram-G4DWMVQ6-w0i-p5HX.js +24 -0
- package/dist/assets/diagram-MMDJMWI5-tIHhwUv3.js +43 -0
- package/dist/assets/diagram-TYMM5635-BAeY3B19.js +24 -0
- package/dist/assets/erDiagram-SMLLAGMA-Ckx_Knko.js +85 -0
- package/dist/assets/flowDiagram-DWJPFMVM-DeoNka6J.js +162 -0
- package/dist/assets/ganttDiagram-T4ZO3ILL-BmGnFbEg.js +292 -0
- package/dist/assets/gitGraphDiagram-UUTBAWPF-Dk48IHsx.js +106 -0
- package/dist/assets/graph-BNzKGvoy.js +1 -0
- package/dist/assets/graph-CI_1htl0.js +331 -0
- package/dist/assets/index-BEfrNBp8.js +123 -0
- package/dist/assets/index-yyrA5OZd.css +1 -0
- package/dist/assets/infoDiagram-42DDH7IO-BRf827i0.js +2 -0
- package/dist/assets/{ishikawaDiagram-FXEZZL3T-CzEB9fQS.js → ishikawaDiagram-UXIWVN3A-0kCZaeCM.js} +5 -5
- package/dist/assets/{journeyDiagram-5HDEW3XC-Bz8TCdz2.js → journeyDiagram-VCZTEJTY-rvU7ayRt.js} +1 -1
- package/dist/assets/{kanban-definition-HUTT4EX6-tozrMoV_.js → kanban-definition-6JOO6SKY-DpQwX1C5.js} +7 -7
- package/dist/assets/katex-DkKDou_j.js +257 -0
- package/dist/assets/layout-BI8cXFPI.js +1 -0
- package/dist/assets/{linear-Ck7gpa5N.js → linear-a0glcDiw.js} +1 -1
- package/dist/assets/min-vPHfnXcC.js +1 -0
- package/dist/assets/{mindmap-definition-LN4V7U3C-DTcHO0DJ.js → mindmap-definition-QFDTVHPH-D14eF-7C.js} +7 -7
- package/dist/assets/mobile-B7m9EO9D.js +17 -0
- package/dist/assets/pieDiagram-DEJITSTG-Cno-gETh.js +30 -0
- package/dist/assets/quadrantDiagram-34T5L4WZ-BUQM1Hfm.js +7 -0
- package/dist/assets/{requirementDiagram-TGXJPOKE-bnI2zJeT.js → requirementDiagram-MS252O5E-pOXlN2-q.js} +3 -3
- package/dist/assets/sankeyDiagram-XADWPNL6-Crynd3_b.js +10 -0
- package/dist/assets/sequenceDiagram-FGHM5R23-D9fZdCM8.js +157 -0
- package/dist/assets/stateDiagram-FHFEXIEX-CW9qVec8.js +1 -0
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-DkVLzHbY.js +1 -0
- package/dist/assets/{timeline-definition-FHXFAJF6-D267GQFF.js → timeline-definition-GMOUNBTQ-BcHzhm_8.js} +3 -3
- package/dist/assets/vennDiagram-DHZGUBPP-BfytJcWk.js +34 -0
- package/dist/assets/wardley-RL74JXVD-DLj-IjyB.js +162 -0
- package/dist/assets/wardleyDiagram-NUSXRM2D-Ds0Ue68c.js +20 -0
- package/dist/assets/xychartDiagram-5P7HB3ND-vjWDXFL6.js +7 -0
- package/dist/index.html +3 -3
- package/lib/agent-string.js +7 -5
- package/lib/append-integrity.js +81 -0
- package/lib/backfill-evidence.js +109 -0
- package/lib/bug-escalation.js +39 -4
- package/lib/build-stream-schema.js +3 -1
- package/lib/build-stream-writer.js +25 -0
- package/lib/build.js +1624 -195
- package/lib/canon-guard.js +245 -0
- package/lib/canon-override.js +196 -0
- package/lib/canon-registry.js +291 -0
- package/lib/cli-commands.js +144 -0
- package/lib/codex-preflight.js +50 -15
- package/lib/colleague/context.js +215 -0
- package/lib/colleague/writeback.js +95 -0
- package/lib/completion-gate.js +1421 -0
- package/lib/completion-writer.js +47 -47
- package/lib/consumer-fanout.js +105 -11
- package/lib/coverage-gate.js +200 -0
- package/lib/dir-lock.js +170 -0
- package/lib/dispatch-ledger.js +301 -0
- package/lib/dispatch-metrics.js +236 -0
- package/lib/experiment-judge.js +6 -1
- package/lib/feature-json.js +1 -1
- package/lib/feature-reconciler.js +8 -0
- package/lib/feature-validator.js +64 -1
- package/lib/feature-writer.js +66 -2
- package/lib/fluid/factory.js +167 -0
- package/lib/fluid/ideabox-dates.js +73 -0
- package/lib/fluid/ideabox-migrate.js +154 -0
- package/lib/fluid/ideabox-ops.js +585 -0
- package/lib/fluid/ideabox-view.js +146 -0
- package/lib/fluid/import-ideabox.js +186 -0
- package/lib/fluid/local-provider.js +606 -0
- package/lib/fluid/provider.js +684 -0
- package/lib/fluid/record-shape.js +214 -0
- package/lib/fluid/record-store.js +328 -0
- package/lib/fluid/render-ideabox.js +261 -0
- package/lib/fluid/schema.js +40 -0
- package/lib/fluid/smartmemory-provider.js +1695 -0
- package/lib/gsd.js +63 -14
- package/lib/guard-cli.js +175 -0
- package/lib/guard-custody.js +141 -0
- package/lib/guard-descriptors.js +530 -0
- package/lib/guard-enrol.js +254 -0
- package/lib/health-score.js +1 -1
- package/lib/hooks-status.js +32 -3
- package/lib/ideabox-cli.js +315 -0
- package/lib/ideabox.js +121 -21
- package/lib/judgment/store/index.js +166 -0
- package/lib/judgment/store/records.js +184 -25
- package/lib/judgment/trace.js +380 -0
- package/lib/judgment-attest.js +259 -0
- package/lib/judgment-decision-write.js +277 -0
- package/lib/judgment-decisions.js +466 -0
- package/lib/judgment-gen.js +375 -22
- package/lib/judgment-verify.js +153 -0
- package/lib/judgment-writer.js +2842 -262
- package/lib/lane-gate.js +2 -0
- package/lib/lifecycle-modes.js +4 -4
- package/lib/lineage.js +400 -0
- package/lib/local-claude-connector.js +250 -54
- package/lib/maya-client.js +302 -0
- package/lib/maya-config.js +53 -0
- package/lib/maya-identity.js +283 -0
- package/lib/mcp-enforcement.js +21 -35
- package/lib/migrate-anon.js +5 -0
- package/lib/migrate-roadmap.js +15 -0
- package/lib/new.js +13 -1
- package/lib/pipeline-compat.js +104 -0
- package/lib/policy-catalog.js +295 -0
- package/lib/policy-check.js +0 -0
- package/lib/process-termination.js +98 -0
- package/lib/resolve-workspace.js +5 -1
- package/lib/result-normalizer.js +428 -153
- package/lib/review-normalize.js +4 -0
- package/lib/roadmap-errors.js +65 -0
- package/lib/roadmap-preservers.js +24 -4
- package/lib/roadmap-residue.js +299 -0
- package/lib/smartmemory-client.js +614 -78
- package/lib/smartmemory-config.js +54 -0
- package/lib/smartmemory-ingest.js +19 -2
- package/lib/step-prompt.js +7 -6
- package/lib/stratum-engine.js +53 -4
- package/lib/stratum-mcp-client.js +391 -31
- package/lib/test-bootstrap.js +31 -0
- package/lib/tool-inventory.js +122 -0
- package/lib/version-check.js +91 -19
- package/lib/vision-writer.js +88 -1
- package/package.json +7 -6
- package/pipelines/bug-fix.stratum.yaml +205 -211
- package/pipelines/build-quick.profiles.json +12 -0
- package/pipelines/build-quick.stratum.yaml +263 -350
- package/pipelines/content.stratum.yaml +81 -77
- package/pipelines/coverage-sweep.stratum.yaml +49 -30
- package/pipelines/plan.stratum.yaml +76 -86
- package/pipelines/refactor.stratum.yaml +125 -125
- package/pipelines/research.stratum.yaml +56 -58
- package/pipelines/review-fix.profiles.json +6 -0
- package/pipelines/review-fix.stratum.yaml +110 -83
- package/presets/team-feature.profiles.json +6 -0
- package/presets/team-feature.stratum.yaml +93 -66
- package/presets/team-research.profiles.json +6 -0
- package/presets/team-research.stratum.yaml +89 -80
- package/presets/team-review.profiles.json +8 -0
- package/presets/team-review.stratum.yaml +98 -80
- package/scripts/cost-census.mjs +70 -0
- package/scripts/guard-sign/compose-guard-sign.sh +62 -0
- package/server/agent-health.js +22 -0
- package/server/agent-hooks.js +14 -1
- package/server/agent-server.js +5 -248
- package/server/agent-spawn.js +3 -4
- package/server/agent-workspace.js +294 -0
- package/server/build-routes.js +6 -5
- package/server/build-stream-bridge.js +53 -0
- package/server/cc-session-watcher.js +4 -1
- package/server/coalescing-buffer.js +7 -1
- package/server/completion-projection.js +228 -0
- package/server/compose-mcp-tools.js +124 -24
- package/server/compose-mcp.js +91 -790
- package/server/decision-event-emit.js +41 -2
- package/server/decision-event-id.js +17 -0
- package/server/decision-events-snapshot.js +3 -0
- package/server/design-routes.js +14 -8
- package/server/feature-scan.js +76 -2
- package/server/file-watcher.js +170 -21
- package/server/ideabox-routes.js +166 -224
- package/server/index.js +70 -100
- package/server/lifecycle-guard.js +240 -10
- package/server/lifecycle-phase-history.js +276 -0
- package/server/maya-routes.js +507 -0
- package/server/mcp-tool-defs.js +940 -0
- package/server/mcp-tool-policy.js +35 -3
- package/server/model-tiers.js +22 -5
- package/server/pipeline-routes.js +21 -11
- package/server/project-root.js +58 -19
- package/server/remote-utils.js +3 -1
- package/server/schema-validator.js +7 -1
- package/server/session-manager.js +5 -6
- package/server/session-routes.js +3 -1
- package/server/stratum-client.js +57 -10
- package/server/stratum-sync.js +6 -3
- package/server/summarizer.js +3 -4
- package/server/supervisor.js +0 -1
- package/server/vision-routes.js +208 -98
- package/server/vision-server.js +86 -23
- package/server/vision-store.js +60 -6
- package/server/vision-utils.js +3 -4
- package/server/workspace-activity.js +18 -0
- package/server/workspace-middleware.js +2 -2
- package/server/workspace-runtime.js +243 -0
- package/server/worktree-gc.js +1 -0
- package/dist/assets/App-BG3ngu8H.js +0 -896
- package/dist/assets/abnfDiagram-VRR7QNED-CjB_sD3D.js +0 -1
- package/dist/assets/arc-_v4hR_uD.js +0 -1
- package/dist/assets/architectureDiagram-ZJ3FMSHR-DreJmzXQ.js +0 -36
- package/dist/assets/blockDiagram-677ZJIJ3-BG9-c0O1.js +0 -132
- package/dist/assets/channel-B3U5wFAT.js +0 -1
- package/dist/assets/chunk-EX3LRPZG-DdELs1qP.js +0 -231
- package/dist/assets/chunk-MOJQB5TN-D-ky35G-.js +0 -88
- package/dist/assets/chunk-RYQCIY6F-Dag_kVlO.js +0 -1
- package/dist/assets/chunk-V7JOEXUC-BtewURat.js +0 -206
- package/dist/assets/classDiagram-OUVF2IWQ-B6fCN-ht.js +0 -1
- package/dist/assets/classDiagram-v2-EOCWNBFH-B6fCN-ht.js +0 -1
- package/dist/assets/cynefin-VYW2F7L2-CT2BA6KE.js +0 -178
- package/dist/assets/cynefinDiagram-TSTJHNR4-Bh6exbyg.js +0 -62
- package/dist/assets/dagre-VKFMJZFB-aXMLSmQL.js +0 -4
- package/dist/assets/diagram-FQU43EPY-Dr7JAOuQ.js +0 -3
- package/dist/assets/diagram-G47NLZAW-DUvA3FQK.js +0 -24
- package/dist/assets/diagram-NH7WQ7WH-BQUARqcu.js +0 -24
- package/dist/assets/diagram-OA4YK3LP-dDUc1zHi.js +0 -30
- package/dist/assets/diagram-WEI45ONY-B2h5Qlb1.js +0 -41
- package/dist/assets/ebnfDiagram-CCIWWBDH-DThRGupB.js +0 -1
- package/dist/assets/erDiagram-Q63AITRT-BUCsprO2.js +0 -85
- package/dist/assets/flowDiagram-23GEKE2U-DXtNNi6r.js +0 -156
- package/dist/assets/ganttDiagram-NO4QXBWP-D4zbBHh_.js +0 -292
- package/dist/assets/gitGraphDiagram-IHSO6WYX-DpoQws0W.js +0 -106
- package/dist/assets/graph-BXPQrYYB.js +0 -331
- package/dist/assets/graph-C9eacEi8.js +0 -1
- package/dist/assets/index-3ZH5eMcZ.js +0 -119
- package/dist/assets/index-LIwREYgH.css +0 -1
- package/dist/assets/infoDiagram-FWYZ7A6U-Bbas2GAo.js +0 -2
- package/dist/assets/katex-C5jXJg4s.js +0 -257
- package/dist/assets/layout-DEXfKzaS.js +0 -1
- package/dist/assets/map-Czzmt4hB.js +0 -1
- package/dist/assets/mobile-CaoXUwAr.js +0 -17
- package/dist/assets/pegDiagram-2B236MQR-CHiINrNy.js +0 -1
- package/dist/assets/pieDiagram-ENE6RG2P-CfS4YFlR.js +0 -39
- package/dist/assets/quadrantDiagram-ABIIQ3AL-CadesS9w.js +0 -7
- package/dist/assets/railroadDiagram-RFXS5EU6-CgWEspBN.js +0 -1
- package/dist/assets/sankeyDiagram-HTMAVEWB-YWKFgOGw.js +0 -40
- package/dist/assets/sequenceDiagram-DBY2YBRQ-BvkNOyF9.js +0 -162
- package/dist/assets/sizeCapture-X5ZJPWSS-DlFPA2yO.js +0 -1
- package/dist/assets/stateDiagram-2N3HPSRC-h8NIx0kQ.js +0 -1
- package/dist/assets/stateDiagram-v2-6OUMAXLB-DjPgZtJ9.js +0 -1
- package/dist/assets/swimlanes-5IMT3BWC-CT5n22kG.js +0 -2
- package/dist/assets/swimlanesDiagram-G3AALYLV-Dn318Bhq.js +0 -8
- package/dist/assets/vennDiagram-L72KCM5P-Dj-wWLYG.js +0 -34
- package/dist/assets/wardleyDiagram-EHGQE667-BxCeYxkG.js +0 -78
- package/dist/assets/xychartDiagram-FW5EYKEG-DMFqWn7z.js +0 -7
- package/lib/staleness.js +0 -87
- package/server/ideabox-cache.js +0 -77
package/lib/result-normalizer.js
CHANGED
|
@@ -224,6 +224,21 @@ export class AgentAbortedError extends Error {
|
|
|
224
224
|
}
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
function copyDispatchId(source, target) {
|
|
228
|
+
try {
|
|
229
|
+
if (!source || !target || typeof source.dispatchId !== 'string') return target;
|
|
230
|
+
Object.defineProperty(target, 'dispatchId', {
|
|
231
|
+
configurable: true,
|
|
232
|
+
enumerable: false,
|
|
233
|
+
value: source.dispatchId,
|
|
234
|
+
});
|
|
235
|
+
} catch {
|
|
236
|
+
// Error replacement must preserve the original control flow even if a
|
|
237
|
+
// third-party target is unexpectedly frozen.
|
|
238
|
+
}
|
|
239
|
+
return target;
|
|
240
|
+
}
|
|
241
|
+
|
|
227
242
|
/**
|
|
228
243
|
* STRAT-DEDUP-AGENTRUN-V3: `runAndNormalize` is now a thin wrapper around the
|
|
229
244
|
* Python connector tier exposed through `stratum_agent_run`. Events arrive as
|
|
@@ -236,6 +251,75 @@ export class AgentAbortedError extends Error {
|
|
|
236
251
|
* call-sites do not all need to be edited in a single sweep. New required opt:
|
|
237
252
|
* `opts.stratum` — the StratumMcpClient instance.
|
|
238
253
|
*/
|
|
254
|
+
/**
|
|
255
|
+
* Sum two usage records into one, for a step whose work took more than one
|
|
256
|
+
* agent call (e.g. a COMP-POLICY-CHECK revision replacing the primary draft).
|
|
257
|
+
* Token/cost fields add; `model` keeps the later run's value when it has one.
|
|
258
|
+
* Either side may be null.
|
|
259
|
+
*
|
|
260
|
+
* @param {object|null} a
|
|
261
|
+
* @param {object|null} b
|
|
262
|
+
* @returns {object|null}
|
|
263
|
+
*/
|
|
264
|
+
export function mergeUsage(a, b) {
|
|
265
|
+
if (!a) return b ?? null;
|
|
266
|
+
if (!b) return a;
|
|
267
|
+
return {
|
|
268
|
+
input_tokens: (a.input_tokens ?? 0) + (b.input_tokens ?? 0),
|
|
269
|
+
output_tokens: (a.output_tokens ?? 0) + (b.output_tokens ?? 0),
|
|
270
|
+
cache_creation_input_tokens: (a.cache_creation_input_tokens ?? 0) + (b.cache_creation_input_tokens ?? 0),
|
|
271
|
+
cache_read_input_tokens: (a.cache_read_input_tokens ?? 0) + (b.cache_read_input_tokens ?? 0),
|
|
272
|
+
cost_usd: (a.cost_usd ?? 0) + (b.cost_usd ?? 0),
|
|
273
|
+
model: b.model ?? a.model ?? null,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function hasReportedUsage(usage) {
|
|
278
|
+
if (!usage || typeof usage !== 'object') return false;
|
|
279
|
+
return [
|
|
280
|
+
usage.tokens, usage.input_tokens, usage.output_tokens,
|
|
281
|
+
usage.cache_read, usage.cache_read_input_tokens,
|
|
282
|
+
usage.cache_creation, usage.cache_creation_input_tokens,
|
|
283
|
+
usage.cost_usd, usage.usd, usage.ms, usage.duration_ms,
|
|
284
|
+
].some((value) => typeof value === 'number' && Number.isFinite(value) && value !== 0);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function usageRecordFromRaw(usage, telemetry, dispatchId, fallback = {}, split = null) {
|
|
288
|
+
if (!hasReportedUsage(usage)) return null;
|
|
289
|
+
// STRAT-USAGE-SPLIT: prefer the connector-reported split over reconstruction
|
|
290
|
+
// (the reconstruction cannot tell input from output; it filed the aggregate
|
|
291
|
+
// as output on every record before surface 16).
|
|
292
|
+
const input = split?.input ?? usage.input_tokens ?? 0;
|
|
293
|
+
const output = split?.output
|
|
294
|
+
?? usage.output_tokens
|
|
295
|
+
?? (typeof usage.tokens === 'number' ? Math.max(0, usage.tokens - input) : 0);
|
|
296
|
+
const record = {
|
|
297
|
+
dispatch_id: dispatchId ?? randomUUID(),
|
|
298
|
+
model: telemetry?.model ?? usage.model ?? fallback.model ?? 'unknown',
|
|
299
|
+
...(telemetry?.effort ?? fallback.effort
|
|
300
|
+
? { effort: telemetry?.effort ?? fallback.effort }
|
|
301
|
+
: {}),
|
|
302
|
+
duration_ms: telemetry?.durationMs ?? usage.duration_ms ?? usage.ms ?? fallback.duration_ms ?? 0,
|
|
303
|
+
input_tokens: input,
|
|
304
|
+
output_tokens: output,
|
|
305
|
+
...(typeof (split?.cacheRead ?? usage.cache_read ?? usage.cache_read_input_tokens) === 'number'
|
|
306
|
+
? { cache_read: split?.cacheRead ?? usage.cache_read ?? usage.cache_read_input_tokens }
|
|
307
|
+
: {}),
|
|
308
|
+
...(typeof (split?.cacheCreation ?? usage.cache_creation ?? usage.cache_creation_input_tokens) === 'number'
|
|
309
|
+
? { cache_creation: split?.cacheCreation ?? usage.cache_creation ?? usage.cache_creation_input_tokens }
|
|
310
|
+
: {}),
|
|
311
|
+
};
|
|
312
|
+
const cost = usage.cost_usd ?? usage.usd;
|
|
313
|
+
const usdSource = ['reported', 'estimated'].includes(usage.usd_source)
|
|
314
|
+
? usage.usd_source
|
|
315
|
+
: (['reported', 'estimated'].includes(fallback.usd_source) ? fallback.usd_source : null);
|
|
316
|
+
if (typeof cost === 'number' && Number.isFinite(cost) && cost > 0 && usdSource) {
|
|
317
|
+
record.cost_usd = cost;
|
|
318
|
+
record.usd_source = usdSource;
|
|
319
|
+
}
|
|
320
|
+
return record;
|
|
321
|
+
}
|
|
322
|
+
|
|
239
323
|
export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, opts = {}) {
|
|
240
324
|
const progress = opts.progress;
|
|
241
325
|
const streamWriter = opts.streamWriter;
|
|
@@ -252,17 +336,45 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
252
336
|
|
|
253
337
|
const stepId = stepDispatch.step_id ?? 'unknown';
|
|
254
338
|
const agentType = stepDispatch.agent ?? 'claude';
|
|
339
|
+
// COMP-AGENT-LANES: when the caller (a parallel fanout item) supplies a lane
|
|
340
|
+
// envelope, stamp it on every relayed stream write so the cockpit can route
|
|
341
|
+
// this run's output to its worker lane. Absent the opt, writes stay
|
|
342
|
+
// byte-identical (single-step runs unchanged).
|
|
343
|
+
const laneStamp = (opts.lane && typeof opts.lane === 'object') ? { lane: opts.lane } : {};
|
|
255
344
|
// D6: the engine ships only the bare provider literal (stepDispatch.agent), so
|
|
256
345
|
// the full profile string (with tool restrictions + model tier) is supplied
|
|
257
346
|
// compose-side via opts.profile, keyed off the compose-owned sidecar. It
|
|
258
347
|
// overrides the bare literal for capability resolution; the provider is
|
|
259
348
|
// unchanged. Absent → bare literal (no restrictions), preserving old behavior.
|
|
260
349
|
const cfg = resolveAgentConfig(opts.profile || agentType);
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const
|
|
350
|
+
const callerTelemetry = opts.telemetry && typeof opts.telemetry === 'object'
|
|
351
|
+
? opts.telemetry
|
|
352
|
+
: {};
|
|
353
|
+
const primaryTelemetry = {};
|
|
354
|
+
for (const field of ['project_cwd', 'site', 'build_id', 'feature_code']) {
|
|
355
|
+
if (callerTelemetry[field] !== undefined) primaryTelemetry[field] = callerTelemetry[field];
|
|
356
|
+
}
|
|
357
|
+
primaryTelemetry.step_id = stepId;
|
|
358
|
+
if (typeof stepDispatch.attempt === 'number' && Number.isFinite(stepDispatch.attempt)) {
|
|
359
|
+
primaryTelemetry.attempt = stepDispatch.attempt;
|
|
360
|
+
}
|
|
361
|
+
primaryTelemetry.effort_intended = cfg.effort ?? null;
|
|
362
|
+
// Claude profiles bind through SDK tool filters. Codex has an OS sandbox;
|
|
363
|
+
// write permission must be explicitly requested by the implementation caller.
|
|
364
|
+
const sandboxMode = cfg.provider === 'codex'
|
|
365
|
+
? (opts.sandboxMode ?? 'read-only')
|
|
366
|
+
: undefined;
|
|
367
|
+
const executionOptions = {
|
|
368
|
+
modelID: cfg.modelID ?? undefined,
|
|
369
|
+
...(cfg.provider === 'claude' ? {
|
|
370
|
+
allowedTools: cfg.allowedTools ?? undefined,
|
|
371
|
+
disallowedTools: cfg.disallowedTools ?? undefined,
|
|
372
|
+
thinking: cfg.thinking ?? undefined,
|
|
373
|
+
} : {}),
|
|
374
|
+
effort: cfg.effort ?? undefined,
|
|
375
|
+
sandboxMode,
|
|
376
|
+
cwd: opts.cwd ?? undefined,
|
|
377
|
+
};
|
|
266
378
|
|
|
267
379
|
const outputFields = stepDispatch.output_fields;
|
|
268
380
|
const hasSchema = outputFields && typeof outputFields === 'object' && Object.keys(outputFields).length > 0;
|
|
@@ -298,6 +410,7 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
298
410
|
cost_usd: 0,
|
|
299
411
|
model: null,
|
|
300
412
|
};
|
|
413
|
+
let primaryUsdSource = null;
|
|
301
414
|
|
|
302
415
|
let timedOut = false;
|
|
303
416
|
let userInterruptAction = null;
|
|
@@ -306,21 +419,19 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
306
419
|
// the run mid-stream by returning a truthy reason from a tool event.
|
|
307
420
|
let abortReason = null;
|
|
308
421
|
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// restrictions AND be interrupted (the sync engine agent_run can do neither;
|
|
312
|
-
// its background mode is codex+read-only-only). Abort replaces cancelAgentRun
|
|
313
|
-
// as the stop mechanism, and the connector streams real tool events so the
|
|
314
|
-
// stuck detector / timeout actually stop a runaway agent.
|
|
422
|
+
// Both local SDK and MCP executions own a cancellable handle. The MCP
|
|
423
|
+
// client translates this signal to an acknowledged foreground cancellation.
|
|
315
424
|
const useLocalClaude = opts.localExecution === true && cfg.provider === 'claude';
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
|
|
425
|
+
const primaryFailure = (source, target) => {
|
|
426
|
+
const record = usageRecordFromRaw(source?.usage, source?.telemetry, source?.dispatchId, {
|
|
427
|
+
model: cfg.modelID, effort: cfg.effort, duration_ms: Date.now() - startTime,
|
|
428
|
+
usd_source: source?.usdSource ?? (useLocalClaude ? 'reported' : undefined),
|
|
429
|
+
}, source?.split);
|
|
430
|
+
if (record) target.usages = [record];
|
|
431
|
+
return copyDispatchId(source, target);
|
|
323
432
|
};
|
|
433
|
+
const abortController = new AbortController();
|
|
434
|
+
const stopRun = () => abortController.abort();
|
|
324
435
|
|
|
325
436
|
// Subscribe BEFORE calling agentRun — events fire during the call.
|
|
326
437
|
const unsub = stratum.onEvent(correlationId, subStepId, (env) => {
|
|
@@ -333,14 +444,14 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
333
444
|
case 'agent_relay':
|
|
334
445
|
if (m.role === 'assistant' && typeof m.text === 'string' && m.text.length > 0) {
|
|
335
446
|
textParts.push(m.text);
|
|
336
|
-
if (streamWriter) streamWriter.write({ type: 'assistant', content: m.text });
|
|
447
|
+
if (streamWriter) streamWriter.write({ type: 'assistant', content: m.text, ...laneStamp });
|
|
337
448
|
}
|
|
338
449
|
break;
|
|
339
450
|
case 'tool_use_summary': {
|
|
340
451
|
const tool = m.tool;
|
|
341
452
|
if (tool) {
|
|
342
453
|
if (streamWriter) {
|
|
343
|
-
streamWriter.write({ type: 'tool_use', tool, input: m.input ?? {} });
|
|
454
|
+
streamWriter.write({ type: 'tool_use', tool, input: m.input ?? {}, ...laneStamp });
|
|
344
455
|
}
|
|
345
456
|
if (onToolUse) onToolUse({ tool, input: m.input ?? {}, timestamp: Date.now() });
|
|
346
457
|
if (progress) {
|
|
@@ -350,7 +461,7 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
350
461
|
}
|
|
351
462
|
if (m.summary) {
|
|
352
463
|
if (streamWriter) {
|
|
353
|
-
streamWriter.write({ type: 'tool_use_summary', summary: m.summary, output: m.output ?? '' });
|
|
464
|
+
streamWriter.write({ type: 'tool_use_summary', summary: m.summary, output: m.output ?? '', ...laneStamp });
|
|
354
465
|
}
|
|
355
466
|
if (progress) progress.toolSummary(m.summary);
|
|
356
467
|
}
|
|
@@ -369,6 +480,9 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
369
480
|
const stepCost = m.cost_usd != null
|
|
370
481
|
? m.cost_usd
|
|
371
482
|
: calculateCost(m.model, inTok, outTok, ccit, crit);
|
|
483
|
+
primaryUsdSource = m.cost_usd != null && primaryUsdSource !== 'estimated'
|
|
484
|
+
? 'reported'
|
|
485
|
+
: 'estimated';
|
|
372
486
|
usageTotals.cost_usd += stepCost;
|
|
373
487
|
if (streamWriter) {
|
|
374
488
|
streamWriter.write({
|
|
@@ -379,6 +493,7 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
379
493
|
cache_read_input_tokens: crit,
|
|
380
494
|
cost_usd: stepCost,
|
|
381
495
|
model: m.model ?? null,
|
|
496
|
+
...laneStamp,
|
|
382
497
|
});
|
|
383
498
|
}
|
|
384
499
|
break;
|
|
@@ -420,7 +535,7 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
420
535
|
// abort a spinning run.
|
|
421
536
|
const localOnToolUse = ({ tool, input }) => {
|
|
422
537
|
if (onToolUse) onToolUse({ tool, input, timestamp: Date.now() });
|
|
423
|
-
if (streamWriter) streamWriter.write({ type: 'tool_use', tool, input: input ?? {} });
|
|
538
|
+
if (streamWriter) streamWriter.write({ type: 'tool_use', tool, input: input ?? {}, ...laneStamp });
|
|
424
539
|
if (progress) progress.toolUse(tool, input?.command ?? input?.pattern ?? input?.file_path ?? '');
|
|
425
540
|
if (opts.onAgentEvent && !abortReason) {
|
|
426
541
|
const env = { schema_version: '0.2.6', kind: 'tool_use_summary', metadata: { tool, input: input ?? {}, summary: '', output: '' } };
|
|
@@ -429,156 +544,316 @@ export async function runAndNormalize(_connectorIgnored, prompt, stepDispatch, o
|
|
|
429
544
|
}
|
|
430
545
|
};
|
|
431
546
|
|
|
432
|
-
|
|
547
|
+
// Keep the deadline and interrupt controls active through review repair.
|
|
433
548
|
try {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
549
|
+
let runResult;
|
|
550
|
+
let primaryDispatchId = null;
|
|
551
|
+
let repairDispatchId = null;
|
|
552
|
+
try {
|
|
553
|
+
if (useLocalClaude) {
|
|
554
|
+
// Test seam: an installed factory shim exposes an SDK-shaped query adapter
|
|
555
|
+
// so the goldens drive the local path without spawning a real claude.
|
|
556
|
+
// Gated on NODE_ENV=test so production always uses the real SDK.
|
|
557
|
+
const localQuery = opts.localQuery
|
|
558
|
+
?? (process.env.NODE_ENV === 'test' && stratum ? stratum._localQuery : undefined);
|
|
559
|
+
runResult = await runLocalClaudeAgent(actualPrompt, {
|
|
560
|
+
cwd: opts.cwd ?? undefined,
|
|
561
|
+
model: cfg.modelID ?? undefined,
|
|
562
|
+
allowedTools: cfg.allowedTools ?? undefined,
|
|
563
|
+
disallowedTools: cfg.disallowedTools ?? undefined,
|
|
564
|
+
thinking: cfg.thinking ?? undefined,
|
|
565
|
+
effort: cfg.effort ?? undefined,
|
|
566
|
+
abortController,
|
|
567
|
+
onToolUse: localOnToolUse,
|
|
568
|
+
// COMP-AGENT-LANES: only a lane-carrying run (parallel fanout item)
|
|
569
|
+
// relays local assistant text to the stream — engine-path parity for
|
|
570
|
+
// the lanes UI. Lane-less local runs keep their historical shape (no
|
|
571
|
+
// assistant stream writes).
|
|
572
|
+
...(laneStamp.lane && streamWriter ? {
|
|
573
|
+
onAssistantText: (text) => {
|
|
574
|
+
streamWriter.write({ type: 'assistant', content: text, ...laneStamp });
|
|
575
|
+
},
|
|
576
|
+
} : {}),
|
|
577
|
+
telemetry: primaryTelemetry,
|
|
578
|
+
...(localQuery ? { query: localQuery } : {}),
|
|
579
|
+
});
|
|
580
|
+
} else {
|
|
581
|
+
runResult = await stratum.agentRun(agentType, actualPrompt, {
|
|
582
|
+
...executionOptions,
|
|
583
|
+
signal: abortController.signal,
|
|
584
|
+
correlationId,
|
|
585
|
+
telemetry: primaryTelemetry,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
primaryDispatchId = typeof runResult?.dispatchId === 'string'
|
|
589
|
+
? runResult.dispatchId
|
|
590
|
+
: null;
|
|
591
|
+
} catch (err) {
|
|
592
|
+
if (['CANCELLATION_UNCONFIRMED', 'CANCELLATION_TEARDOWN_TIMEOUT'].includes(err?.code)) throw primaryFailure(err, err);
|
|
593
|
+
// F3/G3: preserve any billable usage the failed run reported (the local
|
|
594
|
+
// connector attaches it on a non-success result / usage-bearing rejection) so
|
|
595
|
+
// the consumer failure envelope can still debit the engine/GSD ledgers. G3:
|
|
596
|
+
// the timeout/abort throws happen here too — attach the usage to THOSE errors
|
|
597
|
+
// (not only the generic AgentError) so timeout/stuck attempts are billed.
|
|
598
|
+
// The local Claude SDK's price is provider-reported on failures too; label it
|
|
599
|
+
// here so the receipt funnel does not drop it as unlabelled raw usd.
|
|
600
|
+
const errUsage = (err && typeof err === 'object' && err.usage)
|
|
601
|
+
? (useLocalClaude && err.usage.usd_source === undefined ? { ...err.usage, usd_source: 'reported' } : err.usage)
|
|
602
|
+
: null;
|
|
603
|
+
if (timedOut) {
|
|
604
|
+
const e = new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
605
|
+
if (errUsage) e.usage = errUsage;
|
|
606
|
+
throw primaryFailure(err, e);
|
|
607
|
+
}
|
|
608
|
+
if (userInterruptAction) {
|
|
609
|
+
const e = new UserInterruptError(stepId, userInterruptAction);
|
|
610
|
+
if (errUsage) e.usage = errUsage;
|
|
611
|
+
throw primaryFailure(err, e);
|
|
612
|
+
}
|
|
613
|
+
if (abortReason) {
|
|
614
|
+
const e = new AgentAbortedError(stepId, abortReason);
|
|
615
|
+
if (errUsage) e.usage = errUsage;
|
|
616
|
+
throw primaryFailure(err, e);
|
|
617
|
+
}
|
|
618
|
+
const agentError = new AgentError(err?.message ?? 'Agent run failed');
|
|
619
|
+
if (errUsage) agentError.usage = errUsage;
|
|
620
|
+
throw primaryFailure(err, agentError);
|
|
461
621
|
}
|
|
462
|
-
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
// the
|
|
466
|
-
// the timeout
|
|
467
|
-
//
|
|
468
|
-
|
|
622
|
+
|
|
623
|
+
// H2: when the underlying run RESOLVES late (rather than rejecting) after a
|
|
624
|
+
// timeout/abort fired, its billable usage is on runResult.usage. Copy it onto
|
|
625
|
+
// the thrown error — the same single channel the rejection path (G3) uses — so
|
|
626
|
+
// the consumer timeout envelope / stuck-ledger accounting still bill the attempt.
|
|
627
|
+
// Throwing here means usageTotals is never returned, so this is the ONLY channel
|
|
628
|
+
// (no double count).
|
|
629
|
+
const lateUsage = (runResult && typeof runResult === 'object' && runResult.usage && typeof runResult.usage === 'object')
|
|
630
|
+
? runResult.usage
|
|
631
|
+
: null;
|
|
469
632
|
if (timedOut) {
|
|
470
633
|
const e = new AgentTimeoutError(stepId, Date.now() - startTime);
|
|
471
|
-
if (
|
|
472
|
-
throw e;
|
|
634
|
+
if (lateUsage) e.usage = lateUsage;
|
|
635
|
+
throw primaryFailure(runResult, e);
|
|
636
|
+
}
|
|
637
|
+
if (userInterruptAction) {
|
|
638
|
+
const e = new UserInterruptError(stepId, userInterruptAction);
|
|
639
|
+
if (lateUsage) e.usage = lateUsage;
|
|
640
|
+
throw primaryFailure(runResult, e);
|
|
473
641
|
}
|
|
474
|
-
if (userInterruptAction) throw new UserInterruptError(stepId, userInterruptAction);
|
|
475
642
|
if (abortReason) {
|
|
476
643
|
const e = new AgentAbortedError(stepId, abortReason);
|
|
477
|
-
if (
|
|
478
|
-
throw e;
|
|
644
|
+
if (lateUsage) e.usage = lateUsage;
|
|
645
|
+
throw primaryFailure(runResult, e);
|
|
479
646
|
}
|
|
480
|
-
const agentError = new AgentError(err?.message ?? 'Agent run failed');
|
|
481
|
-
if (errUsage) agentError.usage = errUsage;
|
|
482
|
-
throw agentError;
|
|
483
|
-
} finally {
|
|
484
|
-
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
485
|
-
if (onInterrupt && progress?.removeListener) progress.removeListener('interrupt', onInterrupt);
|
|
486
|
-
unsub();
|
|
487
|
-
}
|
|
488
647
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
const
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
648
|
+
// D2(b): the TS agent_run path returns a synchronous `complete` envelope with
|
|
649
|
+
// aggregate usage ({usd?, tokens, ms}) and streams NO step_usage progress
|
|
650
|
+
// events — without folding it in, budget accounting debits nothing on the TS
|
|
651
|
+
// route. The python / factory-shim path streams step_usage events (usageTotals
|
|
652
|
+
// already populated), so adopt runResult.usage only when the event stream
|
|
653
|
+
// contributed nothing (avoids double counting).
|
|
654
|
+
const runUsage = runResult && typeof runResult === 'object' ? runResult.usage : null;
|
|
655
|
+
const runSplit = runResult && typeof runResult === 'object' && runResult.split && typeof runResult.split === 'object'
|
|
656
|
+
? runResult.split
|
|
657
|
+
: null;
|
|
658
|
+
const usageFromEvents = usageTotals.input_tokens || usageTotals.output_tokens || usageTotals.cost_usd;
|
|
659
|
+
if (runUsage && typeof runUsage === 'object' && !usageFromEvents) {
|
|
660
|
+
// STRAT-USAGE-SPLIT: the TS envelope now carries the true input/output
|
|
661
|
+
// detail beside its Budget-shaped usage. Adopt it; only a split-less
|
|
662
|
+
// (pre-surface-16) envelope falls back to the aggregate, which is filed
|
|
663
|
+
// as output for continuity — mislabeled, but the legacy column it always
|
|
664
|
+
// occupied. That path retires with surface 15.
|
|
665
|
+
if (runSplit) {
|
|
666
|
+
usageTotals.input_tokens += runSplit.input ?? 0;
|
|
667
|
+
usageTotals.output_tokens += runSplit.output ?? 0;
|
|
668
|
+
if (typeof runSplit.cacheRead === 'number') {
|
|
669
|
+
usageTotals.cache_read_input_tokens = (usageTotals.cache_read_input_tokens ?? 0) + runSplit.cacheRead;
|
|
670
|
+
}
|
|
671
|
+
if (typeof runSplit.cacheCreation === 'number') {
|
|
672
|
+
usageTotals.cache_creation_input_tokens = (usageTotals.cache_creation_input_tokens ?? 0) + runSplit.cacheCreation;
|
|
673
|
+
}
|
|
674
|
+
} else if (typeof runUsage.tokens === 'number') {
|
|
675
|
+
usageTotals.output_tokens += runUsage.tokens;
|
|
676
|
+
}
|
|
677
|
+
if (typeof runUsage.usd === 'number') usageTotals.cost_usd += runUsage.usd;
|
|
678
|
+
if (typeof runUsage.ms === 'number') usageTotals.duration_ms = (usageTotals.duration_ms ?? 0) + runUsage.ms;
|
|
679
|
+
if (!usageTotals.model && runResult.telemetry?.model) usageTotals.model = runResult.telemetry.model;
|
|
680
|
+
}
|
|
509
681
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
682
|
+
const usages = [];
|
|
683
|
+
const primaryFromEvents = usageFromEvents || primaryUsdSource !== null;
|
|
684
|
+
if (primaryFromEvents) {
|
|
685
|
+
const primary = {
|
|
686
|
+
dispatch_id: primaryDispatchId ?? randomUUID(),
|
|
687
|
+
model: usageTotals.model ?? runResult?.telemetry?.model ?? cfg.modelID ?? 'unknown',
|
|
688
|
+
...(runResult?.telemetry?.effort ?? cfg.effort
|
|
689
|
+
? { effort: runResult?.telemetry?.effort ?? cfg.effort }
|
|
690
|
+
: {}),
|
|
691
|
+
duration_ms: runResult?.telemetry?.durationMs ?? usageTotals.duration_ms ?? (Date.now() - startTime),
|
|
692
|
+
input_tokens: usageTotals.input_tokens,
|
|
693
|
+
output_tokens: usageTotals.output_tokens,
|
|
694
|
+
...(usageTotals.cache_read_input_tokens
|
|
695
|
+
? { cache_read: usageTotals.cache_read_input_tokens }
|
|
696
|
+
: {}),
|
|
697
|
+
...(usageTotals.cache_creation_input_tokens
|
|
698
|
+
? { cache_creation: usageTotals.cache_creation_input_tokens }
|
|
699
|
+
: {}),
|
|
700
|
+
...(usageTotals.cost_usd > 0
|
|
701
|
+
? { cost_usd: usageTotals.cost_usd, usd_source: primaryUsdSource ?? 'estimated' }
|
|
702
|
+
: { usd_source: primaryUsdSource ?? 'estimated' }),
|
|
703
|
+
};
|
|
704
|
+
usages.push(primary);
|
|
705
|
+
} else {
|
|
706
|
+
const primary = usageRecordFromRaw(runUsage, runResult?.telemetry, primaryDispatchId, {
|
|
707
|
+
model: cfg.modelID,
|
|
708
|
+
effort: cfg.effort,
|
|
709
|
+
duration_ms: Date.now() - startTime,
|
|
710
|
+
usd_source: runResult?.usdSource ?? (useLocalClaude ? 'reported' : undefined),
|
|
711
|
+
}, runSplit);
|
|
712
|
+
if (primary) usages.push(primary);
|
|
713
|
+
}
|
|
524
714
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
715
|
+
const text = (runResult && typeof runResult.text === 'string' && runResult.text.length > 0)
|
|
716
|
+
? runResult.text
|
|
717
|
+
: textParts.join('');
|
|
528
718
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
719
|
+
if (progress) {
|
|
720
|
+
progress.debug(`normalizer: textParts=${textParts.length}, text length=${text.length}`);
|
|
721
|
+
if (text.length > 0) progress.debug(`text preview: ${text.slice(0, 300)}`);
|
|
722
|
+
} else if (process.env.COMPOSE_DEBUG) {
|
|
723
|
+
process.stderr.write(` [normalizer] textParts=${textParts.length}, text length=${text.length}\n`);
|
|
724
|
+
}
|
|
535
725
|
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
726
|
+
// review_mode hook — MUST be before the !hasSchema early return (MF-3 in blueprint).
|
|
727
|
+
// Parallel lens steps often have empty output_fields (hasSchema=false), but review
|
|
728
|
+
// normalization must still run. The Stratum server validates the post-normalize result
|
|
729
|
+
// via `ensure` expressions after stratum_step_done — not against raw text.
|
|
730
|
+
if (opts.reviewMode === true) {
|
|
731
|
+
const reviewAgentType = agentType; // already resolved from stepDispatch.agent at line 178
|
|
732
|
+
const reviewModelId = usageTotals.model ?? cfg.modelID ?? null;
|
|
733
|
+
// The repair dispatch is only CREDITED (dispatchIds.repair) when its output
|
|
734
|
+
// actually replaced the primary parse — a failed or unparseable repair still
|
|
735
|
+
// bills its usage but must not absorb the step's settlement.
|
|
736
|
+
let repairUsed = false;
|
|
737
|
+
let repairFailure = null;
|
|
738
|
+
let repairResultForControl = null;
|
|
739
|
+
const foldRepairUsage = (usage) => {
|
|
740
|
+
if (!usage || typeof usage !== 'object') return;
|
|
741
|
+
if (typeof usage.tokens === 'number') usageTotals.output_tokens += usage.tokens;
|
|
742
|
+
if (typeof usage.usd === 'number') usageTotals.cost_usd += usage.usd;
|
|
743
|
+
};
|
|
744
|
+
const repairFn = stratum
|
|
745
|
+
? async (repairPrompt) => {
|
|
746
|
+
try {
|
|
747
|
+
const repairResult = await stratum.agentRun(reviewAgentType, repairPrompt, {
|
|
748
|
+
...executionOptions,
|
|
749
|
+
signal: abortController.signal,
|
|
750
|
+
telemetry: { ...primaryTelemetry, site: 'review-repair' },
|
|
751
|
+
});
|
|
752
|
+
repairResultForControl = repairResult;
|
|
753
|
+
repairDispatchId = typeof repairResult?.dispatchId === 'string'
|
|
754
|
+
? repairResult.dispatchId
|
|
755
|
+
: null;
|
|
756
|
+
foldRepairUsage(repairResult?.usage);
|
|
757
|
+
const record = usageRecordFromRaw(
|
|
758
|
+
repairResult?.usage,
|
|
759
|
+
repairResult?.telemetry,
|
|
760
|
+
repairDispatchId,
|
|
761
|
+
{ model: cfg.modelID, effort: cfg.effort, usd_source: repairResult?.usdSource },
|
|
762
|
+
repairResult?.split && typeof repairResult.split === 'object' ? repairResult.split : null,
|
|
763
|
+
);
|
|
764
|
+
if (record) usages.push(record);
|
|
765
|
+
return repairResult?.text ?? '';
|
|
766
|
+
} catch (error) {
|
|
767
|
+
repairFailure = error;
|
|
768
|
+
repairDispatchId = typeof error?.dispatchId === 'string'
|
|
769
|
+
? error.dispatchId
|
|
770
|
+
: null;
|
|
771
|
+
foldRepairUsage(error?.usage);
|
|
772
|
+
const record = usageRecordFromRaw(
|
|
773
|
+
error?.usage,
|
|
774
|
+
error?.telemetry,
|
|
775
|
+
repairDispatchId,
|
|
776
|
+
{ model: cfg.modelID, effort: cfg.effort, usd_source: error?.usdSource },
|
|
777
|
+
error?.split,
|
|
778
|
+
);
|
|
779
|
+
if (record) usages.push(record);
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
: undefined;
|
|
784
|
+
const reviewResult = await normalizeReviewResult(text, {
|
|
785
|
+
agentType: reviewAgentType,
|
|
786
|
+
modelId: reviewModelId,
|
|
787
|
+
confidenceGate: opts.confidenceGate ?? 7,
|
|
788
|
+
lens: opts.lens ?? 'general',
|
|
789
|
+
repairFn,
|
|
790
|
+
onRepairUsed: () => { repairUsed = true; },
|
|
791
|
+
});
|
|
792
|
+
// The tolerant text parser can recover ordinary repair failures, but it
|
|
793
|
+
// must never turn a cancelled or still-running repair into a clean review.
|
|
794
|
+
if (['CANCELLATION_UNCONFIRMED', 'CANCELLATION_TEARDOWN_TIMEOUT'].includes(repairFailure?.code)) {
|
|
795
|
+
repairFailure.usages = usages;
|
|
796
|
+
throw repairFailure;
|
|
797
|
+
}
|
|
798
|
+
const controlSource = repairFailure ?? repairResultForControl;
|
|
799
|
+
const controlError = timedOut
|
|
800
|
+
? new AgentTimeoutError(stepId, Date.now() - startTime)
|
|
801
|
+
: userInterruptAction
|
|
802
|
+
? new UserInterruptError(stepId, userInterruptAction)
|
|
803
|
+
: abortReason ? new AgentAbortedError(stepId, abortReason) : null;
|
|
804
|
+
if (controlError) {
|
|
805
|
+
if (controlSource?.usage) controlError.usage = controlSource.usage;
|
|
806
|
+
controlError.usages = usages;
|
|
807
|
+
throw copyDispatchId(controlSource, controlError);
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
text,
|
|
811
|
+
result: reviewResult,
|
|
812
|
+
usage: usageTotals,
|
|
813
|
+
usages,
|
|
814
|
+
dispatchIds: { primary: primaryDispatchId, repair: repairUsed ? repairDispatchId : null },
|
|
815
|
+
};
|
|
816
|
+
}
|
|
561
817
|
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
818
|
+
if (!hasStructuredOutput) {
|
|
819
|
+
return {
|
|
820
|
+
text,
|
|
821
|
+
result: null,
|
|
822
|
+
usage: usageTotals,
|
|
823
|
+
usages,
|
|
824
|
+
dispatchIds: { primary: primaryDispatchId, repair: repairDispatchId },
|
|
825
|
+
};
|
|
826
|
+
}
|
|
565
827
|
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
828
|
+
const result = extractJson(text);
|
|
829
|
+
if (result === null) {
|
|
830
|
+
if (progress) {
|
|
831
|
+
progress.warn('Could not extract JSON from agent output, using fallback');
|
|
832
|
+
} else {
|
|
833
|
+
process.stderr.write(' ⚠ Could not extract JSON from agent output, using fallback\n');
|
|
834
|
+
}
|
|
835
|
+
const summary = text.slice(0, 200).replace(/\n/g, ' ').trim();
|
|
836
|
+
const normalizationFailure = summary || 'Could not extract structured output';
|
|
837
|
+
return {
|
|
838
|
+
text,
|
|
839
|
+
result: { summary: normalizationFailure },
|
|
840
|
+
usage: usageTotals,
|
|
841
|
+
usages,
|
|
842
|
+
normalizationFailure,
|
|
843
|
+
dispatchIds: { primary: primaryDispatchId, repair: repairDispatchId },
|
|
844
|
+
};
|
|
572
845
|
}
|
|
573
|
-
|
|
574
|
-
const normalizationFailure = summary || 'Could not extract structured output';
|
|
846
|
+
|
|
575
847
|
return {
|
|
576
848
|
text,
|
|
577
|
-
result
|
|
849
|
+
result,
|
|
578
850
|
usage: usageTotals,
|
|
579
|
-
|
|
851
|
+
usages,
|
|
852
|
+
dispatchIds: { primary: primaryDispatchId, repair: repairDispatchId },
|
|
580
853
|
};
|
|
854
|
+
} finally {
|
|
855
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
856
|
+
if (onInterrupt && progress?.removeListener) progress.removeListener('interrupt', onInterrupt);
|
|
857
|
+
unsub();
|
|
581
858
|
}
|
|
582
|
-
|
|
583
|
-
return { text, result, usage: usageTotals };
|
|
584
859
|
}
|