onto-mcp 0.4.11 → 0.4.13
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/.onto/authority/core-lexicon.yaml +1 -1
- package/.onto/authority/supported-models.yaml +153 -0
- package/.onto/domains/accounting-kr/competency_qs.md +153 -153
- package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
- package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
- package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
- package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +202 -49
- package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
- package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
- package/.onto/processes/review/nesting-batch-worker-contract.md +1 -1
- package/.onto/processes/review/productized-live-path.md +9 -5
- package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
- package/.onto/processes/review/review-target-profile-contract.md +47 -4
- package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
- package/.onto/processes/shared/target-material-kind-contract.md +1 -1
- package/README.md +64 -1
- package/dist/cli.js +11 -1
- package/dist/core-api/reconstruct-api.js +679 -18
- package/dist/core-api/review-api.js +70 -5
- package/dist/core-api/review-progress.js +7 -0
- package/dist/core-api/runtime-observation.js +7 -0
- package/dist/core-runtime/artifact-io.js +131 -0
- package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
- package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
- package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
- package/dist/core-runtime/cli/prepare-review-session.js +34 -1
- package/dist/core-runtime/cli/render-review-final-output.js +2 -1
- package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
- package/dist/core-runtime/cli/run-seats.js +18 -0
- package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
- package/dist/core-runtime/cli/unit-resubmit.js +269 -0
- package/dist/core-runtime/cli/worker-structured-output.js +80 -2
- package/dist/core-runtime/discovery/host-detection.js +71 -22
- package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
- package/dist/core-runtime/discovery/review-cert-record.js +442 -0
- package/dist/core-runtime/discovery/review-execution-units.js +22 -0
- package/dist/core-runtime/discovery/seat-inventory.js +167 -0
- package/dist/core-runtime/discovery/settings-chain.js +311 -52
- package/dist/core-runtime/discovery/supported-models.js +289 -25
- package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
- package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
- package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
- package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
- package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
- package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
- package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
- package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
- package/dist/core-runtime/effort-calibration-ingest.js +326 -0
- package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
- package/dist/core-runtime/effort-calibration-review.js +22 -0
- package/dist/core-runtime/effort-calibration-sweep.js +110 -0
- package/dist/core-runtime/effort-frontier.js +134 -0
- package/dist/core-runtime/llm/claude-bin.js +77 -0
- package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
- package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
- package/dist/core-runtime/llm/llm-caller.js +316 -35
- package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
- package/dist/core-runtime/llm/model-switcher.js +1 -1
- package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
- package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
- package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
- package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
- package/dist/core-runtime/onboard/cli-host.js +5 -2
- package/dist/core-runtime/onboard/configure-provider.js +33 -1
- package/dist/core-runtime/onboard/host-target.js +28 -3
- package/dist/core-runtime/onboard/register.js +54 -12
- package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
- package/dist/core-runtime/reconstruct/artifact-types.js +37 -0
- package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
- package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
- package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
- package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
- package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
- package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
- package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
- package/dist/core-runtime/reconstruct/directive-validation.js +4 -3
- package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
- package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -9
- package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
- package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
- package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
- package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
- package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
- package/dist/core-runtime/reconstruct/material-admission-validation.js +20 -5
- package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
- package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
- package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
- package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
- package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
- package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
- package/dist/core-runtime/reconstruct/output-budget.js +6 -0
- package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
- package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
- package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
- package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
- package/dist/core-runtime/reconstruct/record.js +49 -3
- package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
- package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
- package/dist/core-runtime/reconstruct/run.js +5409 -517
- package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
- package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
- package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
- package/dist/core-runtime/reconstruct/source-observations.js +166 -0
- package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
- package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
- package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
- package/dist/core-runtime/review/continuation-plan.js +12 -5
- package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
- package/dist/core-runtime/review/materializers.js +100 -11
- package/dist/core-runtime/review/nesting-batch.js +6 -2
- package/dist/core-runtime/review/obligation-shardability.js +138 -0
- package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
- package/dist/core-runtime/review/review-artifact-utils.js +219 -7
- package/dist/core-runtime/review/review-execution-steps.js +32 -8
- package/dist/core-runtime/review/review-progress-contract.js +8 -0
- package/dist/core-runtime/review/review-prompt-budget.js +97 -0
- package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
- package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
- package/dist/core-runtime/route-identity.js +182 -0
- package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
- package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
- package/dist/core-runtime/target-material-kind.js +58 -0
- package/dist/mcp/server.js +27 -5
- package/dist/mcp/tool-schemas.js +8 -0
- package/dist/tui/app.js +167 -0
- package/dist/tui/data/event-follower.js +116 -0
- package/dist/tui/data/node-detail.js +42 -0
- package/dist/tui/data/projection-poll.js +26 -0
- package/dist/tui/data/session-discovery.js +65 -0
- package/dist/tui/index.js +108 -0
- package/dist/tui/screens/log.js +25 -0
- package/dist/tui/screens/node-detail.js +17 -0
- package/dist/tui/screens/session-selector.js +26 -0
- package/dist/tui/screens/workflow-tree.js +90 -0
- package/dist/tui/view-model/reconstruct-adapter.js +162 -0
- package/dist/tui/view-model/review-adapter.js +270 -0
- package/dist/tui/view-model/tree-view-model.js +19 -0
- package/package.json +20 -3
- package/settings.example.json +77 -16
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dispatch limit/transport circuit breaker — pure policy logic (no I/O, no
|
|
3
|
+
* timers of its own, no LLM calls).
|
|
4
|
+
*
|
|
5
|
+
* 설계 B (development-records/design/20260704-review-unit-resubmit-and-limit-breaker-design.md §4):
|
|
6
|
+
* an unattended batch must not throw a retry storm at a dead rate limit and
|
|
7
|
+
* lose items. The repo has no common dispatch surface (§8 재앵커링), so the
|
|
8
|
+
* policy is injected per loop. Currently wired: the reconstruct semantic-map
|
|
9
|
+
* judgment loop and the review lens/stance fan-out pools — both the flat
|
|
10
|
+
* per-unit loops AND the nested-workers first-attempt batch (§4-1: a batch-window
|
|
11
|
+
* SUCCESS is recorded skipped so a stale batch success never resets the streak,
|
|
12
|
+
* while a batch-window FAILURE is classified like any failure; the
|
|
13
|
+
* directly-observed flat retries drive the streak).
|
|
14
|
+
*
|
|
15
|
+
* 1. backoff first — a per-item failure counts toward the breaker only after
|
|
16
|
+
* the item's bounded backoff retries are exhausted. Providers surface 429s
|
|
17
|
+
* as opaque message strings (no Retry-After anywhere), so the schedule is
|
|
18
|
+
* a capped exponential.
|
|
19
|
+
* 2. systemic detection — only consecutive FINAL item failures of a systemic
|
|
20
|
+
* class (rate_limit / auth / transport) across DISTINCT items count;
|
|
21
|
+
* threshold N via settings. A success resets the streak.
|
|
22
|
+
* 3. poison item — an item that keeps failing while the streak stays below N
|
|
23
|
+
* is dead-lettered (complete-with-failure) and the batch continues.
|
|
24
|
+
* 4. breaker trip — the loop halts the batch and persists the incomplete-item
|
|
25
|
+
* list (fallback provider swap is a deferred later cut).
|
|
26
|
+
* 5. recovery re-dispatch targets exactly the persisted incomplete set. For
|
|
27
|
+
* reconstruct semantic-map, stage-local resume validates the same-batch
|
|
28
|
+
* partition plus prior census/sidecar truth before reusing retained rows
|
|
29
|
+
* and re-dispatching incomplete rows.
|
|
30
|
+
*
|
|
31
|
+
* File I/O (persisting the artifact), timestamps, and halt mechanics stay in
|
|
32
|
+
* the wiring; this module owns classification, the backoff schedule, the
|
|
33
|
+
* breaker state machine, and the artifact projection.
|
|
34
|
+
*/
|
|
35
|
+
import path from "node:path";
|
|
36
|
+
import { readStructuredDispatchFailureClass, readStructuredDispatchFailureEvidence, } from "./structured-dispatch-error.js";
|
|
37
|
+
const RATE_LIMIT_PATTERNS = [
|
|
38
|
+
"429",
|
|
39
|
+
"rate limit",
|
|
40
|
+
"limit reached",
|
|
41
|
+
"rate_limit",
|
|
42
|
+
"too many requests",
|
|
43
|
+
"overloaded",
|
|
44
|
+
"session limit",
|
|
45
|
+
"usage limit",
|
|
46
|
+
"quota",
|
|
47
|
+
"retry-after",
|
|
48
|
+
"retry_after",
|
|
49
|
+
];
|
|
50
|
+
const AUTH_PATTERNS = [
|
|
51
|
+
"401",
|
|
52
|
+
"403",
|
|
53
|
+
"unauthorized",
|
|
54
|
+
"forbidden",
|
|
55
|
+
"invalid api key",
|
|
56
|
+
"invalid x-api-key",
|
|
57
|
+
"authentication",
|
|
58
|
+
"not logged in",
|
|
59
|
+
];
|
|
60
|
+
/** Single source for the transient-transport message patterns shared with the
|
|
61
|
+
* review runner's per-unit retry decision (isTransientExecutorFailureMessage)
|
|
62
|
+
* — the two consumers differ only in their extras. */
|
|
63
|
+
export const TRANSIENT_TRANSPORT_MESSAGE_PATTERNS = [
|
|
64
|
+
"stream disconnected before completion",
|
|
65
|
+
"connection reset by peer",
|
|
66
|
+
"error sending request",
|
|
67
|
+
"failed to connect to websocket",
|
|
68
|
+
"transport channel closed",
|
|
69
|
+
"http/request failed",
|
|
70
|
+
"request failed after",
|
|
71
|
+
];
|
|
72
|
+
const TRANSPORT_PATTERNS = [
|
|
73
|
+
...TRANSIENT_TRANSPORT_MESSAGE_PATTERNS,
|
|
74
|
+
"timed out",
|
|
75
|
+
"timeout",
|
|
76
|
+
"econnrefused",
|
|
77
|
+
"econnreset",
|
|
78
|
+
"etimedout",
|
|
79
|
+
"socket hang up",
|
|
80
|
+
"fetch failed",
|
|
81
|
+
];
|
|
82
|
+
/**
|
|
83
|
+
* Classify a failure message into a systemic dispatch class, or null for
|
|
84
|
+
* item-local failures (malformed output, validation rejection, …) that must
|
|
85
|
+
* never trip the batch breaker. Message-based by necessity: both adapter
|
|
86
|
+
* families flatten provider status into strings (재앵커링 노트 (4)).
|
|
87
|
+
*/
|
|
88
|
+
export function classifySystemicDispatchFailure(message) {
|
|
89
|
+
if (typeof message !== "string" || message.length === 0)
|
|
90
|
+
return null;
|
|
91
|
+
const normalized = message.toLowerCase();
|
|
92
|
+
if (RATE_LIMIT_PATTERNS.some((pattern) => normalized.includes(pattern))) {
|
|
93
|
+
return "rate_limit";
|
|
94
|
+
}
|
|
95
|
+
if (AUTH_PATTERNS.some((pattern) => normalized.includes(pattern))) {
|
|
96
|
+
return "auth";
|
|
97
|
+
}
|
|
98
|
+
if (TRANSPORT_PATTERNS.some((pattern) => normalized.includes(pattern))) {
|
|
99
|
+
return "transport";
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Classify a dispatch ERROR: structured provider status first (the SDK
|
|
105
|
+
* adapters rethrow the original error with `.status` preserved), message
|
|
106
|
+
* substrings as the fallback for the CLI/worker adapters that flatten
|
|
107
|
+
* everything into text.
|
|
108
|
+
*/
|
|
109
|
+
export function classifyDispatchError(error) {
|
|
110
|
+
const structured = readStructuredDispatchFailureEvidence(error);
|
|
111
|
+
if (structured)
|
|
112
|
+
return structured.failure_class;
|
|
113
|
+
const status = error?.status;
|
|
114
|
+
if (typeof status === "number") {
|
|
115
|
+
if (status === 429)
|
|
116
|
+
return "rate_limit";
|
|
117
|
+
if (status === 401 || status === 403)
|
|
118
|
+
return "auth";
|
|
119
|
+
if (status >= 500)
|
|
120
|
+
return "transport";
|
|
121
|
+
}
|
|
122
|
+
return classifySystemicDispatchFailure(error instanceof Error ? error.message : String(error));
|
|
123
|
+
}
|
|
124
|
+
/** Marker stamped by {@link runWithDispatchBackoff} on errors that came from
|
|
125
|
+
* an actual provider dispatch. Item-level attribution MUST read the marker
|
|
126
|
+
* (readDispatchFailureClass) instead of re-classifying arbitrary error text —
|
|
127
|
+
* deterministic stage errors embed content-derived text (sheet names, row
|
|
128
|
+
* ranges) that substring patterns would misread as systemic. */
|
|
129
|
+
const DISPATCH_FAILURE_CLASS = Symbol.for("onto.dispatch_failure_class");
|
|
130
|
+
function markDispatchFailureClass(error, failureClass) {
|
|
131
|
+
if (error !== null && typeof error === "object") {
|
|
132
|
+
error[DISPATCH_FAILURE_CLASS] = failureClass;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/** Systemic class of a dispatch-marked error; null for unmarked errors
|
|
136
|
+
* (deterministic/stage-local throws) and marked-but-item-local failures. */
|
|
137
|
+
export function readDispatchFailureClass(error) {
|
|
138
|
+
const structured = readStructuredDispatchFailureEvidence(error);
|
|
139
|
+
if (structured)
|
|
140
|
+
return readStructuredDispatchFailureClass(error);
|
|
141
|
+
if (error === null || typeof error !== "object")
|
|
142
|
+
return null;
|
|
143
|
+
const failureClass = error[DISPATCH_FAILURE_CLASS];
|
|
144
|
+
return failureClass === "rate_limit" || failureClass === "auth" || failureClass === "transport"
|
|
145
|
+
? failureClass
|
|
146
|
+
: null;
|
|
147
|
+
}
|
|
148
|
+
/** Capped exponential backoff (no jitter — deterministic for replay/tests).
|
|
149
|
+
* attempt is 0-based: delay before retry #1 is initialMs. */
|
|
150
|
+
export function dispatchBackoffDelayMs(args) {
|
|
151
|
+
const exponential = args.initialMs * 2 ** Math.max(0, args.attempt);
|
|
152
|
+
const bounded = Math.min(args.capMs, exponential);
|
|
153
|
+
return Number.isFinite(bounded) && bounded > 0 ? Math.floor(bounded) : args.capMs;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Breaker state machine over one batch. The loop reports each item's FINAL
|
|
157
|
+
* outcome (after its bounded retries); the machine answers "has this become
|
|
158
|
+
* systemic?" and keeps the dead-letter/completion bookkeeping the wiring
|
|
159
|
+
* persists.
|
|
160
|
+
*
|
|
161
|
+
* Poison-vs-systemic attribution rule: a systemic-class failure is held
|
|
162
|
+
* PENDING until the batch proves the provider lane is alive (a later item
|
|
163
|
+
* succeeds) — only then is it a poison item (reproduced on that item alone)
|
|
164
|
+
* and dead-lettered. If the streak instead reaches the threshold, the batch
|
|
165
|
+
* trips and the pending items stay in the INCOMPLETE set: they were victims
|
|
166
|
+
* of the outage and must be re-dispatched on recovery (규칙 5), not
|
|
167
|
+
* complete-with-failure. Item-local failures (null class) dead-letter
|
|
168
|
+
* immediately and say nothing about the provider, so they neither extend nor
|
|
169
|
+
* reset the systemic streak.
|
|
170
|
+
*/
|
|
171
|
+
export class DispatchBreakerState {
|
|
172
|
+
policy;
|
|
173
|
+
pendingSystemic = [];
|
|
174
|
+
trip = null;
|
|
175
|
+
completed = [];
|
|
176
|
+
deadLetter = [];
|
|
177
|
+
constructor(policy) {
|
|
178
|
+
this.policy = policy;
|
|
179
|
+
}
|
|
180
|
+
/** Report a REAL dispatch success — the only event that proves the
|
|
181
|
+
* provider lane is alive and may reclassify pending systemic failures as
|
|
182
|
+
* poison. Items that made no successful provider call must use
|
|
183
|
+
* {@link recordItemSkipped} instead. */
|
|
184
|
+
recordItemSuccess(itemId) {
|
|
185
|
+
this.completed.push(itemId);
|
|
186
|
+
// Attribution freezes at trip: a CONCURRENT pool (review lens/stance)
|
|
187
|
+
// can deliver an in-flight success after the trip decision, and letting
|
|
188
|
+
// it reclassify the pending outage victims as poison would dead-letter
|
|
189
|
+
// them OUT of the incomplete recovery set (규칙 5 위반). The late unit
|
|
190
|
+
// itself still counts as completed.
|
|
191
|
+
if (this.trip !== null)
|
|
192
|
+
return;
|
|
193
|
+
// Concurrent pool (opt-in, F1): during a concurrent burst a success does
|
|
194
|
+
// NOT prove the whole lane is alive (a partial rate-limit yields some 200s
|
|
195
|
+
// and some 429s), and letting completion order decide which pending victims
|
|
196
|
+
// become poison makes the trip/classification non-deterministic. In this
|
|
197
|
+
// mode systemic victims stay pending — the trip is count-based and
|
|
198
|
+
// order-independent; un-tripped victims end as incomplete (review recovery
|
|
199
|
+
// re-derives from the frontier either way). Sequential callers omit the flag
|
|
200
|
+
// and keep the poison-via-later-success attribution.
|
|
201
|
+
if (this.policy.concurrent)
|
|
202
|
+
return;
|
|
203
|
+
// The provider lane is alive: pending systemic failures were item-scoped
|
|
204
|
+
// after all — poison, dead-lettered.
|
|
205
|
+
for (const entry of this.pendingSystemic)
|
|
206
|
+
this.deadLetter.push(entry);
|
|
207
|
+
this.pendingSystemic = [];
|
|
208
|
+
}
|
|
209
|
+
/** Report an item that owes no dispatch (structural skip, budget cap, all
|
|
210
|
+
* subsumed): completed for recovery-set purposes, but it proves NOTHING
|
|
211
|
+
* about the provider lane — the systemic streak and pending attribution
|
|
212
|
+
* are untouched. Conflating this with success let one interleaved skip
|
|
213
|
+
* reset an outage streak and write its victims off as poison. */
|
|
214
|
+
recordItemSkipped(itemId) {
|
|
215
|
+
this.completed.push(itemId);
|
|
216
|
+
}
|
|
217
|
+
/** Report an item's FINAL failure (per-item budget exhausted). Returns the
|
|
218
|
+
* trip state when this failure crosses the systemic threshold. */
|
|
219
|
+
recordItemFailure(entry) {
|
|
220
|
+
if (entry.failure_class === null) {
|
|
221
|
+
// Item-local failure class: dead-letter, never breaker fuel.
|
|
222
|
+
this.deadLetter.push(entry);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
if (!this.pendingSystemic.some((pending) => pending.item_id === entry.item_id)) {
|
|
226
|
+
// Post-trip in-flight systemic failures still join the pending set —
|
|
227
|
+
// they are outage victims and belong to the incomplete recovery set.
|
|
228
|
+
this.pendingSystemic.push(entry);
|
|
229
|
+
}
|
|
230
|
+
if (this.policy.enabled &&
|
|
231
|
+
this.trip === null &&
|
|
232
|
+
this.pendingSystemic.length >= this.policy.systemic_threshold) {
|
|
233
|
+
// The FIRST crossing is the trip authority; later records must not
|
|
234
|
+
// rewrite its count. Concurrent-mode guarantee (F1): the trip DECISION
|
|
235
|
+
// (bool), `consecutive_item_count`, and the completed/dead-letter/
|
|
236
|
+
// incomplete SETS are order-independent (pendingSystemic is never
|
|
237
|
+
// flushed). `failure_class` is NOT: the trip fires early on the
|
|
238
|
+
// first-N-to-complete prefix, so a mixed-class burst labels the trip by
|
|
239
|
+
// whichever systemic class happened to cross. It is a best-effort
|
|
240
|
+
// diagnostic label (halt_reason / disclosure), never recovery-relevant,
|
|
241
|
+
// and is left as the crossing item's class — a deterministic full-batch
|
|
242
|
+
// class summary, if ever needed, belongs at end-of-batch over the full
|
|
243
|
+
// pending set, not at this early trip point (F1 후속).
|
|
244
|
+
this.trip = {
|
|
245
|
+
failure_class: entry.failure_class,
|
|
246
|
+
consecutive_item_count: this.pendingSystemic.length,
|
|
247
|
+
threshold: this.policy.systemic_threshold,
|
|
248
|
+
};
|
|
249
|
+
return this.trip;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
tripped() {
|
|
254
|
+
return this.trip;
|
|
255
|
+
}
|
|
256
|
+
completedItemIds() {
|
|
257
|
+
return this.completed;
|
|
258
|
+
}
|
|
259
|
+
deadLetterEntries() {
|
|
260
|
+
return this.deadLetter;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
export class DispatchBreakerTrippedError extends Error {
|
|
264
|
+
trip;
|
|
265
|
+
constructor(trip, incompleteArtifactPath, options) {
|
|
266
|
+
super(`dispatch breaker tripped: ${trip.failure_class} failed ${trip.consecutive_item_count} consecutive items (threshold ${trip.threshold}) — batch halted, incomplete items persisted for exact re-dispatch${incompleteArtifactPath ? ` (${incompleteArtifactPath})` : ""}${options?.fallbackOutcomePath ? `; fallback outcome: ${options.fallbackOutcomePath}` : ""}`);
|
|
267
|
+
this.name = "DispatchBreakerTrippedError";
|
|
268
|
+
this.trip = trip;
|
|
269
|
+
if (options?.structuredContributors) {
|
|
270
|
+
this.structuredContributors = options.structuredContributors.map((entry) => structuredClone(entry));
|
|
271
|
+
}
|
|
272
|
+
if (options?.fallbackOutcomePath) {
|
|
273
|
+
this.fallbackOutcomePath = options.fallbackOutcomePath;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
/**
|
|
278
|
+
* Run one dispatch under the backoff-first rule (규칙 1): systemic-class
|
|
279
|
+
* errors are retried with capped exponential backoff up to the per-item
|
|
280
|
+
* attempt cap; item-local errors (null class) never retry here — the item's
|
|
281
|
+
* own semantics own those. On exhaustion the LAST error is rethrown so the
|
|
282
|
+
* caller can classify it and report the item's FINAL outcome to
|
|
283
|
+
* {@link DispatchBreakerState}. `sleep` is injected so fixtures stay
|
|
284
|
+
* deterministic and fast.
|
|
285
|
+
*/
|
|
286
|
+
export async function runWithDispatchBackoff(args) {
|
|
287
|
+
const maxAttempts = Math.max(1, args.policy.per_call_max_attempts);
|
|
288
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
289
|
+
try {
|
|
290
|
+
return await args.dispatch();
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
294
|
+
const failureClass = classifyDispatchError(error);
|
|
295
|
+
markDispatchFailureClass(error, failureClass);
|
|
296
|
+
const retryable = failureClass !== null && attempt + 1 < maxAttempts;
|
|
297
|
+
if (!retryable)
|
|
298
|
+
throw error;
|
|
299
|
+
const delayMs = dispatchBackoffDelayMs({
|
|
300
|
+
attempt,
|
|
301
|
+
initialMs: args.policy.backoff_initial_ms,
|
|
302
|
+
capMs: args.policy.backoff_cap_ms,
|
|
303
|
+
});
|
|
304
|
+
await args.onRetry?.({
|
|
305
|
+
label: args.label,
|
|
306
|
+
attempt: attempt + 1,
|
|
307
|
+
delayMs,
|
|
308
|
+
failureClass,
|
|
309
|
+
message,
|
|
310
|
+
});
|
|
311
|
+
await args.sleep(delayMs);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// Unreachable: the loop always returns/throws on the final attempt.
|
|
315
|
+
throw new Error(`runWithDispatchBackoff fell through for ${args.label}`);
|
|
316
|
+
}
|
|
317
|
+
/** Session-root location of the dead-letter/incomplete artifact — part of the
|
|
318
|
+
* F-B3 recovery contract (재디스패치 집합의 진실 위치), shared by every wired
|
|
319
|
+
* pipeline (reconstruct semantic-map, review lens/stance). One batch trips at
|
|
320
|
+
* most once per run (trip is terminal), so a fixed per-session path holds the
|
|
321
|
+
* latest batch's end state; the `pipeline`/`batch_label` fields identify it. */
|
|
322
|
+
export function dispatchIncompleteArtifactPath(sessionRoot) {
|
|
323
|
+
return path.join(sessionRoot, "dispatch-incomplete.yaml");
|
|
324
|
+
}
|
|
325
|
+
export function isDispatchIncompleteArtifact(value) {
|
|
326
|
+
const candidate = value;
|
|
327
|
+
return Boolean(candidate &&
|
|
328
|
+
typeof candidate === "object" &&
|
|
329
|
+
candidate.schema_version === "1" &&
|
|
330
|
+
typeof candidate.pipeline === "string" &&
|
|
331
|
+
typeof candidate.batch_label === "string" &&
|
|
332
|
+
candidate.breaker &&
|
|
333
|
+
typeof candidate.breaker === "object" &&
|
|
334
|
+
typeof candidate.breaker.tripped === "boolean" &&
|
|
335
|
+
typeof candidate.breaker.threshold === "number" &&
|
|
336
|
+
Array.isArray(candidate.completed_item_ids) &&
|
|
337
|
+
candidate.completed_item_ids.every((id) => typeof id === "string") &&
|
|
338
|
+
Array.isArray(candidate.dead_letter) &&
|
|
339
|
+
candidate.dead_letter.every((entry) => entry && typeof entry === "object" && typeof entry.item_id === "string") &&
|
|
340
|
+
Array.isArray(candidate.incomplete_item_ids) &&
|
|
341
|
+
candidate.incomplete_item_ids.every((id) => typeof id === "string"));
|
|
342
|
+
}
|
|
343
|
+
function assertUniqueItemIds(itemIds, label) {
|
|
344
|
+
const seen = new Set();
|
|
345
|
+
for (const itemId of itemIds) {
|
|
346
|
+
if (seen.has(itemId)) {
|
|
347
|
+
throw new Error(`dispatch incomplete artifact: duplicate ${label} item_id '${itemId}'`);
|
|
348
|
+
}
|
|
349
|
+
seen.add(itemId);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
/** Deterministic projection of a batch's end state — the wiring persists it
|
|
353
|
+
* on breaker trip AND on normal completion (rule 6 observability), so a
|
|
354
|
+
* recovery run always has the exact re-dispatch set. This overload accepts a
|
|
355
|
+
* fully-expanded partition so recovery runs can persist retained prior work +
|
|
356
|
+
* current retry work without pretending the live breaker state observed every
|
|
357
|
+
* retained item. `createdAt` is supplied by the caller (runtime owns timestamps). */
|
|
358
|
+
export function buildDispatchIncompleteArtifactFromPartition(args) {
|
|
359
|
+
assertUniqueItemIds(args.plannedItemIds, "planned");
|
|
360
|
+
assertUniqueItemIds(args.completedItemIds, "completed");
|
|
361
|
+
assertUniqueItemIds(args.deadLetter.map((entry) => entry.item_id), "dead_letter");
|
|
362
|
+
const planned = new Set(args.plannedItemIds);
|
|
363
|
+
const completed = new Set(args.completedItemIds);
|
|
364
|
+
const deadLettered = new Set(args.deadLetter.map((entry) => entry.item_id));
|
|
365
|
+
const unknownCompleted = args.completedItemIds.filter((itemId) => !planned.has(itemId));
|
|
366
|
+
const unknownDeadLetter = args.deadLetter
|
|
367
|
+
.map((entry) => entry.item_id)
|
|
368
|
+
.filter((itemId) => !planned.has(itemId));
|
|
369
|
+
const overlapping = args.completedItemIds.filter((itemId) => deadLettered.has(itemId));
|
|
370
|
+
if (unknownCompleted.length > 0 || unknownDeadLetter.length > 0 || overlapping.length > 0) {
|
|
371
|
+
throw new Error([
|
|
372
|
+
"dispatch incomplete artifact: invalid full-batch partition",
|
|
373
|
+
unknownCompleted.length > 0
|
|
374
|
+
? `unknown completed item_ids=${unknownCompleted.join(",")}`
|
|
375
|
+
: null,
|
|
376
|
+
unknownDeadLetter.length > 0
|
|
377
|
+
? `unknown dead_letter item_ids=${unknownDeadLetter.join(",")}`
|
|
378
|
+
: null,
|
|
379
|
+
overlapping.length > 0 ? `overlapping item_ids=${overlapping.join(",")}` : null,
|
|
380
|
+
].filter((part) => part !== null).join("; "));
|
|
381
|
+
}
|
|
382
|
+
return {
|
|
383
|
+
schema_version: "1",
|
|
384
|
+
pipeline: args.pipeline,
|
|
385
|
+
batch_label: args.batchLabel,
|
|
386
|
+
created_at: args.createdAt,
|
|
387
|
+
breaker: args.breaker,
|
|
388
|
+
completed_item_ids: args.plannedItemIds.filter((itemId) => completed.has(itemId)),
|
|
389
|
+
dead_letter: args.plannedItemIds
|
|
390
|
+
.filter((itemId) => deadLettered.has(itemId))
|
|
391
|
+
.map((itemId) => args.deadLetter.find((entry) => entry.item_id === itemId)),
|
|
392
|
+
incomplete_item_ids: args.plannedItemIds.filter((itemId) => !completed.has(itemId) && !deadLettered.has(itemId)),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
/** State-machine convenience wrapper for normal first-attempt batches. */
|
|
396
|
+
export function buildDispatchIncompleteArtifact(args) {
|
|
397
|
+
const trip = args.state.tripped();
|
|
398
|
+
return buildDispatchIncompleteArtifactFromPartition({
|
|
399
|
+
pipeline: args.pipeline,
|
|
400
|
+
batchLabel: args.batchLabel,
|
|
401
|
+
createdAt: args.createdAt,
|
|
402
|
+
plannedItemIds: args.plannedItemIds,
|
|
403
|
+
completedItemIds: args.state.completedItemIds(),
|
|
404
|
+
deadLetter: args.state.deadLetterEntries(),
|
|
405
|
+
breaker: {
|
|
406
|
+
tripped: trip !== null,
|
|
407
|
+
failure_class: trip?.failure_class ?? null,
|
|
408
|
+
consecutive_item_count: trip?.consecutive_item_count ?? null,
|
|
409
|
+
threshold: args.state.policy.systemic_threshold,
|
|
410
|
+
},
|
|
411
|
+
});
|
|
412
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const CAPABILITY_ROWS = [
|
|
2
|
+
{
|
|
3
|
+
execution_adapter: "openai_sdk",
|
|
4
|
+
adapter_package_version: "6.39.0",
|
|
5
|
+
protocol_version: "openai_responses_v1",
|
|
6
|
+
endpoint_kind: "official_sdk",
|
|
7
|
+
capabilities: {
|
|
8
|
+
structured_failure_evidence: true,
|
|
9
|
+
counted_adapter_requests: true,
|
|
10
|
+
sdk_retry_zero: true,
|
|
11
|
+
invoke_once: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
execution_adapter: "anthropic_sdk",
|
|
16
|
+
adapter_package_version: "0.99.0",
|
|
17
|
+
protocol_version: "anthropic_messages_v1",
|
|
18
|
+
endpoint_kind: "official_sdk",
|
|
19
|
+
capabilities: {
|
|
20
|
+
structured_failure_evidence: true,
|
|
21
|
+
counted_adapter_requests: true,
|
|
22
|
+
sdk_retry_zero: true,
|
|
23
|
+
invoke_once: true,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
export function resolveDispatchFallbackAdapterCapabilities(args) {
|
|
28
|
+
const row = CAPABILITY_ROWS.find((candidate) => candidate.execution_adapter === args.executionAdapter &&
|
|
29
|
+
candidate.adapter_package_version === args.adapterPackageVersion &&
|
|
30
|
+
candidate.protocol_version === args.protocolVersion &&
|
|
31
|
+
candidate.endpoint_kind === "official_sdk");
|
|
32
|
+
return row ? { ...row.capabilities } : null;
|
|
33
|
+
}
|