sortie-dogs 0.8.2 → 0.8.3

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 (81) hide show
  1. package/README.md +1 -1
  2. package/dist/asset-version.d.ts +1 -1
  3. package/dist/asset-version.js +1 -1
  4. package/dist/core/acceptance-compiler.d.ts +66 -0
  5. package/dist/core/acceptance-compiler.js +125 -0
  6. package/dist/core/adaptive-eligibility.d.ts +34 -0
  7. package/dist/core/adaptive-eligibility.js +98 -0
  8. package/dist/core/adaptive-progression.d.ts +35 -0
  9. package/dist/core/adaptive-progression.js +60 -0
  10. package/dist/core/adaptive-remediation-runtime.d.ts +133 -0
  11. package/dist/core/adaptive-remediation-runtime.js +249 -0
  12. package/dist/core/adaptive-signal-comparison.d.ts +29 -0
  13. package/dist/core/adaptive-signal-comparison.js +64 -0
  14. package/dist/core/child-cancellation-plan.d.ts +45 -0
  15. package/dist/core/child-cancellation-plan.js +87 -0
  16. package/dist/core/child-lifecycle-runtime.d.ts +46 -0
  17. package/dist/core/child-lifecycle-runtime.js +154 -0
  18. package/dist/core/child-terminal-reconciliation.d.ts +44 -0
  19. package/dist/core/child-terminal-reconciliation.js +134 -0
  20. package/dist/core/critical-path.d.ts +46 -0
  21. package/dist/core/critical-path.js +152 -0
  22. package/dist/core/evidence-capsule.d.ts +86 -0
  23. package/dist/core/evidence-capsule.js +278 -0
  24. package/dist/core/evidence-ledger-audit.d.ts +34 -0
  25. package/dist/core/evidence-ledger-audit.js +108 -0
  26. package/dist/core/execution-plan.d.ts +26 -0
  27. package/dist/core/execution-plan.js +134 -0
  28. package/dist/core/experience-evidence-summary.d.ts +36 -0
  29. package/dist/core/experience-evidence-summary.js +151 -0
  30. package/dist/core/experience-route-policy.d.ts +76 -0
  31. package/dist/core/experience-route-policy.js +187 -0
  32. package/dist/core/experience-routing-runtime.d.ts +95 -0
  33. package/dist/core/experience-routing-runtime.js +394 -0
  34. package/dist/core/failure-diagnosis.d.ts +48 -0
  35. package/dist/core/failure-diagnosis.js +92 -0
  36. package/dist/core/failure-swarm-plan.d.ts +63 -0
  37. package/dist/core/failure-swarm-plan.js +136 -0
  38. package/dist/core/failure-swarm-runtime.d.ts +91 -0
  39. package/dist/core/failure-swarm-runtime.js +189 -0
  40. package/dist/core/recovery-outcome-aggregate.d.ts +98 -0
  41. package/dist/core/recovery-outcome-aggregate.js +267 -0
  42. package/dist/core/run-flight-ledger.d.ts +350 -0
  43. package/dist/core/run-flight-ledger.js +754 -0
  44. package/dist/core/scope-lease-registry.d.ts +2 -0
  45. package/dist/core/scope-lease-registry.js +9 -0
  46. package/dist/core/terminal-rescue-policy.d.ts +76 -0
  47. package/dist/core/terminal-rescue-policy.js +152 -0
  48. package/dist/core/terminal-rescue-runtime.d.ts +61 -0
  49. package/dist/core/terminal-rescue-runtime.js +107 -0
  50. package/dist/core/types.d.ts +134 -1
  51. package/dist/core/validate-schema.d.ts +2 -1
  52. package/dist/core/validate-schema.js +148 -0
  53. package/dist/core/wave-boundary-replay.d.ts +50 -0
  54. package/dist/core/wave-boundary-replay.js +108 -0
  55. package/dist/core/worktree-commit-artifact.d.ts +5 -3
  56. package/dist/core/worktree-commit-artifact.js +56 -21
  57. package/dist/core/worktree-lifecycle.d.ts +5 -0
  58. package/dist/core/worktree-lifecycle.js +143 -47
  59. package/dist/core/worktree-parallel-dispatch.d.ts +62 -1
  60. package/dist/core/worktree-parallel-dispatch.js +646 -50
  61. package/dist/index.d.ts +14 -0
  62. package/dist/index.js +7 -0
  63. package/dist/plugin/adaptive-remediation-host.d.ts +161 -0
  64. package/dist/plugin/adaptive-remediation-host.js +289 -0
  65. package/dist/plugin/fast-lane.d.ts +1 -0
  66. package/dist/plugin/fast-lane.js +2 -0
  67. package/dist/plugin/index.d.ts +2 -0
  68. package/dist/plugin/index.js +916 -25
  69. package/dist/plugin/model-routing-hook.d.ts +2 -0
  70. package/dist/plugin/model-routing-hook.js +12 -1
  71. package/dist/plugin/terminal-rescue-binding.d.ts +13 -0
  72. package/dist/plugin/terminal-rescue-binding.js +40 -0
  73. package/dist/plugin/terminal-rescue-host.d.ts +69 -0
  74. package/dist/plugin/terminal-rescue-host.js +167 -0
  75. package/dist/reflection/managed-block.d.ts +34 -0
  76. package/dist/reflection/managed-block.js +128 -0
  77. package/dist/reflection/managed-sync.d.ts +18 -0
  78. package/dist/reflection/managed-sync.js +144 -0
  79. package/dist/runtime-assets.d.ts +8 -8
  80. package/dist/runtime-assets.js +42 -11
  81. package/package.json +6 -2
