@aiwg/cli 2026.10.10 → 2026.10.12
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/agentic/code/providers/model-capabilities.v1.json +78 -14
- package/agentic/code/providers/model-catalog.v1.json +62 -12
- package/dist/src/artifacts/binding-target-identity.js +51 -0
- package/dist/src/artifacts/checksum-manifest.js +12 -0
- package/dist/src/artifacts/index-builder.js +11 -13
- package/dist/src/artifacts/query-engine.js +5 -11
- package/dist/src/cli/handlers/doctor-help.js +32 -0
- package/dist/src/cli/handlers/gates.js +19 -18
- package/dist/src/cli/handlers/models-help.js +7 -1
- package/dist/src/cli/handlers/models.js +3 -3
- package/dist/src/cli/handlers/script-runner.js +8 -5
- package/dist/src/cli/handlers/sessions-intake.js +104 -0
- package/dist/src/cli/handlers/sessions-tail.js +172 -0
- package/dist/src/cli/handlers/sessions.js +287 -31
- package/dist/src/cli/handlers/skill-lint.js +10 -3
- package/dist/src/cli/handlers/steward-format.js +100 -0
- package/dist/src/cli/handlers/steward-models.js +167 -0
- package/dist/src/cli/handlers/steward.js +14 -248
- package/dist/src/cli/handlers/use-bundle-counts.js +61 -0
- package/dist/src/cli/handlers/use.js +18 -28
- package/dist/src/cli/handlers/utilities.js +1 -24
- package/dist/src/config/aiwg-config.js +7 -0
- package/dist/src/config/authorization-cli.js +89 -0
- package/dist/src/config/cli-help.js +1 -0
- package/dist/src/config/cli.js +10 -1
- package/dist/src/config/worker-context.js +337 -0
- package/dist/src/decision/context-pruning.js +3 -2
- package/dist/src/decision/ensemble/contract.js +15 -4
- package/dist/src/decision/ensemble-study/artifacts.js +5 -0
- package/dist/src/decision/ensemble-study/score.js +4 -3
- package/dist/src/decision/feature-export.js +3 -9
- package/dist/src/decision/heldout/collector.js +2 -2
- package/dist/src/decision/issue-triage/runtime.js +3 -9
- package/dist/src/decision/qualification/gates.js +2 -1
- package/dist/src/decision/qualification/release.js +2 -3
- package/dist/src/decision/routing/report.js +2 -0
- package/dist/src/decision/sdlc-screening.js +3 -5
- package/dist/src/decision/sensitivity/comparative.js +5 -3
- package/dist/src/gates/driver.js +6 -77
- package/dist/src/gates/evaluate.js +4 -11
- package/dist/src/gates/floors.js +4 -4
- package/dist/src/gates/index.js +3 -3
- package/dist/src/gates/lattice.js +26 -0
- package/dist/src/gates/registry.js +2 -1
- package/dist/src/gates/report.js +19 -7
- package/dist/src/issues/audit-cli.js +120 -0
- package/dist/src/issues/audit-contract.js +64 -0
- package/dist/src/issues/audit-local.js +82 -0
- package/dist/src/issues/audit.js +220 -0
- package/dist/src/lint/rule-activation.js +15 -1
- package/dist/src/mcp/tools/worker-invoke.mjs +108 -10
- package/dist/src/mission-protocol/canonical-codecs.js +270 -0
- package/dist/src/mission-protocol/codecs.js +22 -550
- package/dist/src/mission-protocol/health-trace-codecs.js +281 -0
- package/dist/src/mission-protocol/health-trace-types.js +43 -0
- package/dist/src/mission-protocol/types.js +2 -42
- package/dist/src/mission-protocol/worker-contract-json.js +10 -0
- package/dist/src/mission-protocol/worker-review-gate.js +100 -0
- package/dist/src/mission-protocol/worker-review-types.js +2 -0
- package/dist/src/models/catalog-policy.js +280 -0
- package/dist/src/models/index.js +1 -1
- package/dist/src/models/model-capabilities.v1.json +78 -14
- package/dist/src/models/model-catalog.v1.json +62 -12
- package/dist/src/models/policy-resources.js +28 -0
- package/dist/src/models/provider-policy.js +10 -539
- package/dist/src/models/resolver.js +26 -8
- package/dist/src/models/router.js +15 -2
- package/dist/src/models/tiers.js +13 -0
- package/dist/src/models/types.js +0 -6
- package/dist/src/models/worker-ceilings.js +59 -0
- package/dist/src/models/worker-invocation-policy.js +243 -0
- package/dist/src/models/worker-plan.js +16 -20
- package/dist/src/models/worker-review-lineage.js +55 -0
- package/dist/src/models/worker-selection-policy.js +1 -1
- package/dist/src/models/wrapper-route.js +6 -3
- package/dist/src/policy/authorization.js +30 -1
- package/dist/src/policy/hitl-config.js +63 -0
- package/dist/src/policy/hitl-receipt.js +57 -0
- package/dist/src/policy/hitl-types.js +4 -0
- package/dist/src/policy/hitl-validation.js +39 -0
- package/dist/src/policy/hitl.js +117 -0
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- package/dist/src/rules/activation.d.mts +1 -1
- package/dist/src/rules/activation.mjs +0 -2
- package/dist/src/rules/gate-coverage.js +51 -0
- package/dist/src/rules/gate-references.js +16 -0
- package/dist/src/rules/rule-frontmatter.schema.json +7 -1
- package/dist/src/serve/worker-command-request.js +21 -4
- package/dist/src/serve/worker-invocation-preparation.js +3 -1
- package/dist/src/serve/worker-invocation.js +21 -10
- package/dist/src/sessions/adapters/deepseek-harness.js +1 -0
- package/dist/src/sessions/adapters/grok-build-native.js +269 -0
- package/dist/src/sessions/adapters/grok-build.js +57 -15
- package/dist/src/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/adapters/hermes-state-db.js +252 -0
- package/dist/src/sessions/adapters/hermes.js +42 -3
- package/dist/src/sessions/adapters/warp.js +1 -1
- package/dist/src/sessions/batch-import.js +44 -6
- package/dist/src/sessions/contracts.js +20 -1
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/discovery-capability.js +38 -0
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +9 -232
- package/dist/src/sessions/index.js +6 -0
- package/dist/src/sessions/intake-dir.js +71 -0
- package/dist/src/sessions/normalization.js +251 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/tail.js +91 -0
- package/dist/src/sessions/workspace-discovery.js +88 -9
- package/dist/src/skills/agent-skill-sidecar.js +28 -0
- package/dist/src/skills/agent-skills.js +4 -23
- package/dist/src/skills/claude-surface-projection.js +25 -0
- package/dist/src/skills/deployer.js +8 -288
- package/dist/src/skills/deployment-policy.js +114 -0
- package/dist/src/skills/deployment-projection.js +152 -0
- package/dist/src/skills/deployment-types.js +20 -0
- package/dist/src/skills/exporter.js +8 -31
- package/dist/src/skills/provider-extensions.js +41 -0
- package/dist/src/skills/run.js +6 -0
- package/dist/src/skills/types.js +0 -8
- package/dist/src/skills/validator-resources.js +68 -0
- package/dist/src/skills/validator.js +6 -55
- package/dist/src/storage/cli-migrate-options.js +125 -0
- package/dist/src/storage/cli-migrate.js +111 -0
- package/dist/src/storage/cli.js +4 -279
- package/dist/src/storage/migration-adapter.js +79 -0
- package/dist/src/storage/migration-copy.js +108 -0
- package/dist/src/storage/migration-protocol.js +14 -197
- package/dist/src/storage/migration-records.js +186 -0
- package/dist/src/storage/migration-types.js +12 -0
- package/dist/src/tracker/forge-http.js +2 -1
- package/dist/src/tracker/issue-pages.js +110 -0
- package/package.json +7 -1
- package/schemas/mission-v1.schema.json +335 -0
- package/schemas/models/model-catalog.v1.schema.json +19 -1
- package/schemas/models/model-config-compatibility.v1.schema.json +3 -2
- package/schemas/models/model-policy.v1.schema.json +1 -1
- package/tools/agents/providers/provider-paths-staging.mjs +35 -0
- package/tools/agents/providers/rule-binding-targets.mjs +146 -0
- package/tools/agents/providers/rule-delivery.mjs +7 -126
- package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
- package/tools/ralph-external/lib/worker-context.mjs +237 -0
- package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +17 -29
- package/tools/ralph-external/lib/worker-launch-cli.mjs +3 -1
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +51 -96
- package/vscode-extension/schemas/aiwg.config.v1.json +177 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// Mission worker payload validation, negotiation and immutable snapshots.
|
|
2
|
+
import { stableJson } from './worker-contract-json.js';
|
|
3
|
+
import { prepareWorkerAcceptance, workerReviewEvidenceErrors, assertWorkerReviewSuccessor } from './worker-review-gate.js';
|
|
1
4
|
import { workerSemanticErrors, contractValidationPointer } from './worker-contract-diagnostics.js';
|
|
2
5
|
import { createHash } from 'node:crypto';
|
|
3
6
|
import { readFileSync } from 'node:fs';
|
|
@@ -5,273 +8,9 @@ import { dirname, join, resolve } from 'node:path';
|
|
|
5
8
|
import { fileURLToPath } from 'node:url';
|
|
6
9
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
7
10
|
import addFormats from 'ajv-formats';
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
12
|
-
throw new Error(`${label} must be an object.`);
|
|
13
|
-
return value;
|
|
14
|
-
}
|
|
15
|
-
function string(value) {
|
|
16
|
-
return typeof value === 'string' && value.length ? value : undefined;
|
|
17
|
-
}
|
|
18
|
-
function requiredId(record) {
|
|
19
|
-
const metadata = record.metadata && typeof record.metadata === 'object' ? record.metadata : {};
|
|
20
|
-
const id = string(record.missionId) ?? string(record.mission_id) ?? string(record.id)
|
|
21
|
-
?? string(record.taskId) ?? string(record.task_id) ?? string(record.responseId) ?? string(metadata.id);
|
|
22
|
-
if (!id)
|
|
23
|
-
throw new Error('Mission source is missing a stable identifier.');
|
|
24
|
-
return id;
|
|
25
|
-
}
|
|
26
|
-
export function normalizeMissionState(nativeState) {
|
|
27
|
-
const state = String(nativeState ?? 'unknown').toLowerCase().replace(/_/g, '-');
|
|
28
|
-
if (['done', 'completed', 'succeeded', 'success'].includes(state))
|
|
29
|
-
return 'completed';
|
|
30
|
-
if (['aborted', 'cancelled', 'canceled'].includes(state))
|
|
31
|
-
return 'cancelled';
|
|
32
|
-
if (['failed', 'error', 'rejected', 'timed-out'].includes(state))
|
|
33
|
-
return 'failed';
|
|
34
|
-
if (['incomplete', 'budget-exhausted', 'max-iterations', 'max-tokens'].includes(state))
|
|
35
|
-
return 'incomplete';
|
|
36
|
-
if (['running', 'in-progress', 'working', 'started', 'assigned', 'admitted', 'starting'].includes(state))
|
|
37
|
-
return 'running';
|
|
38
|
-
if (['pending', 'submitted', 'queued', 'runnable', 'scheduled'].includes(state))
|
|
39
|
-
return 'pending';
|
|
40
|
-
if (['blocked', 'blocked-hitl', 'input-required', 'auth-required', 'paused', 'suspended'].includes(state))
|
|
41
|
-
return 'blocked';
|
|
42
|
-
if (['operator-review', 'operator-review-required', 'manual-review'].includes(state))
|
|
43
|
-
return 'operator-review';
|
|
44
|
-
if (['unknown', 'disconnected', 'detached', 'unreachable'].includes(state))
|
|
45
|
-
return 'unknown';
|
|
46
|
-
return 'unknown';
|
|
47
|
-
}
|
|
48
|
-
function knownKeysFor(source) {
|
|
49
|
-
const common = ['id', 'missionId', 'mission_id', 'taskId', 'task_id', 'responseId', 'title', 'goal', 'objective', 'prompt', 'completion', 'completionCriterion', 'completion_criterion', 'status', 'state', 'nativeState', 'createdAt', 'created_at', 'updatedAt', 'updated_at', 'metadata', 'spec', 'provenance', 'extensions', 'artifacts', 'output', 'partialOutput', 'error', 'checkpoint', 'cycles', 'activityLog', 'totalCost', 'runtimesUsed', 'apiVersion', 'api_version', 'schemaVersion', 'schema_version', 'kind', 'session_id', 'previous_response_id'];
|
|
50
|
-
if (source === 'uhp-2026-08-11')
|
|
51
|
-
common.push('object', 'model', 'incomplete_details', 'store', 'usage');
|
|
52
|
-
return new Set(common);
|
|
53
|
-
}
|
|
54
|
-
function extensions(record, source) {
|
|
55
|
-
const explicit = record.extensions && typeof record.extensions === 'object' ? structuredClone(record.extensions) : {};
|
|
56
|
-
if (record.metadata && typeof record.metadata === 'object')
|
|
57
|
-
explicit.metadata = structuredClone(record.metadata);
|
|
58
|
-
if (record.status && typeof record.status === 'object')
|
|
59
|
-
explicit.nativeStatus = structuredClone(record.status);
|
|
60
|
-
for (const [key, value] of Object.entries(record))
|
|
61
|
-
if (!knownKeysFor(source).has(key))
|
|
62
|
-
explicit[key] = structuredClone(value);
|
|
63
|
-
return explicit;
|
|
64
|
-
}
|
|
65
|
-
function artifacts(record) {
|
|
66
|
-
const values = Array.isArray(record.artifacts) ? record.artifacts : [];
|
|
67
|
-
return values.flatMap((item, index) => {
|
|
68
|
-
if (!item || typeof item !== 'object')
|
|
69
|
-
return [];
|
|
70
|
-
const artifact = item;
|
|
71
|
-
const id = string(artifact.id) ?? string(artifact.fileId) ?? string(artifact.file_id) ?? string(artifact.uri) ?? `artifact-${index}`;
|
|
72
|
-
return [{
|
|
73
|
-
id,
|
|
74
|
-
kind: string(artifact.kind) ?? 'other',
|
|
75
|
-
...(string(artifact.uri) ? { uri: string(artifact.uri) } : {}),
|
|
76
|
-
...(string(artifact.sha256) ? { sha256: string(artifact.sha256) } : {}),
|
|
77
|
-
...(string(artifact.mediaType) ?? string(artifact.media_type) ? { mediaType: string(artifact.mediaType) ?? string(artifact.media_type) } : {}),
|
|
78
|
-
extensions: structuredClone(artifact),
|
|
79
|
-
}];
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
function uhpOutputArtifacts(record) {
|
|
83
|
-
const found = new Map();
|
|
84
|
-
function visit(value) {
|
|
85
|
-
if (Array.isArray(value)) {
|
|
86
|
-
value.forEach(visit);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
if (!value || typeof value !== 'object')
|
|
90
|
-
return;
|
|
91
|
-
const item = value;
|
|
92
|
-
const id = string(item.file_id);
|
|
93
|
-
if (id)
|
|
94
|
-
found.set(id, { id, kind: 'uhp-file', ...(string(item.media_type) ? { mediaType: string(item.media_type) } : {}), extensions: { 'uhp.file': structuredClone(item) } });
|
|
95
|
-
Object.values(item).forEach(visit);
|
|
96
|
-
}
|
|
97
|
-
visit(record.output);
|
|
98
|
-
return [...found.values()];
|
|
99
|
-
}
|
|
100
|
-
function nativeStateFor(source, record, statusRecord) {
|
|
101
|
-
const explicit = string(statusRecord.state) ?? string(record.nativeState) ?? string(record.status)
|
|
102
|
-
?? string(record.state) ?? string(record.observed_state) ?? string(record.observedState) ?? string(record.nodeState);
|
|
103
|
-
if (explicit)
|
|
104
|
-
return explicit;
|
|
105
|
-
if (source === 'mission-ledger' && record.checkpoint && typeof record.checkpoint === 'object') {
|
|
106
|
-
const checkpoint = record.checkpoint;
|
|
107
|
-
if (Array.isArray(checkpoint.pending) && checkpoint.pending.length)
|
|
108
|
-
return 'running';
|
|
109
|
-
if (Array.isArray(checkpoint.failed) && checkpoint.failed.length)
|
|
110
|
-
return 'failed';
|
|
111
|
-
if (Array.isArray(checkpoint.completed) && checkpoint.completed.length)
|
|
112
|
-
return 'completed';
|
|
113
|
-
}
|
|
114
|
-
return 'unknown';
|
|
115
|
-
}
|
|
116
|
-
function sourceVersion(source, record) {
|
|
117
|
-
if (source === 'canonical')
|
|
118
|
-
return string(record.apiVersion) ?? 'unknown';
|
|
119
|
-
if (source === 'uhp-2026-08-11')
|
|
120
|
-
return '2026-08-11';
|
|
121
|
-
if (source === 'executor-v1')
|
|
122
|
-
return 'executor.aiwg.io/v1';
|
|
123
|
-
if (source === 'fleet-workload-v1')
|
|
124
|
-
return 'fleet-workload/v1';
|
|
125
|
-
if (source === 'graph-flow-v1')
|
|
126
|
-
return 'graph.flow.aiwg.io/v1';
|
|
127
|
-
if (source === 'activity-v1')
|
|
128
|
-
return 'activity.aiwg.io/v1';
|
|
129
|
-
return string(record.apiVersion) ?? string(record.api_version) ?? string(record.schemaVersion) ?? string(record.schema_version) ?? 'unversioned';
|
|
130
|
-
}
|
|
131
|
-
function assertSupported(version) {
|
|
132
|
-
const supported = new Set(['unversioned', MISSION_API_VERSION, '2026-08-11', 'executor.aiwg.io/v1', 'fleet-workload/v1', 'graph.flow.aiwg.io/v1', 'activity.aiwg.io/v1']);
|
|
133
|
-
if (supported.has(version))
|
|
134
|
-
return;
|
|
135
|
-
const major = version.match(/(?:^|\/)v(\d+)(?:$|[.-])/)?.[1];
|
|
136
|
-
if (major && major !== '1')
|
|
137
|
-
throw new Error(`Unsupported Mission source major version '${version}'; supported major is v1.`);
|
|
138
|
-
throw new Error(`Unsupported Mission source version '${version}'.`);
|
|
139
|
-
}
|
|
140
|
-
export function validateCanonicalMission(value) {
|
|
141
|
-
const record = object(value, 'Canonical Mission');
|
|
142
|
-
assertMissionWorkerHandoff(record, 'canonical');
|
|
143
|
-
if (record.apiVersion !== MISSION_API_VERSION)
|
|
144
|
-
throw new Error(`Unsupported canonical Mission version '${String(record.apiVersion)}'.`);
|
|
145
|
-
if (record.kind !== 'Mission')
|
|
146
|
-
throw new Error("Canonical Mission kind must be 'Mission'.");
|
|
147
|
-
const metadata = object(record.metadata, 'Mission metadata');
|
|
148
|
-
const spec = object(record.spec, 'Mission spec');
|
|
149
|
-
const status = object(record.status, 'Mission status');
|
|
150
|
-
const provenance = object(record.provenance, 'Mission provenance');
|
|
151
|
-
if (!string(metadata.id) || !string(spec.objective) || !string(provenance.sourceContract) || !string(provenance.sourceVersion))
|
|
152
|
-
throw new Error('Canonical Mission is missing required identity, objective, or provenance.');
|
|
153
|
-
const state = normalizeMissionState(status.state);
|
|
154
|
-
if (status.state !== state)
|
|
155
|
-
throw new Error(`Canonical Mission state '${String(status.state)}' is not normalized.`);
|
|
156
|
-
if (status.terminal !== TERMINAL.has(state))
|
|
157
|
-
throw new Error(`Canonical Mission terminal flag contradicts state '${state}'.`);
|
|
158
|
-
if (!Array.isArray(status.artifacts))
|
|
159
|
-
throw new Error('Canonical Mission artifacts must be an array.');
|
|
160
|
-
return structuredClone(value);
|
|
161
|
-
}
|
|
162
|
-
export function decodeMission(input, source, workerConsumer) {
|
|
163
|
-
const record = object(input);
|
|
164
|
-
assertMissionWorkerHandoff(record, source, workerConsumer);
|
|
165
|
-
if (source === 'canonical') {
|
|
166
|
-
const value = validateCanonicalMission(record);
|
|
167
|
-
return { value, sourceVersion: MISSION_API_VERSION, warnings: [], preservedExtensions: structuredClone(value.extensions ?? {}), lossReport: [] };
|
|
168
|
-
}
|
|
169
|
-
const version = sourceVersion(source, record);
|
|
170
|
-
assertSupported(version);
|
|
171
|
-
const statusRecord = record.status && typeof record.status === 'object' ? record.status : {};
|
|
172
|
-
const nativeState = nativeStateFor(source, record, statusRecord);
|
|
173
|
-
const state = normalizeMissionState(nativeState);
|
|
174
|
-
const objective = string(record.goal) ?? string(record.objective) ?? string(record.title) ?? string(record.prompt) ?? '(legacy mission objective unavailable)';
|
|
175
|
-
const completionCriterion = string(record.completionCriterion) ?? string(record.completion_criterion) ?? string(record.completion);
|
|
176
|
-
const preserved = extensions(record, source);
|
|
177
|
-
const warnings = [];
|
|
178
|
-
const lossReport = [];
|
|
179
|
-
if (objective.startsWith('(legacy'))
|
|
180
|
-
warnings.push('Source did not contain a mission objective; placeholder retained.');
|
|
181
|
-
if (state === 'unknown' && nativeState !== 'unknown')
|
|
182
|
-
warnings.push(`Unknown native state '${nativeState}' preserved beside normalized unknown state.`);
|
|
183
|
-
const sourceArtifacts = artifacts(record);
|
|
184
|
-
if (source === 'uhp-2026-08-11') {
|
|
185
|
-
const ids = new Set(sourceArtifacts.map(artifact => artifact.id));
|
|
186
|
-
for (const artifact of uhpOutputArtifacts(record))
|
|
187
|
-
if (!ids.has(artifact.id))
|
|
188
|
-
sourceArtifacts.push(artifact);
|
|
189
|
-
}
|
|
190
|
-
const budgets = record.budgets && typeof record.budgets === 'object' ? record.budgets : undefined;
|
|
191
|
-
const lineage = Array.isArray(record.lineage) ? record.lineage.flatMap(item => {
|
|
192
|
-
if (!item || typeof item !== 'object')
|
|
193
|
-
return [];
|
|
194
|
-
const entry = item;
|
|
195
|
-
return string(entry.relation) && string(entry.id) ? [{ relation: string(entry.relation), id: string(entry.id) }] : [];
|
|
196
|
-
}) : undefined;
|
|
197
|
-
const value = {
|
|
198
|
-
apiVersion: MISSION_API_VERSION,
|
|
199
|
-
kind: 'Mission',
|
|
200
|
-
metadata: {
|
|
201
|
-
id: requiredId(record),
|
|
202
|
-
...(string(record.createdAt) ?? string(record.created_at) ? { createdAt: string(record.createdAt) ?? string(record.created_at) } : {}),
|
|
203
|
-
...(string(record.updatedAt) ?? string(record.updated_at) ? { updatedAt: string(record.updatedAt) ?? string(record.updated_at) } : {}),
|
|
204
|
-
...(string(record.previous_response_id) ? { previousId: string(record.previous_response_id) } : {}),
|
|
205
|
-
...(lineage?.length ? { lineage } : {}),
|
|
206
|
-
},
|
|
207
|
-
spec: { objective, ...(completionCriterion ? { completionCriterion } : {}), ...(budgets ? { budgets: structuredClone(budgets) } : {}) },
|
|
208
|
-
status: {
|
|
209
|
-
state,
|
|
210
|
-
terminal: TERMINAL.has(state),
|
|
211
|
-
nativeState,
|
|
212
|
-
artifacts: sourceArtifacts,
|
|
213
|
-
...(record.output !== undefined || record.partialOutput !== undefined || record.partial_output !== undefined ? { partialOutput: structuredClone(record.partialOutput ?? record.partial_output ?? record.output) } : {}),
|
|
214
|
-
},
|
|
215
|
-
provenance: { sourceContract: source, sourceVersion: version, ...(source === 'a2a' || source.startsWith('uhp') ? { transport: source.split('-')[0] } : {}), sourceId: requiredId(record) },
|
|
216
|
-
...(Object.keys(preserved).length ? { extensions: { [`aiwg.source.${source}`]: preserved } } : {}),
|
|
217
|
-
};
|
|
218
|
-
return { value, sourceVersion: version, warnings, preservedExtensions: preserved, lossReport };
|
|
219
|
-
}
|
|
220
|
-
function projectionLosses(value, target) {
|
|
221
|
-
const losses = [];
|
|
222
|
-
if (value.status.verification?.length)
|
|
223
|
-
losses.push({ path: '/status/verification', reason: `${target} has no native verification ledger`, severity: 'warning' });
|
|
224
|
-
if (value.metadata.lineage?.length && !['mission-ledger', 'fleet-workload-v1', 'canonical'].includes(target))
|
|
225
|
-
losses.push({ path: '/metadata/lineage', reason: `${target} cannot represent full lineage`, severity: 'required' });
|
|
226
|
-
if (value.spec.budgets && !['fleet-workload-v1', 'canonical'].includes(target))
|
|
227
|
-
losses.push({ path: '/spec/budgets', reason: `${target} cannot represent every canonical budget`, severity: 'warning' });
|
|
228
|
-
return losses;
|
|
229
|
-
}
|
|
230
|
-
export function encodeMission(value, target, options = {}) {
|
|
231
|
-
assertMissionWorkerHandoff(value, target, options.workerConsumer);
|
|
232
|
-
const mission = validateCanonicalMission(value);
|
|
233
|
-
if (target === 'canonical')
|
|
234
|
-
return { value: mission, targetVersion: MISSION_API_VERSION, warnings: [], lossReport: [] };
|
|
235
|
-
const lossReport = projectionLosses(mission, target);
|
|
236
|
-
const required = lossReport.filter(loss => loss.severity === 'required');
|
|
237
|
-
if (required.length && !options.allowLoss)
|
|
238
|
-
throw new Error(`Projection to ${target} would silently lose required semantics: ${required.map(loss => loss.path).join(', ')}.`);
|
|
239
|
-
const base = {
|
|
240
|
-
missionId: mission.metadata.id,
|
|
241
|
-
goal: mission.spec.objective,
|
|
242
|
-
completionCriterion: mission.spec.completionCriterion,
|
|
243
|
-
status: mission.status.nativeState ?? mission.status.state,
|
|
244
|
-
artifacts: structuredClone(mission.status.artifacts),
|
|
245
|
-
partialOutput: structuredClone(mission.status.partialOutput),
|
|
246
|
-
lineage: structuredClone(mission.metadata.lineage ?? []),
|
|
247
|
-
extensions: {
|
|
248
|
-
'aiwg.mission.canonical': { apiVersion: mission.apiVersion, state: mission.status.state, lossReport },
|
|
249
|
-
...(mission.extensions ?? {}),
|
|
250
|
-
},
|
|
251
|
-
};
|
|
252
|
-
let projected = base;
|
|
253
|
-
if (target === 'mission-plan')
|
|
254
|
-
projected = { missionId: base.missionId, goal: base.goal, completionCriterion: base.completionCriterion ?? '', cycles: [], extensions: base.extensions };
|
|
255
|
-
else if (target === 'mission-ledger')
|
|
256
|
-
projected = { ...base, activityLog: [], cycles: [], totalCost: 0, checkpoint: { completed: [], pending: [], failed: [] }, runtimesUsed: [] };
|
|
257
|
-
else if (target === 'uhp-2026-08-11')
|
|
258
|
-
projected = { id: mission.provenance.sourceId ?? mission.metadata.id, object: 'response', created_at: 0, status: base.status, model: '', output: mission.status.partialOutput ?? [], metadata: base.extensions };
|
|
259
|
-
else if (target === 'a2a')
|
|
260
|
-
projected = { id: mission.provenance.sourceId ?? mission.metadata.id, status: { state: base.status }, artifacts: base.artifacts, partialOutput: base.partialOutput, metadata: base.extensions };
|
|
261
|
-
else if (target === 'graph-flow-v1')
|
|
262
|
-
projected = { schemaVersion: 'graph.flow.aiwg.io/v1', runId: mission.metadata.id, nodeState: mission.status.state, metadata: base.extensions };
|
|
263
|
-
else if (target === 'cockpit')
|
|
264
|
-
projected = { id: mission.metadata.id, session_id: mission.metadata.id, source: mission.provenance.transport ?? mission.provenance.sourceContract, title: mission.spec.objective, completion: mission.spec.completionCriterion, status: mission.status.state, artifacts: base.artifacts, partialOutput: base.partialOutput, extensions: base.extensions };
|
|
265
|
-
else if (target === 'activity-v1')
|
|
266
|
-
projected = { event: 'mission.projected', missionId: mission.metadata.id, objective: mission.spec.objective, state: mission.status.state, metadata: base.extensions };
|
|
267
|
-
else if (target === 'executor-v1')
|
|
268
|
-
projected = { mission_id: mission.metadata.id, objective: mission.spec.objective, completion_criterion: mission.spec.completionCriterion, state: base.status, artifacts: base.artifacts, partial_output: base.partialOutput, metadata: base.extensions };
|
|
269
|
-
else if (target === 'fleet-workload-v1')
|
|
270
|
-
projected = { mission_id: mission.metadata.id, objective: mission.spec.objective, completion_criterion: mission.spec.completionCriterion, observed_state: base.status, budgets: mission.spec.budgets, artifacts: base.artifacts, partial_output: base.partialOutput, lineage: base.lineage, metadata: base.extensions };
|
|
271
|
-
else if (target === 'mission-control-session')
|
|
272
|
-
projected = { id: mission.metadata.id, objective: mission.spec.objective, status: mission.status.state, completion: mission.spec.completionCriterion, artifacts: base.artifacts, partialOutput: base.partialOutput, extensions: base.extensions };
|
|
273
|
-
return { value: projected, targetVersion: target, warnings: lossReport.map(loss => `${loss.path}: ${loss.reason}`), lossReport };
|
|
274
|
-
}
|
|
11
|
+
import { WORKER_BASELINE_FEATURES, WORKER_CONTRACT_REVISION, WORKER_DEFINITIONS, WORKER_DEFINITION_PRODUCERS, WORKER_KNOWN_FEATURES, WorkerContractError, } from './types.js';
|
|
12
|
+
export * from './canonical-codecs.js';
|
|
13
|
+
export * from './health-trace-codecs.js';
|
|
275
14
|
let cachedWorkerValidators;
|
|
276
15
|
function missionPackageRoot(start) {
|
|
277
16
|
let current = resolve(start);
|
|
@@ -296,7 +35,7 @@ function loadWorkerValidators() {
|
|
|
296
35
|
const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
|
|
297
36
|
if (typeof schema.$id !== 'string')
|
|
298
37
|
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Worker contract owner has no schema $id.');
|
|
299
|
-
const ajv = addFormats(new Ajv2020({ strict: false, allErrors: true }));
|
|
38
|
+
const ajv = addFormats(new Ajv2020({ strict: true, strictRequired: false, allErrors: true }));
|
|
300
39
|
ajv.addSchema(schema);
|
|
301
40
|
const validators = new Map();
|
|
302
41
|
for (const definition of WORKER_DEFINITIONS)
|
|
@@ -305,14 +44,6 @@ function loadWorkerValidators() {
|
|
|
305
44
|
}
|
|
306
45
|
return cachedWorkerValidators;
|
|
307
46
|
}
|
|
308
|
-
function stableJson(value) {
|
|
309
|
-
if (Array.isArray(value))
|
|
310
|
-
return `[${value.map(stableJson).join(',')}]`;
|
|
311
|
-
if (value && typeof value === 'object') {
|
|
312
|
-
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
313
|
-
}
|
|
314
|
-
return JSON.stringify(value) ?? 'null';
|
|
315
|
-
}
|
|
316
47
|
function workerBundleDigestFor(schema) {
|
|
317
48
|
const defs = schema.$defs;
|
|
318
49
|
const workerDefs = Object.fromEntries(WORKER_DEFINITIONS.filter(definition => definition in defs).map(definition => [definition, defs[definition]]));
|
|
@@ -345,6 +76,10 @@ export function validateWorkerPayload(definition, value) {
|
|
|
345
76
|
if (health.quietSeconds >= health.stallSeconds)
|
|
346
77
|
errors.push('Health quietSeconds must be less than stallSeconds');
|
|
347
78
|
};
|
|
79
|
+
if (definition === 'workerVerification' || definition === 'workerDirectVerification')
|
|
80
|
+
errors.push(...workerReviewEvidenceErrors(value));
|
|
81
|
+
if (definition === 'workerResult' || definition === 'workerDirectResult')
|
|
82
|
+
errors.push(...workerReviewEvidenceErrors(value.verification));
|
|
348
83
|
if (definition === 'workerEffectiveHealth')
|
|
349
84
|
checkHealth(value);
|
|
350
85
|
if (definition === 'workerResolvedInvocation') {
|
|
@@ -410,7 +145,14 @@ export function assembleWorkerResult(input, previous) {
|
|
|
410
145
|
const brief = decodeWorkerProducedPayload(briefDefinition, 'conductor', input.brief);
|
|
411
146
|
const execution = decodeWorkerProducedPayload('workerExecution', 'adapter', input.execution);
|
|
412
147
|
const worker = input.worker === null ? null : decodeWorkerProducedPayload('workerClaims', 'worker', input.worker);
|
|
413
|
-
|
|
148
|
+
// Shape-check before relational checks; telemetry is derived only by trusted assembly.
|
|
149
|
+
const verificationInput = structuredClone(input.verification);
|
|
150
|
+
if (verificationInput.status === 'accepted' && verificationInput.reviewRounds?.length) {
|
|
151
|
+
verificationInput.roundsToClean ??= verificationInput.reviewRounds.length;
|
|
152
|
+
verificationInput.headSha ??= input.headSha;
|
|
153
|
+
}
|
|
154
|
+
const shapedVerification = decodeWorkerProducedPayload(direct ? 'workerDirectVerification' : 'workerVerification', 'verifier', verificationInput);
|
|
155
|
+
const verification = prepareWorkerAcceptance(brief, execution, shapedVerification, input);
|
|
414
156
|
if (stableJson(execution.requested) !== stableJson(brief.selected)) {
|
|
415
157
|
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Adapter requested identity differs from the bound brief.');
|
|
416
158
|
}
|
|
@@ -604,6 +346,9 @@ export function encodeWorkerPayloadSnapshot(definition, value, previous) {
|
|
|
604
346
|
if (prior[identity] !== next[identity])
|
|
605
347
|
throw new WorkerContractError('OG_CONTRACT_INVALID', `Worker successor changes ${identity}.`);
|
|
606
348
|
}
|
|
349
|
+
if (definition === 'workerResult' || definition === 'workerDirectResult') {
|
|
350
|
+
assertWorkerReviewSuccessor(prior.verification, next.verification);
|
|
351
|
+
}
|
|
607
352
|
if (definition === 'workerDispatch') {
|
|
608
353
|
const link = next.previousRecord;
|
|
609
354
|
if (link?.digest !== previous.artifact.digest || link?.ref !== previous.artifact.ref) {
|
|
@@ -615,277 +360,4 @@ export function encodeWorkerPayloadSnapshot(definition, value, previous) {
|
|
|
615
360
|
const digest = `sha256:${createHash('sha256').update(bytes, 'utf8').digest('hex')}`;
|
|
616
361
|
return Object.freeze({ definition, bytes, artifact: Object.freeze({ ref: `artifact:${digest}`, digest }), previousRecord: previous ? Object.freeze({ ...previous.artifact }) : null });
|
|
617
362
|
}
|
|
618
|
-
// S1 health-to-lease and Mission reconciliation mapping (B-OG-54 / #2916,
|
|
619
|
-
// 05 §8, ADR-OG-10/13). Pure functions over validated shapes: no scheduler,
|
|
620
|
-
// store, Mission runtime or managed import. S2 owners (B-OG-12/16) wire these
|
|
621
|
-
// verdicts into actual renew/fence/stop/settle/release runtime.
|
|
622
|
-
const HEALTH_TERMINAL_LIKE = new Set(['completed', 'failed', 'cancelled', 'crashed', 'failed-to-start']);
|
|
623
|
-
/**
|
|
624
|
-
* Classify supervision evidence into a distinct shared-trace kind. Quiet,
|
|
625
|
-
* stalled and uncertain map distinctly. Heartbeat/lease-only observation
|
|
626
|
-
* yields activity-only, never productive work. A crashed or failed-to-start
|
|
627
|
-
* worker without confirmed termination is terminal-unconfirmed: fail-stop is
|
|
628
|
-
* claimed but a live descendant may survive, so it is never stop proof
|
|
629
|
-
* (ADR-OG-10). Precondition: schema-valid workerHealthSummary.
|
|
630
|
-
*/
|
|
631
|
-
export function classifyHealthTrace(health) {
|
|
632
|
-
if (health.termination === 'confirmed' && HEALTH_TERMINAL_LIKE.has(health.state))
|
|
633
|
-
return 'terminal-proof';
|
|
634
|
-
if (HEALTH_TERMINAL_LIKE.has(health.state))
|
|
635
|
-
return 'terminal-unconfirmed';
|
|
636
|
-
// Confirmed termination with a non-terminal state is contradictory owner
|
|
637
|
-
// evidence (finish() never produces it): downgrade, never productive.
|
|
638
|
-
if (health.termination === 'confirmed')
|
|
639
|
-
return 'uncertain';
|
|
640
|
-
if (health.termination === 'uncertain')
|
|
641
|
-
return 'uncertain';
|
|
642
|
-
switch (health.state) {
|
|
643
|
-
case 'launching':
|
|
644
|
-
case 'started':
|
|
645
|
-
return 'setup';
|
|
646
|
-
case 'active':
|
|
647
|
-
return health.lastProgressAt === null ? 'activity-only' : 'productive';
|
|
648
|
-
case 'quiet':
|
|
649
|
-
return 'quiet';
|
|
650
|
-
case 'stalled':
|
|
651
|
-
case 'hung':
|
|
652
|
-
return 'stalled';
|
|
653
|
-
default:
|
|
654
|
-
return 'uncertain';
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
const TRACE_TRUNCATION_MARKER = '…[truncated]';
|
|
658
|
-
/** Strip control/format characters and cap at maxUtf8Bytes (marker included). */
|
|
659
|
-
export function boundTraceText(text, maxUtf8Bytes) {
|
|
660
|
-
const clean = String(text).replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029]/g, '');
|
|
661
|
-
if (maxUtf8Bytes <= 0)
|
|
662
|
-
return '';
|
|
663
|
-
if (Buffer.byteLength(clean, 'utf8') <= maxUtf8Bytes)
|
|
664
|
-
return clean;
|
|
665
|
-
const markerBytes = Buffer.byteLength(TRACE_TRUNCATION_MARKER, 'utf8');
|
|
666
|
-
if (maxUtf8Bytes < markerBytes) {
|
|
667
|
-
// The budget cannot hold even the marker: return the longest
|
|
668
|
-
// character-boundary prefix of the marker that fits, so the output
|
|
669
|
-
// never exceeds the budget.
|
|
670
|
-
let bytes = 0;
|
|
671
|
-
let end = 0;
|
|
672
|
-
for (const char of TRACE_TRUNCATION_MARKER) {
|
|
673
|
-
const size = Buffer.byteLength(char, 'utf8');
|
|
674
|
-
if (bytes + size > maxUtf8Bytes)
|
|
675
|
-
break;
|
|
676
|
-
bytes += size;
|
|
677
|
-
end += char.length;
|
|
678
|
-
}
|
|
679
|
-
return TRACE_TRUNCATION_MARKER.slice(0, end);
|
|
680
|
-
}
|
|
681
|
-
let bytes = 0;
|
|
682
|
-
let end = 0;
|
|
683
|
-
for (const char of clean) {
|
|
684
|
-
const size = Buffer.byteLength(char, 'utf8');
|
|
685
|
-
if (bytes + size > maxUtf8Bytes - markerBytes)
|
|
686
|
-
break;
|
|
687
|
-
bytes += size;
|
|
688
|
-
end += char.length;
|
|
689
|
-
}
|
|
690
|
-
return clean.slice(0, end) + TRACE_TRUNCATION_MARKER;
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Allowlisted reason label for the shared trace. Unlisted codes (including
|
|
694
|
-
* any free text carrying secret material) project as UNCLASSIFIED#digest:
|
|
695
|
-
* stable and bounded, never verbatim.
|
|
696
|
-
*/
|
|
697
|
-
export function healthReasonLabel(reasonCode) {
|
|
698
|
-
if (HEALTH_KNOWN_REASON_CODES.includes(reasonCode))
|
|
699
|
-
return reasonCode;
|
|
700
|
-
const digest = createHash('sha256').update(reasonCode, 'utf8').digest('hex').slice(0, 12);
|
|
701
|
-
return `UNCLASSIFIED#${digest}`;
|
|
702
|
-
}
|
|
703
|
-
/**
|
|
704
|
-
* Digest-only lifecycle reference for the effect ledger. Only enum, counter,
|
|
705
|
-
* digest and timestamp fields enter the digest; no payload content crosses.
|
|
706
|
-
*/
|
|
707
|
-
export function healthLifecycleDigest(health) {
|
|
708
|
-
assertValidWorkerPayload('workerHealthSummary', health);
|
|
709
|
-
const lifecycle = {
|
|
710
|
-
version: health.version,
|
|
711
|
-
state: health.state,
|
|
712
|
-
coverage: health.coverage,
|
|
713
|
-
handshake: health.handshake,
|
|
714
|
-
termination: health.termination,
|
|
715
|
-
confidence: health.confidence,
|
|
716
|
-
observerGeneration: health.observerGeneration,
|
|
717
|
-
transitionCount: health.transitionCount,
|
|
718
|
-
restartCount: health.restartCount,
|
|
719
|
-
lastObservedAt: health.lastObservedAt,
|
|
720
|
-
lastActivityAt: health.lastActivityAt,
|
|
721
|
-
lastProgressAt: health.lastProgressAt,
|
|
722
|
-
};
|
|
723
|
-
const digest = `sha256:${createHash('sha256').update(stableJson(lifecycle), 'utf8').digest('hex')}`;
|
|
724
|
-
return { ref: `artifact:${digest}`, digest };
|
|
725
|
-
}
|
|
726
|
-
/**
|
|
727
|
-
* Closed observed-state vocabulary for the previous-state channel. Free
|
|
728
|
-
* text (including leaked bearer material) never interpolates into the
|
|
729
|
-
* shared trace.
|
|
730
|
-
*/
|
|
731
|
-
const HEALTH_OBSERVED_STATES = new Set([
|
|
732
|
-
'launching', 'started', 'active', 'quiet', 'stalled', 'hung',
|
|
733
|
-
'crashed', 'failed-to-start', 'completed', 'failed', 'cancelled',
|
|
734
|
-
]);
|
|
735
|
-
/**
|
|
736
|
-
* Parse an ISO timestamp into epoch milliseconds for chronological
|
|
737
|
-
* comparison. Comparison must never fall back to string ordering: distinct
|
|
738
|
-
* representations of the same instant (e.g. '.000Z' vs 'Z') must compare
|
|
739
|
-
* equal, not greater/lesser. Unparseable values reject as having no
|
|
740
|
-
* timestamp evidence (null), never throw.
|
|
741
|
-
*/
|
|
742
|
-
function parseProgressTimestampMs(value) {
|
|
743
|
-
if (value === null)
|
|
744
|
-
return null;
|
|
745
|
-
const ms = Date.parse(value);
|
|
746
|
-
return Number.isFinite(ms) ? ms : null;
|
|
747
|
-
}
|
|
748
|
-
/**
|
|
749
|
-
* Fresh-progress gate: credit requires genuinely new progress evidence.
|
|
750
|
-
* The health shape carries no progress sequence/key, so freshness is a
|
|
751
|
-
* NONDECREASING credit watermark: `previousWatermark` is not merely "the
|
|
752
|
-
* last observation" but the highest-ever CREDITED lastProgressAt, compared
|
|
753
|
-
* by parsed epoch milliseconds (never raw strings). Out-of-order or
|
|
754
|
-
* replayed snapshots (older than, or equal to, the watermark) earn no
|
|
755
|
-
* credit and never move the watermark backward; heartbeats (which advance
|
|
756
|
-
* activity/observed timestamps while the watermark is frozen) never
|
|
757
|
-
* re-credit historical progress. Unparseable timestamps earn no credit.
|
|
758
|
-
* Callers thread the watermark forward with `advanceProgressWatermark`.
|
|
759
|
-
*/
|
|
760
|
-
function isFreshProgressEvidence(health, previousWatermark) {
|
|
761
|
-
const currentMs = parseProgressTimestampMs(health.lastProgressAt);
|
|
762
|
-
if (currentMs === null)
|
|
763
|
-
return false;
|
|
764
|
-
if (previousWatermark === null || previousWatermark.lastProgressAt === null)
|
|
765
|
-
return true;
|
|
766
|
-
const watermarkMs = parseProgressTimestampMs(previousWatermark.lastProgressAt);
|
|
767
|
-
if (watermarkMs === null)
|
|
768
|
-
return false;
|
|
769
|
-
return currentMs > watermarkMs;
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Pure watermark reducer (no store): callers thread the returned value into
|
|
773
|
-
* the next observation's `previousHealth`/watermark argument instead of
|
|
774
|
-
* re-passing the raw last observation. The watermark advances ONLY when the
|
|
775
|
-
* current observation was actually credited; a non-credited observation
|
|
776
|
-
* (replay, heartbeat, or an out-of-order/older snapshot) leaves the
|
|
777
|
-
* watermark exactly where it was. This is what makes real A -> B -> A -> B
|
|
778
|
-
* delivery sequences credit true,true,false,false instead of re-crediting
|
|
779
|
-
* B the second time it arrives: a naive caller that re-threads "the last
|
|
780
|
-
* observation" (rather than this reducer's output) regresses to that bug.
|
|
781
|
-
*/
|
|
782
|
-
export function advanceProgressWatermark(previousWatermark, credited, health) {
|
|
783
|
-
if (credited)
|
|
784
|
-
return { lastProgressAt: health.lastProgressAt };
|
|
785
|
-
return previousWatermark ?? { lastProgressAt: null };
|
|
786
|
-
}
|
|
787
|
-
/**
|
|
788
|
-
* Project one health observation into the shared trace. The activity summary
|
|
789
|
-
* is built only from enum/counter/generation fields plus the allowlisted
|
|
790
|
-
* reason label; completionCredit is always false because Mission/Flow
|
|
791
|
-
* completion gates stay authoritative. previousState must be a closed
|
|
792
|
-
* observed state (or null); anything else is rejected, never interpolated.
|
|
793
|
-
* progressCredit requires fresh progress evidence relative to
|
|
794
|
-
* previousHealth, which callers must maintain as the nondecreasing credit
|
|
795
|
-
* watermark (see advanceProgressWatermark) rather than the raw last
|
|
796
|
-
* observation (null for a first observation / no watermark yet).
|
|
797
|
-
*/
|
|
798
|
-
export function projectHealthTrace(health, previousState = null, previousHealth = null) {
|
|
799
|
-
assertValidWorkerPayload('workerHealthSummary', health);
|
|
800
|
-
if (previousState !== null && !HEALTH_OBSERVED_STATES.has(previousState)) {
|
|
801
|
-
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Invalid previous health state.');
|
|
802
|
-
}
|
|
803
|
-
const kind = classifyHealthTrace(health);
|
|
804
|
-
const summary = `health.${health.state}:${health.termination}`
|
|
805
|
-
+ ` gen=${health.observerGeneration ?? 'none'} transitions=${health.transitionCount}`
|
|
806
|
-
+ ` prev=${previousState ?? 'none'} reason=${healthReasonLabel(health.reasonCode)}`;
|
|
807
|
-
return {
|
|
808
|
-
kind,
|
|
809
|
-
progressCredit: kind === 'productive' && isFreshProgressEvidence(health, previousHealth),
|
|
810
|
-
completionCredit: false,
|
|
811
|
-
activitySummary: boundTraceText(summary, HEALTH_ACTIVITY_SUMMARY_MAX_UTF8_BYTES),
|
|
812
|
-
effectRef: healthLifecycleDigest(health),
|
|
813
|
-
};
|
|
814
|
-
}
|
|
815
|
-
/**
|
|
816
|
-
* Derive stop proof from owner-observed supervision evidence (ADR-OG-10).
|
|
817
|
-
* Only a terminal state with owner-confirmed termination is stop proof;
|
|
818
|
-
* terminal-unconfirmed and uncertain observations stay uncertain, and the
|
|
819
|
-
* absence of evidence is none. There is no free-form proof parameter:
|
|
820
|
-
* callers pass the health summary (or null) and the proof is derived.
|
|
821
|
-
* A late final after cancel-disconnect therefore never releases while
|
|
822
|
-
* termination is uncertain (ADR-OG-13: late results stay unaccepted
|
|
823
|
-
* evidence).
|
|
824
|
-
*/
|
|
825
|
-
export function stopProofFromHealth(health) {
|
|
826
|
-
if (health === null)
|
|
827
|
-
return 'none';
|
|
828
|
-
assertValidWorkerPayload('workerHealthSummary', health);
|
|
829
|
-
if (health.termination === 'confirmed') {
|
|
830
|
-
return HEALTH_TERMINAL_LIKE.has(health.state) ? 'confirmed' : 'uncertain';
|
|
831
|
-
}
|
|
832
|
-
if (HEALTH_TERMINAL_LIKE.has(health.state) || health.termination === 'uncertain')
|
|
833
|
-
return 'uncertain';
|
|
834
|
-
return 'none';
|
|
835
|
-
}
|
|
836
|
-
/**
|
|
837
|
-
* The generation the stop-proof evidence actually belongs to, bridging a
|
|
838
|
-
* missing observerGeneration to 0 (the bootstrap generation) for fencing —
|
|
839
|
-
* mirroring the same bridge decideLeaseMapping already applies to the
|
|
840
|
-
* caller-supplied leaseGeneration/observedGeneration fields. This is bound
|
|
841
|
-
* to the health summary's OWN observerGeneration field, which the caller
|
|
842
|
-
* cannot set independently of the evidence: a caller cannot claim generation
|
|
843
|
-
* 3 while attaching generation-2 evidence and have it treated as current.
|
|
844
|
-
*/
|
|
845
|
-
function stopProofGeneration(health) {
|
|
846
|
-
if (health === null)
|
|
847
|
-
return null;
|
|
848
|
-
return health.observerGeneration ?? 0;
|
|
849
|
-
}
|
|
850
|
-
/**
|
|
851
|
-
* Pure renewal/reconcile/release verdict over a mocked scheduler boundary.
|
|
852
|
-
* Fencing binds every action to the current generation: a stale observation
|
|
853
|
-
* denies renew, cancel, settle and accept alike. Expiry or cancel-disconnect
|
|
854
|
-
* without derived confirmed stop proof retains the uncertain reservation
|
|
855
|
-
* and releases nothing. Confirmed stop proof settles exactly once — and only
|
|
856
|
-
* when that proof's OWN generation (not merely the caller-asserted
|
|
857
|
-
* observedGeneration) matches the current leaseGeneration; a confirmed
|
|
858
|
-
* proof carried over from a stale generation never releases.
|
|
859
|
-
*/
|
|
860
|
-
export function decideLeaseMapping(input) {
|
|
861
|
-
let record;
|
|
862
|
-
try {
|
|
863
|
-
record = object(input, 'Lease mapping input');
|
|
864
|
-
}
|
|
865
|
-
catch {
|
|
866
|
-
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Invalid lease mapping input.');
|
|
867
|
-
}
|
|
868
|
-
const leaseStates = ['admitted', 'expired', 'cancelled-disconnected'];
|
|
869
|
-
if (typeof record.leaseState !== 'string' || !leaseStates.includes(record.leaseState)
|
|
870
|
-
|| !Number.isSafeInteger(record.leaseGeneration) || record.leaseGeneration < 0
|
|
871
|
-
|| !Number.isSafeInteger(record.observedGeneration) || record.observedGeneration < 0
|
|
872
|
-
|| typeof record.settled !== 'boolean'
|
|
873
|
-
|| !('health' in record)
|
|
874
|
-
|| (record.health !== null && !validateWorkerPayload('workerHealthSummary', record.health).valid)) {
|
|
875
|
-
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Invalid lease mapping input.');
|
|
876
|
-
}
|
|
877
|
-
const mapping = input;
|
|
878
|
-
if (mapping.observedGeneration !== mapping.leaseGeneration)
|
|
879
|
-
return 'deny-stale';
|
|
880
|
-
if (mapping.settled)
|
|
881
|
-
return 'deny-already-settled';
|
|
882
|
-
const stopProof = stopProofFromHealth(mapping.health);
|
|
883
|
-
const proofIsCurrent = stopProofGeneration(mapping.health) === mapping.leaseGeneration;
|
|
884
|
-
if (stopProof === 'confirmed' && proofIsCurrent)
|
|
885
|
-
return 'release-once';
|
|
886
|
-
if (stopProof === 'uncertain' || (stopProof === 'confirmed' && !proofIsCurrent) || mapping.leaseState !== 'admitted') {
|
|
887
|
-
return 'retain-uncertain';
|
|
888
|
-
}
|
|
889
|
-
return 'renew';
|
|
890
|
-
}
|
|
891
363
|
//# sourceMappingURL=codecs.js.map
|