@@ -0,0 +1,249 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { compareAdaptiveSignal } from "./adaptive-signal-comparison.js";
3
+ import { selectAdaptiveRemediation } from "./adaptive-eligibility.js";
4
+ /** Shared RunFlightLedger adapter: probe iterations charge the existing cumulative recovery authority. */
5
+ export class AdaptiveRunFlightLineage {
6
+ options;
7
+ predecessor;
8
+ reservations = new Set();
9
+ constructor(options) {
10
+ this.options = options;
11
+ this.predecessor = options.predecessor_attempt_id;
12
+ }
13
+ async reserveProbe() {
14
+ const attemptID = randomUUID();
15
+ await this.options.ledger.append({ kind: "attempt.started", at: new Date().toISOString(), attempt_id: attemptID,
16
+ predecessor_attempt_id: this.predecessor, unit_id: this.options.unit_id, candidate_id: this.options.candidate_id,
17
+ route_id: this.options.route_id, role: "implementation", selected_model: this.options.selected_model,
18
+ selected_variant: this.options.selected_variant, child_id: null, call_id: randomUUID(),
19
+ budget_charge: { kind: "adaptive_probe", recovery_actions: 1, probe_iterations: 1,
20
+ model_attempts: this.options.model_attempt_per_patch ? 1 : 0 } });
21
+ this.reservations.add(attemptID);
22
+ this.predecessor = attemptID;
23
+ return attemptID;
24
+ }
25
+ async finishProbe(input) {
26
+ if (!this.reservations.delete(input.reservation_id))
27
+ throw new Error("adaptive-lineage-reservation-invalid");
28
+ await this.options.ledger.append({ kind: "attempt.finished", at: new Date().toISOString(), attempt_id: input.reservation_id,
29
+ observed_model: this.options.model_attempt_per_patch ? this.options.selected_model : null,
30
+ observed_variant: this.options.model_attempt_per_patch ? this.options.selected_variant : null,
31
+ failure: null, disposition: "succeeded", observation: { stage: "recovery", duration_ms: input.duration_ms,
32
+ usage: { input_tokens: null, cache_read_tokens: null, output_tokens: null, provenance: "unknown" },
33
+ estimated_cost: { usd: null, provenance: "unknown" } },
34
+ references: { capsule_ids: [], artifact_ids: [`sha256:${input.patch.patch_fingerprint}`] } });
35
+ }
36
+ async failProbe(input) {
37
+ if (!this.reservations.delete(input.reservation_id))
38
+ throw new Error("adaptive-lineage-reservation-invalid");
39
+ await this.options.ledger.append({ kind: "attempt.finished", at: new Date().toISOString(), attempt_id: input.reservation_id,
40
+ observed_model: this.options.model_attempt_per_patch ? this.options.selected_model : null,
41
+ observed_variant: this.options.model_attempt_per_patch ? this.options.selected_variant : null,
42
+ failure: { category: "implementation", code: input.code }, disposition: "failed",
43
+ observation: { stage: "recovery", duration_ms: input.duration_ms,
44
+ usage: { input_tokens: null, cache_read_tokens: null, output_tokens: null, provenance: "unknown" },
45
+ estimated_cost: { usd: null, provenance: "unknown" } }, references: { capsule_ids: [], artifact_ids: [] } });
46
+ }
47
+ }
48
+ const pending = (command) => ({
49
+ command: [command.executable, ...(command.args ?? [])], status: "pending", exit_code: null, fingerprint: null,
50
+ });
51
+ const goalMet = (signal, value) => value !== null &&
52
+ (signal.goal.operator === "at-least" ? value >= signal.goal.value : value <= signal.goal.value);
53
+ const failure = (code, detail) => ({ code, fallback: "stop", detail });
54
+ /** Executes the bounded state machine. Hosts own process/worktree mechanics; evidence cannot skip a stage. */
55
+ export class AdaptiveRemediationRuntime {
56
+ host;
57
+ constructor(host) {
58
+ this.host = host;
59
+ }
60
+ async execute(request) {
61
+ const started = Date.now();
62
+ const selected = selectAdaptiveRemediation(request.eligibility);
63
+ if (selected.status !== "selected")
64
+ throw new Error(`adaptive-admission-${selected.reason}`);
65
+ const target = await this.host.snapshotTarget(request.target_ref);
66
+ const contract = {
67
+ version: "0.1.0", task_id: request.task_id, selection: selected.selection, state: "ADMITTED",
68
+ target: { ref: request.target_ref, base_head: target.head, base_tree: target.tree },
69
+ allowed_paths: { read: [...request.allowed_paths.read], write: [...request.allowed_paths.write] },
70
+ limits: { iteration_budget: request.iteration_budget, max_changed_paths: request.max_changed_paths, cleanup_timeout_ms: request.cleanup_timeout_ms },
71
+ probe_validation: request.probe_validation, canonical_validation: request.canonical_validation,
72
+ improvement_signals: [request.improvement_signal], risk_class: request.risk_class,
73
+ post_merge_validation: request.post_merge_validation, baseline_probe: null, iterations: [], converged: false,
74
+ full_validation: pending(request.canonical_validation), review: { status: "pending", fingerprint: null, remediation_attempts: 0 },
75
+ promotion: { status: "pending", expected_target_head: target.head, observed_target_head: null, candidate_head: null },
76
+ post_merge: pending(request.post_merge_validation), failure: null,
77
+ metrics: { mode: selected.selection.mode, reason: selected.selection.reason, activation_reason: selected.selection.activation_reason,
78
+ iteration_count: 0, probe_count: 0, full_validation_count: 0, post_merge_validation_count: 0,
79
+ discarded_patch_count: 0, time_to_first_signal_ms: null, duration_ms: 0, total_tokens: null,
80
+ estimated_cost_usd: null, sol_demotions: 0, cas_conflicts: 0, post_merge_failures: 0 },
81
+ };
82
+ if (selected.selection.mode === "standard")
83
+ return { ...contract, metrics: { ...contract.metrics, duration_ms: Date.now() - started } };
84
+ const candidates = [];
85
+ let currentHead = target.head;
86
+ let baseline;
87
+ try {
88
+ for (let number = 1; number <= request.iteration_budget; number += 1) {
89
+ const iteration = number;
90
+ const candidate = await this.host.openCandidate(currentHead, iteration);
91
+ candidates.push(candidate);
92
+ if (baseline === undefined) {
93
+ contract.state = "PROBING";
94
+ baseline = await this.host.runBaselineProbe(candidate, request.probe_validation);
95
+ contract.metrics.probe_count += 1;
96
+ if (baseline.exit_code !== 0 || baseline.signal_id !== request.improvement_signal.signal_id) {
97
+ contract.state = "ABANDONED";
98
+ contract.failure = failure("PROBE_FAILED", "Baseline probe did not return the declared typed signal.");
99
+ break;
100
+ }
101
+ contract.baseline_probe = { signal_id: baseline.signal_id, value: baseline.value, artifact_fingerprint: baseline.fingerprint };
102
+ contract.metrics.time_to_first_signal_ms = Date.now() - started;
103
+ }
104
+ const before = await this.host.snapshotTarget(request.target_ref);
105
+ if (before.head !== target.head || before.tree !== target.tree) {
106
+ contract.state = "REINTEGRATE_REQUIRED";
107
+ contract.failure = failure("CAS_DRIFT", "Target changed before candidate promotion.");
108
+ contract.metrics.cas_conflicts += 1;
109
+ break;
110
+ }
111
+ let reservation = "";
112
+ const iterationStarted = Date.now();
113
+ try {
114
+ reservation = await this.host.reserveProbe?.({ task_id: request.task_id, iteration, parent_candidate: currentHead }) ?? "";
115
+ }
116
+ catch (error) {
117
+ if (error !== null && typeof error === "object" && "code" in error && error.code === "budget") {
118
+ contract.state = "SOL_DEMOTED";
119
+ contract.failure = failure("ITERATION_BUDGET_EXHAUSTED", "Shared recovery budget denied the probe iteration.");
120
+ contract.metrics.sol_demotions += 1;
121
+ break;
122
+ }
123
+ throw error;
124
+ }
125
+ let patch;
126
+ try {
127
+ patch = await this.host.producePatch({ candidate, iteration, acceptance: request.acceptance,
128
+ allowed_paths: request.allowed_paths, probe_validation: request.probe_validation });
129
+ }
130
+ catch (error) {
131
+ if (reservation !== "")
132
+ await this.host.failProbe?.({ reservation_id: reservation,
133
+ duration_ms: Date.now() - iterationStarted, code: "PROBE_FAILED" });
134
+ contract.state = "ABANDONED";
135
+ contract.failure = failure("PROBE_FAILED", error instanceof Error ? error.message : "Probe failed.");
136
+ break;
137
+ }
138
+ contract.metrics.probe_count += 1;
139
+ if (reservation !== "")
140
+ await this.host.finishProbe?.({ reservation_id: reservation, patch, duration_ms: Date.now() - iterationStarted });
141
+ const outside = patch.changed_paths.filter((path) => !request.allowed_paths.write.includes(path));
142
+ if (patch.candidate.parent_head !== currentHead || patch.changed_paths.length === 0 ||
143
+ patch.changed_paths.length > request.max_changed_paths || outside.length > 0) {
144
+ contract.state = "REJECTED";
145
+ contract.failure = failure("SCOPE_EXPANSION_REQUIRED", "Patch changed paths outside its bounded unit.");
146
+ break;
147
+ }
148
+ const compared = compareAdaptiveSignal({ signal: { signal_id: request.improvement_signal.signal_id,
149
+ improvement_direction: request.improvement_signal.improvement_direction,
150
+ absolute_improvement_threshold: request.improvement_signal.absolute_improvement_threshold },
151
+ baseline: { signal_id: baseline.signal_id, value: baseline.value },
152
+ current: { signal_id: patch.probe.signal_id, value: patch.probe.value } });
153
+ const outcome = compared.status === "compared" ? compared.outcome : "inconclusive";
154
+ const cleanup = await this.host.inspectCleanup(candidate, request.cleanup_timeout_ms);
155
+ const evidence = { iteration, parent_candidate: currentHead,
156
+ candidate_head: patch.candidate_head, hypothesis: patch.hypothesis, patch_fingerprint: patch.patch_fingerprint,
157
+ changed_paths: [...patch.changed_paths], measured_probe: { signal_id: patch.probe.signal_id,
158
+ baseline_value: baseline.value, current_value: patch.probe.value, outcome,
159
+ artifact_fingerprint: patch.probe.fingerprint }, cleanup: { status: cleanup.ok ? "pass" : "fail", remaining_paths: [...cleanup.remaining_paths] } };
160
+ contract.iterations = [...contract.iterations, evidence];
161
+ contract.metrics.iteration_count += 1;
162
+ if (!cleanup.ok) {
163
+ contract.state = "ABANDONED";
164
+ contract.failure = failure("CLEANUP_FAILED", "Iteration cleanup did not close all residue or processes.");
165
+ break;
166
+ }
167
+ if (patch.probe.exit_code !== 0) {
168
+ contract.state = "ABANDONED";
169
+ contract.failure = failure("PROBE_FAILED", "Patch probe exited unsuccessfully.");
170
+ break;
171
+ }
172
+ if (outcome !== "improved") {
173
+ contract.metrics.discarded_patch_count += 1;
174
+ contract.state = "ABANDONED";
175
+ contract.failure = failure("NO_IMPROVEMENT", `Measured signal outcome was ${outcome}.`);
176
+ break;
177
+ }
178
+ contract.state = "PROMISING";
179
+ currentHead = patch.candidate_head;
180
+ baseline = patch.probe;
181
+ if (goalMet(request.improvement_signal, patch.probe.value)) {
182
+ contract.converged = true;
183
+ contract.state = "CONVERGED";
184
+ break;
185
+ }
186
+ if (number === request.iteration_budget) {
187
+ contract.state = "SOL_DEMOTED";
188
+ contract.failure = failure("ITERATION_BUDGET_EXHAUSTED", "Declared convergence goal was not reached within the shared iteration budget.");
189
+ contract.metrics.sol_demotions += 1;
190
+ break;
191
+ }
192
+ contract.state = "REFINING";
193
+ }
194
+ if (!contract.converged || contract.failure !== null)
195
+ return contract;
196
+ const finalCandidate = candidates.at(-1);
197
+ contract.full_validation = await this.host.runCanonical(finalCandidate, request.canonical_validation);
198
+ contract.metrics.full_validation_count = 1;
199
+ if (contract.full_validation.status !== "pass") {
200
+ contract.state = "ABANDONED";
201
+ contract.failure = failure("FULL_VALIDATION_FAILED", "Canonical candidate validation failed.");
202
+ return contract;
203
+ }
204
+ contract.state = "FULL_VALIDATED";
205
+ const review = await this.host.review({ candidate: finalCandidate, candidate_head: currentHead, risk_class: request.risk_class });
206
+ contract.review = { status: review.status, fingerprint: review.fingerprint, remediation_attempts: review.status === "remediation-required" ? 1 : 0 };
207
+ if (review.status !== "pass") {
208
+ contract.state = "REJECTED";
209
+ contract.failure = failure("REVIEW_REMEDIATION_REQUIRED", "Risk-based review did not approve this candidate.");
210
+ return contract;
211
+ }
212
+ contract.state = "REVIEWED";
213
+ const beforeCas = await this.host.snapshotTarget(request.target_ref);
214
+ if (beforeCas.head !== target.head || beforeCas.tree !== target.tree) {
215
+ contract.state = "REINTEGRATE_REQUIRED";
216
+ contract.failure = failure("CAS_DRIFT", "Expected target base changed before CAS.");
217
+ contract.metrics.cas_conflicts += 1;
218
+ return contract;
219
+ }
220
+ const promotion = await this.host.promote({ target_ref: request.target_ref, expected_head: target.head, candidate_head: currentHead });
221
+ contract.promotion = { status: promotion.status, expected_target_head: target.head,
222
+ observed_target_head: promotion.observed_head, candidate_head: currentHead };
223
+ if (promotion.status !== "promoted") {
224
+ contract.state = "REINTEGRATE_REQUIRED";
225
+ contract.failure = failure("CAS_DRIFT", "Target CAS lost; target was not changed by this attempt.");
226
+ contract.metrics.cas_conflicts += 1;
227
+ return contract;
228
+ }
229
+ contract.state = "MERGED";
230
+ contract.post_merge = await this.host.runPostMerge(request.post_merge_validation);
231
+ contract.metrics.post_merge_validation_count = 1;
232
+ if (contract.post_merge.status !== "pass") {
233
+ contract.failure = failure("POST_MERGE_VERIFICATION_FAILED", "Post-merge verification failed; a new repair candidate is required.");
234
+ contract.metrics.post_merge_failures += 1;
235
+ return contract;
236
+ }
237
+ contract.state = "VERIFIED";
238
+ return contract;
239
+ }
240
+ finally {
241
+ const released = await this.host.release(candidates).catch(() => ({ ok: false, remaining_paths: candidates.map(({ path }) => path) }));
242
+ if (!released.ok && contract.failure === null) {
243
+ contract.state = "ABANDONED";
244
+ contract.failure = failure("CLEANUP_FAILED", "Final candidate cleanup failed.");
245
+ }
246
+ contract.metrics.duration_ms = Date.now() - started;
247
+ }
248
+ }
249
+ }
@@ -0,0 +1,29 @@
1
+ export type AdaptiveSignalImprovementDirection = "increase" | "decrease";
2
+ export interface AdaptiveSignalDefinition {
3
+ readonly signal_id: string;
4
+ readonly improvement_direction: AdaptiveSignalImprovementDirection;
5
+ readonly absolute_improvement_threshold: number;
6
+ }
7
+ export interface AdaptiveSignalObservation {
8
+ readonly signal_id: string;
9
+ readonly value: number | null;
10
+ }
11
+ export interface AdaptiveSignalComparisonInput {
12
+ readonly signal: AdaptiveSignalDefinition;
13
+ readonly baseline: AdaptiveSignalObservation;
14
+ readonly current: AdaptiveSignalObservation;
15
+ }
16
+ export type AdaptiveSignalComparisonOutcome = "improved" | "unchanged" | "worse" | "inconclusive";
17
+ export type AdaptiveSignalComparisonRejectionReason = "invalid-input" | "invalid-signal-identity" | "invalid-improvement-direction" | "invalid-improvement-threshold" | "invalid-observation" | "numeric-overflow";
18
+ export type AdaptiveSignalComparisonResult = {
19
+ readonly status: "compared";
20
+ readonly outcome: AdaptiveSignalComparisonOutcome;
21
+ } | {
22
+ readonly status: "rejected";
23
+ readonly reason: AdaptiveSignalComparisonRejectionReason;
24
+ };
25
+ /**
26
+ * Compares two declared observations without probing, validation, or runtime side effects.
27
+ * The result is evidence classification only; it does not establish convergence or authorize promotion.
28
+ */
29
+ export declare function compareAdaptiveSignal(input: unknown): AdaptiveSignalComparisonResult;
@@ -0,0 +1,64 @@
1
+ const INPUT_KEYS = ["signal", "baseline", "current"];
2
+ const SIGNAL_KEYS = [
3
+ "signal_id",
4
+ "improvement_direction",
5
+ "absolute_improvement_threshold",
6
+ ];
7
+ const OBSERVATION_KEYS = ["signal_id", "value"];
8
+ const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
9
+ const hasOnlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
10
+ const isSignalId = (value) => typeof value === "string" && value.length > 0 && value.trim() === value;
11
+ const reject = (reason) => ({ status: "rejected", reason });
12
+ /**
13
+ * Compares two declared observations without probing, validation, or runtime side effects.
14
+ * The result is evidence classification only; it does not establish convergence or authorize promotion.
15
+ */
16
+ export function compareAdaptiveSignal(input) {
17
+ if (!isObject(input) || !hasOnlyKeys(input, INPUT_KEYS)
18
+ || !Object.hasOwn(input, "signal") || !isObject(input.signal)
19
+ || !Object.hasOwn(input, "baseline") || !isObject(input.baseline)
20
+ || !Object.hasOwn(input, "current") || !isObject(input.current)
21
+ || !hasOnlyKeys(input.signal, SIGNAL_KEYS)
22
+ || !hasOnlyKeys(input.baseline, OBSERVATION_KEYS)
23
+ || !hasOnlyKeys(input.current, OBSERVATION_KEYS)) {
24
+ return reject("invalid-input");
25
+ }
26
+ const { signal, baseline, current } = input;
27
+ if (!Object.hasOwn(signal, "signal_id") || !isSignalId(signal.signal_id)
28
+ || !Object.hasOwn(baseline, "signal_id") || !isSignalId(baseline.signal_id)
29
+ || !Object.hasOwn(current, "signal_id") || !isSignalId(current.signal_id)
30
+ || signal.signal_id !== baseline.signal_id || signal.signal_id !== current.signal_id) {
31
+ return reject("invalid-signal-identity");
32
+ }
33
+ if (!Object.hasOwn(signal, "improvement_direction")
34
+ || (signal.improvement_direction !== "increase" && signal.improvement_direction !== "decrease")) {
35
+ return reject("invalid-improvement-direction");
36
+ }
37
+ if (!Object.hasOwn(signal, "absolute_improvement_threshold")
38
+ || typeof signal.absolute_improvement_threshold !== "number"
39
+ || !Number.isFinite(signal.absolute_improvement_threshold)
40
+ || signal.absolute_improvement_threshold < 0) {
41
+ return reject("invalid-improvement-threshold");
42
+ }
43
+ if (!Object.hasOwn(baseline, "value") || !Object.hasOwn(current, "value")
44
+ || (baseline.value !== null
45
+ && (typeof baseline.value !== "number" || !Number.isFinite(baseline.value)))
46
+ || (current.value !== null
47
+ && (typeof current.value !== "number" || !Number.isFinite(current.value)))) {
48
+ return reject("invalid-observation");
49
+ }
50
+ if (baseline.value === null || current.value === null) {
51
+ return { status: "compared", outcome: "inconclusive" };
52
+ }
53
+ const directedDifference = signal.improvement_direction === "increase"
54
+ ? current.value - baseline.value
55
+ : baseline.value - current.value;
56
+ if (!Number.isFinite(directedDifference))
57
+ return reject("numeric-overflow");
58
+ if (directedDifference < 0)
59
+ return { status: "compared", outcome: "worse" };
60
+ if (directedDifference === 0 || directedDifference < signal.absolute_improvement_threshold) {
61
+ return { status: "compared", outcome: "unchanged" };
62
+ }
63
+ return { status: "compared", outcome: "improved" };
64
+ }
@@ -0,0 +1,45 @@
1
+ import { type ChildTerminalEvidence, type ChildTerminalIdentity, type ChildTerminalObservation, type ChildTerminalReconciliationReason } from "./child-terminal-reconciliation.js";
2
+ export interface ChildCancellationPlanInput {
3
+ readonly current: ChildTerminalIdentity;
4
+ readonly observation: ChildTerminalObservation;
5
+ readonly evidence: ChildTerminalEvidence;
6
+ readonly settled_fingerprint?: string | null;
7
+ /** Caller-supplied monotonic or wall-clock milliseconds. Must share a clock domain with now_ms. */
8
+ readonly deadline_ms: number;
9
+ /** Caller-supplied current time; this planner never reads a clock. */
10
+ readonly now_ms: number;
11
+ readonly cancellation_requested: boolean;
12
+ /** Durable stop-request history. Exact identity matching provides replay suppression. */
13
+ readonly prior_stop_requests: readonly ChildTerminalIdentity[];
14
+ }
15
+ export type ChildCancellationTrigger = "deadline_expired" | "explicit_cancellation";
16
+ export interface ChildCancellationStopProposal {
17
+ readonly identity: ChildTerminalIdentity;
18
+ readonly trigger: ChildCancellationTrigger;
19
+ readonly runtime_responsibilities: {
20
+ readonly persist_stop_request_for_idempotency: true;
21
+ readonly stop_normal_process_tree: true;
22
+ readonly verify_actual_cancellation_and_no_leaks: true;
23
+ };
24
+ }
25
+ export type ChildCancellationNonProposalReason = "artifact_window_open" | "artifact_window_unknown" | "terminal_unknown" | "terminal_already_observed" | "cancellation_not_requested" | "stop_already_requested";
26
+ export type ChildCancellationPlanOutcome = {
27
+ readonly status: "not_proposed";
28
+ readonly reason: ChildCancellationNonProposalReason;
29
+ } | {
30
+ readonly status: "rejected";
31
+ readonly reason: "invalid_input" | "invalid_time";
32
+ } | {
33
+ readonly status: "rejected";
34
+ readonly reason: "terminal_reconciliation_rejected";
35
+ readonly reconciliation_reason: ChildTerminalReconciliationReason;
36
+ } | {
37
+ readonly status: "proposed";
38
+ readonly proposal: ChildCancellationStopProposal;
39
+ };
40
+ /**
41
+ * Produces an inert stop proposal. It performs no I/O, clock read, process stop, release,
42
+ * ledger append, writer admission, or target mutation. A satisfied terminal observation only
43
+ * makes stopping unnecessary; it does not independently assert that every resource is released.
44
+ */
45
+ export declare function planChildCancellation(input: unknown): ChildCancellationPlanOutcome;
@@ -0,0 +1,87 @@
1
+ import { reconcileChildTerminal, } from "./child-terminal-reconciliation.js";
2
+ const INPUT_KEYS = [
3
+ "current",
4
+ "observation",
5
+ "evidence",
6
+ "settled_fingerprint",
7
+ "deadline_ms",
8
+ "now_ms",
9
+ "cancellation_requested",
10
+ "prior_stop_requests",
11
+ ];
12
+ function isRecord(value) {
13
+ return typeof value === "object" && value !== null && !Array.isArray(value);
14
+ }
15
+ function hasOnlyInputKeys(value) {
16
+ return Object.keys(value).every((key) => INPUT_KEYS.includes(key));
17
+ }
18
+ function isTime(value) {
19
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
20
+ }
21
+ function sameIdentity(left, right) {
22
+ if (!isRecord(left))
23
+ return false;
24
+ return left.run_id === right.run_id && left.unit_id === right.unit_id &&
25
+ left.attempt_id === right.attempt_id && left.predecessor_attempt_id === right.predecessor_attempt_id &&
26
+ left.candidate_id === right.candidate_id && left.route_id === right.route_id &&
27
+ left.child_id === right.child_id && left.call_id === right.call_id;
28
+ }
29
+ /**
30
+ * Produces an inert stop proposal. It performs no I/O, clock read, process stop, release,
31
+ * ledger append, writer admission, or target mutation. A satisfied terminal observation only
32
+ * makes stopping unnecessary; it does not independently assert that every resource is released.
33
+ */
34
+ export function planChildCancellation(input) {
35
+ if (!isRecord(input) || !hasOnlyInputKeys(input) || typeof input.cancellation_requested !== "boolean" ||
36
+ !Array.isArray(input.prior_stop_requests)) {
37
+ return { status: "rejected", reason: "invalid_input" };
38
+ }
39
+ if (!isTime(input.deadline_ms) || !isTime(input.now_ms)) {
40
+ return { status: "rejected", reason: "invalid_time" };
41
+ }
42
+ const reconciliation = reconcileChildTerminal({
43
+ current: input.current,
44
+ observation: input.observation,
45
+ evidence: input.evidence,
46
+ settled_fingerprint: input.settled_fingerprint,
47
+ });
48
+ if (reconciliation.status === "rejected") {
49
+ return {
50
+ status: "rejected",
51
+ reason: "terminal_reconciliation_rejected",
52
+ reconciliation_reason: reconciliation.reason,
53
+ };
54
+ }
55
+ const current = input.current;
56
+ const evidence = input.evidence;
57
+ if (evidence.artifact_window_closed === "unsatisfied") {
58
+ return { status: "not_proposed", reason: "artifact_window_open" };
59
+ }
60
+ if (evidence.artifact_window_closed === "unknown") {
61
+ return { status: "not_proposed", reason: "artifact_window_unknown" };
62
+ }
63
+ if (evidence.terminal === "unknown")
64
+ return { status: "not_proposed", reason: "terminal_unknown" };
65
+ if (evidence.terminal === "satisfied") {
66
+ return { status: "not_proposed", reason: "terminal_already_observed" };
67
+ }
68
+ const expired = input.now_ms >= input.deadline_ms;
69
+ if (!expired && !input.cancellation_requested) {
70
+ return { status: "not_proposed", reason: "cancellation_not_requested" };
71
+ }
72
+ if (input.prior_stop_requests.some((identity) => sameIdentity(identity, current))) {
73
+ return { status: "not_proposed", reason: "stop_already_requested" };
74
+ }
75
+ return {
76
+ status: "proposed",
77
+ proposal: {
78
+ identity: { ...current },
79
+ trigger: input.cancellation_requested ? "explicit_cancellation" : "deadline_expired",
80
+ runtime_responsibilities: {
81
+ persist_stop_request_for_idempotency: true,
82
+ stop_normal_process_tree: true,
83
+ verify_actual_cancellation_and_no_leaks: true,
84
+ },
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,46 @@
1
+ import { RunFlightLedger, type ChildFlightState } from "./run-flight-ledger.js";
2
+ import { type ChildTerminalIdentity, type ChildTerminalObservation, type ChildTerminalEvidence } from "./child-terminal-reconciliation.js";
3
+ export declare const DEFAULT_CHILD_DEADLINE_MS: number;
4
+ export declare const DEFAULT_CHILD_CHECK_WAIT_MS = 5000;
5
+ export interface ChildLifecycleDescriptor {
6
+ readonly identity: ChildTerminalIdentity;
7
+ readonly deadline_ms: number;
8
+ }
9
+ export interface ChildLifecycleRuntime {
10
+ observe(): Promise<{
11
+ observation: ChildTerminalObservation;
12
+ evidence: ChildTerminalEvidence;
13
+ }>;
14
+ /** Optional host policy entry. A terminal result means the same lifecycle completed a live takeover. */
15
+ takeover?(): Promise<ChildLifecycleResult | undefined>;
16
+ /** Resolves only after the host's normal process-tree stop request has completed. */
17
+ stop(): Promise<void>;
18
+ release(): Promise<void>;
19
+ terminal(state: ChildFlightState): Promise<void>;
20
+ }
21
+ export type ChildLifecycleResult = {
22
+ status: "terminal";
23
+ state: ChildFlightState;
24
+ } | {
25
+ status: "waiting";
26
+ reason: string;
27
+ };
28
+ export interface ChildLifecycleOptions {
29
+ readonly checkWaitMs?: number;
30
+ }
31
+ /** One lifecycle for implementation, diagnosis, and escalation; the host retains process ownership. */
32
+ export declare class CancellableChildLifecycle {
33
+ #private;
34
+ readonly descriptor: ChildLifecycleDescriptor;
35
+ readonly ledger: RunFlightLedger;
36
+ readonly runtime: ChildLifecycleRuntime;
37
+ readonly checkWaitMs: number;
38
+ private constructor();
39
+ static open(descriptor: ChildLifecycleDescriptor, ledger: RunFlightLedger, runtime: ChildLifecycleRuntime, options?: ChildLifecycleOptions): Promise<CancellableChildLifecycle>;
40
+ get stopping(): boolean;
41
+ arm(): void;
42
+ dispose(): void;
43
+ check(explicitCancellation?: boolean): Promise<ChildLifecycleResult>;
44
+ /** Live takeover uses the same bounded stop/release path, but must name the currently registered attempt. */
45
+ stopForTakeover(identity: ChildTerminalIdentity): Promise<ChildLifecycleResult>;
46
+ }