@aiwg/cli 2026.10.1 → 2026.10.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.
- package/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +210 -4
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +75 -27
- package/dist/src/decision/driver.js +3 -1
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +1 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +21 -3
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +49 -32
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { dirname, join, resolve } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
6
|
+
import addFormats from 'ajv-formats';
|
|
7
|
+
import { HEALTH_ACTIVITY_SUMMARY_MAX_UTF8_BYTES, HEALTH_KNOWN_REASON_CODES, MISSION_API_VERSION, WORKER_BASELINE_FEATURES, WORKER_CONTRACT_REVISION, WORKER_DEFINITIONS, WORKER_DEFINITION_PRODUCERS, WORKER_KNOWN_FEATURES, WorkerContractError, } from './types.js';
|
|
2
8
|
const TERMINAL = new Set(['completed', 'failed', 'incomplete', 'cancelled']);
|
|
3
9
|
function object(value, label = 'Mission input') {
|
|
4
10
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
@@ -132,6 +138,7 @@ function assertSupported(version) {
|
|
|
132
138
|
}
|
|
133
139
|
export function validateCanonicalMission(value) {
|
|
134
140
|
const record = object(value, 'Canonical Mission');
|
|
141
|
+
assertMissionWorkerHandoff(record, 'canonical');
|
|
135
142
|
if (record.apiVersion !== MISSION_API_VERSION)
|
|
136
143
|
throw new Error(`Unsupported canonical Mission version '${String(record.apiVersion)}'.`);
|
|
137
144
|
if (record.kind !== 'Mission')
|
|
@@ -153,6 +160,7 @@ export function validateCanonicalMission(value) {
|
|
|
153
160
|
}
|
|
154
161
|
export function decodeMission(input, source) {
|
|
155
162
|
const record = object(input);
|
|
163
|
+
assertMissionWorkerHandoff(record, source);
|
|
156
164
|
if (source === 'canonical') {
|
|
157
165
|
const value = validateCanonicalMission(record);
|
|
158
166
|
return { value, sourceVersion: MISSION_API_VERSION, warnings: [], preservedExtensions: structuredClone(value.extensions ?? {}), lossReport: [] };
|
|
@@ -219,6 +227,7 @@ function projectionLosses(value, target) {
|
|
|
219
227
|
return losses;
|
|
220
228
|
}
|
|
221
229
|
export function encodeMission(value, target, options = {}) {
|
|
230
|
+
assertMissionWorkerHandoff(value, target);
|
|
222
231
|
const mission = validateCanonicalMission(value);
|
|
223
232
|
if (target === 'canonical')
|
|
224
233
|
return { value: mission, targetVersion: MISSION_API_VERSION, warnings: [], lossReport: [] };
|
|
@@ -262,4 +271,633 @@ export function encodeMission(value, target, options = {}) {
|
|
|
262
271
|
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 };
|
|
263
272
|
return { value: projected, targetVersion: target, warnings: lossReport.map(loss => `${loss.path}: ${loss.reason}`), lossReport };
|
|
264
273
|
}
|
|
274
|
+
let cachedWorkerValidators;
|
|
275
|
+
function missionPackageRoot(start) {
|
|
276
|
+
let current = resolve(start);
|
|
277
|
+
for (;;) {
|
|
278
|
+
try {
|
|
279
|
+
const packageJson = JSON.parse(readFileSync(join(current, 'package.json'), 'utf8'));
|
|
280
|
+
if (packageJson.name === 'aiwg' || packageJson.name === '@aiwg/cli')
|
|
281
|
+
return current;
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
// Keep walking; source and compiled modules have different depths.
|
|
285
|
+
}
|
|
286
|
+
const parent = dirname(current);
|
|
287
|
+
if (parent === current)
|
|
288
|
+
throw new Error('worker contracts: could not locate the aiwg package root');
|
|
289
|
+
current = parent;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function loadWorkerValidators() {
|
|
293
|
+
if (!cachedWorkerValidators) {
|
|
294
|
+
const schemaPath = join(missionPackageRoot(dirname(fileURLToPath(import.meta.url))), 'schemas/mission-v1.schema.json');
|
|
295
|
+
const schema = JSON.parse(readFileSync(schemaPath, 'utf8'));
|
|
296
|
+
if (typeof schema.$id !== 'string')
|
|
297
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Worker contract owner has no schema $id.');
|
|
298
|
+
const ajv = addFormats(new Ajv2020({ strict: false, allErrors: true }));
|
|
299
|
+
ajv.addSchema(schema);
|
|
300
|
+
const validators = new Map();
|
|
301
|
+
for (const definition of WORKER_DEFINITIONS)
|
|
302
|
+
validators.set(definition, ajv.compile({ $ref: `${schema.$id}#/$defs/${definition}` }));
|
|
303
|
+
cachedWorkerValidators = { schemaId: schema.$id, bundleDigest: workerBundleDigestFor(schema), validators };
|
|
304
|
+
}
|
|
305
|
+
return cachedWorkerValidators;
|
|
306
|
+
}
|
|
307
|
+
function stableJson(value) {
|
|
308
|
+
if (Array.isArray(value))
|
|
309
|
+
return `[${value.map(stableJson).join(',')}]`;
|
|
310
|
+
if (value && typeof value === 'object') {
|
|
311
|
+
return `{${Object.keys(value).sort().map(key => `${JSON.stringify(key)}:${stableJson(value[key])}`).join(',')}}`;
|
|
312
|
+
}
|
|
313
|
+
return JSON.stringify(value) ?? 'null';
|
|
314
|
+
}
|
|
315
|
+
function workerBundleDigestFor(schema) {
|
|
316
|
+
const defs = schema.$defs;
|
|
317
|
+
const workerDefs = Object.fromEntries(WORKER_DEFINITIONS.filter(definition => definition in defs).map(definition => [definition, defs[definition]]));
|
|
318
|
+
return `sha256:${createHash('sha256').update(stableJson(workerDefs)).digest('hex')}`;
|
|
319
|
+
}
|
|
320
|
+
/** Digest of the shipped worker definition bundle; consumers advertise it back during negotiation. */
|
|
321
|
+
export function workerDefinitionBundleDigest() {
|
|
322
|
+
return loadWorkerValidators().bundleDigest;
|
|
323
|
+
}
|
|
324
|
+
export function workerContractSchemaId() {
|
|
325
|
+
return loadWorkerValidators().schemaId;
|
|
326
|
+
}
|
|
327
|
+
// 05 §5.1: complete JSON payloads are bounded independently of field limits.
|
|
328
|
+
export const WORKER_MAX_JSON_BYTES = 262_144;
|
|
329
|
+
export const WORKER_MAX_NESTING_DEPTH = 16;
|
|
330
|
+
function workerSemanticErrors(value) {
|
|
331
|
+
const ancestors = new Set();
|
|
332
|
+
function visit(node, depth) {
|
|
333
|
+
if (!node || typeof node !== 'object')
|
|
334
|
+
return undefined;
|
|
335
|
+
if (depth > WORKER_MAX_NESTING_DEPTH)
|
|
336
|
+
return `JSON nesting depth exceeds ${WORKER_MAX_NESTING_DEPTH}`;
|
|
337
|
+
if (ancestors.has(node))
|
|
338
|
+
return 'Worker JSON must not contain cycles';
|
|
339
|
+
ancestors.add(node);
|
|
340
|
+
for (const child of Object.values(node)) {
|
|
341
|
+
const error = visit(child, depth + 1);
|
|
342
|
+
if (error)
|
|
343
|
+
return error;
|
|
344
|
+
}
|
|
345
|
+
ancestors.delete(node);
|
|
346
|
+
return undefined;
|
|
347
|
+
}
|
|
348
|
+
// Root object/array is depth 1; scalar leaves do not add a nesting level.
|
|
349
|
+
const depthError = visit(value, 1);
|
|
350
|
+
if (depthError)
|
|
351
|
+
return [depthError];
|
|
352
|
+
try {
|
|
353
|
+
const bytes = JSON.stringify(value);
|
|
354
|
+
if (bytes === undefined)
|
|
355
|
+
return ['Worker payload must be JSON serializable'];
|
|
356
|
+
if (Buffer.byteLength(bytes, 'utf8') > WORKER_MAX_JSON_BYTES)
|
|
357
|
+
return [`Serialized UTF-8 JSON exceeds ${WORKER_MAX_JSON_BYTES} bytes`];
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
return ['Worker payload must be JSON serializable'];
|
|
361
|
+
}
|
|
362
|
+
return [];
|
|
363
|
+
}
|
|
364
|
+
/** Validate a worker payload against its bundled definition. Closed shapes reject unknown fields. */
|
|
365
|
+
export function validateWorkerPayload(definition, value) {
|
|
366
|
+
const semanticErrors = workerSemanticErrors(value);
|
|
367
|
+
if (semanticErrors.length)
|
|
368
|
+
return { valid: false, errors: semanticErrors };
|
|
369
|
+
const validate = loadWorkerValidators().validators.get(definition);
|
|
370
|
+
if (!validate)
|
|
371
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', `Unknown worker definition '${definition}'.`);
|
|
372
|
+
const valid = validate(value);
|
|
373
|
+
const errors = valid ? [] : (validate.errors ?? []).map(error => `${error.instancePath || '/'} ${error.message ?? error.keyword}`);
|
|
374
|
+
if (valid) {
|
|
375
|
+
const checkHealth = (health) => {
|
|
376
|
+
if (health.checkSeconds > health.startupSeconds)
|
|
377
|
+
errors.push('Health checkSeconds must not exceed startupSeconds');
|
|
378
|
+
if (health.quietSeconds >= health.stallSeconds)
|
|
379
|
+
errors.push('Health quietSeconds must be less than stallSeconds');
|
|
380
|
+
};
|
|
381
|
+
if (definition === 'workerEffectiveHealth')
|
|
382
|
+
checkHealth(value);
|
|
383
|
+
if (definition === 'workerResolvedInvocation') {
|
|
384
|
+
// Legacy and preparatory descriptors may omit health. Preserve that
|
|
385
|
+
// omission in codecs; resolution and observers supply runtime defaults.
|
|
386
|
+
const health = value.health;
|
|
387
|
+
if (health !== undefined)
|
|
388
|
+
checkHealth(health);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
return { valid: valid && errors.length === 0, errors };
|
|
392
|
+
}
|
|
393
|
+
export function assertValidWorkerPayload(definition, value) {
|
|
394
|
+
const { valid, errors } = validateWorkerPayload(definition, value);
|
|
395
|
+
if (!valid)
|
|
396
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', `Invalid ${definition}: ${errors.join('; ') || 'shape mismatch'}.`);
|
|
397
|
+
}
|
|
398
|
+
/** The producer is supplied by trusted calling code, never read from payload data. */
|
|
399
|
+
export function decodeWorkerProducedPayload(definition, producer, value) {
|
|
400
|
+
if (WORKER_DEFINITION_PRODUCERS[definition] !== producer) {
|
|
401
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', `Producer ${producer} cannot author ${definition}.`);
|
|
402
|
+
}
|
|
403
|
+
return decodeWorkerPayload(definition, value);
|
|
404
|
+
}
|
|
405
|
+
export function assertWorkerTerminal(observation) {
|
|
406
|
+
if (!observation || !['shell', 'mcp'].includes(observation.transport)
|
|
407
|
+
|| observation.status !== 'completed' || observation.termination !== 'confirmed'
|
|
408
|
+
|| (observation.transport === 'shell' ? observation.exitCode !== 0 : observation.exitCode !== null && observation.exitCode !== 0)
|
|
409
|
+
|| (observation.isError !== undefined && observation.isError !== false)) {
|
|
410
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Worker terminal evidence is missing, erroneous or uncertain; exit zero alone is insufficient.');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Assemble detached, immutable evidence from separately attributed producers.
|
|
415
|
+
* artifactBytes are read by the conductor from the exact artifact (or protected
|
|
416
|
+
* canonical tree manifest) being verified/integrated, never a worker digest.
|
|
417
|
+
* This is a shared value operation; no Mission/lease/store is needed in direct mode.
|
|
418
|
+
*/
|
|
419
|
+
// Ordered weakest-to-strongest, matching WorkerExecution['identityQuality'] exactly
|
|
420
|
+
// (05 §5.1 / BLOCKER #2890): a required tier is satisfied by that tier or any
|
|
421
|
+
// stronger one. Keep this the single source of truth for both the valid-value
|
|
422
|
+
// check and the ordering comparison below, so a renamed/added quality tier
|
|
423
|
+
// cannot silently resurrect the former `requiredIdentity: 'runtime-configured'`
|
|
424
|
+
// no-op (the option never matched any real `identityQuality` value).
|
|
425
|
+
const IDENTITY_QUALITY_ORDER = ['unavailable', 'runtime-configured-only', 'backend-attested'];
|
|
426
|
+
export function assembleWorkerResult(input, previous) {
|
|
427
|
+
if (input.requiredIdentity !== undefined && !IDENTITY_QUALITY_ORDER.includes(input.requiredIdentity)) {
|
|
428
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Unknown required identity evidence.');
|
|
429
|
+
}
|
|
430
|
+
const direct = 'mode' in input.brief && input.brief.mode === 'direct';
|
|
431
|
+
const briefDefinition = direct ? 'workerDirectBrief' : 'workerBrief';
|
|
432
|
+
const brief = decodeWorkerProducedPayload(briefDefinition, 'conductor', input.brief);
|
|
433
|
+
const execution = decodeWorkerProducedPayload('workerExecution', 'adapter', input.execution);
|
|
434
|
+
const worker = input.worker === null ? null : decodeWorkerProducedPayload('workerClaims', 'worker', input.worker);
|
|
435
|
+
const verification = decodeWorkerProducedPayload(direct ? 'workerDirectVerification' : 'workerVerification', 'verifier', input.verification);
|
|
436
|
+
if (stableJson(execution.requested) !== stableJson(brief.selected)) {
|
|
437
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Adapter requested identity differs from the bound brief.');
|
|
438
|
+
}
|
|
439
|
+
if (verification.status === 'accepted') {
|
|
440
|
+
if (execution.status !== 'completed' || execution.termination !== 'confirmed'
|
|
441
|
+
|| execution.endedAt === null || Date.parse(execution.endedAt) < Date.parse(execution.startedAt) || execution.failure !== null
|
|
442
|
+
|| (execution.exitCode !== null && execution.exitCode !== 0) || worker?.status !== 'completed') {
|
|
443
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Acceptance requires completed claims and confirmed successful execution.');
|
|
444
|
+
}
|
|
445
|
+
if (execution.identityQuality === 'unavailable' || !execution.identityEvidence.length
|
|
446
|
+
|| (input.requiredIdentity !== undefined
|
|
447
|
+
&& IDENTITY_QUALITY_ORDER.indexOf(execution.identityQuality) < IDENTITY_QUALITY_ORDER.indexOf(input.requiredIdentity))
|
|
448
|
+
|| execution.observedModel !== brief.selected.model || execution.observedFamily !== brief.selected.family
|
|
449
|
+
|| stableJson(execution.observedEffort) !== stableJson(brief.selected.effort)) {
|
|
450
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Acceptance requires matching qualified observed identity.');
|
|
451
|
+
}
|
|
452
|
+
if (!(input.artifactBytes instanceof Uint8Array)
|
|
453
|
+
|| `sha256:${createHash('sha256').update(input.artifactBytes).digest('hex')}` !== verification.artifactDigest) {
|
|
454
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Verified artifact digest differs from the exact artifact bytes.');
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
const briefSnapshot = encodeWorkerPayloadSnapshot(briefDefinition, brief);
|
|
458
|
+
return encodeWorkerPayloadSnapshot(direct ? 'workerDirectResult' : 'workerResult', {
|
|
459
|
+
version: WORKER_CONTRACT_REVISION,
|
|
460
|
+
taskId: brief.intent.taskId,
|
|
461
|
+
invocationId: brief.invocationId,
|
|
462
|
+
briefDigest: briefSnapshot.artifact.digest,
|
|
463
|
+
execution, worker, verification,
|
|
464
|
+
...(direct ? { mode: 'direct' } : {}),
|
|
465
|
+
}, previous);
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* Fail-closed negotiation before any task data handoff. Absent or unknown
|
|
469
|
+
* required features, incompatible digests/revisions, or codecs without
|
|
470
|
+
* lossless round trips yield OG_CONTRACT_UNSUPPORTED.
|
|
471
|
+
*/
|
|
472
|
+
export function workerSupportBlockers(consumer, required) {
|
|
473
|
+
const blockers = [];
|
|
474
|
+
if (consumer.revision !== WORKER_CONTRACT_REVISION)
|
|
475
|
+
blockers.push(`unsupported revision ${String(consumer.revision)}; required ${WORKER_CONTRACT_REVISION}`);
|
|
476
|
+
if (consumer.bundleDigest !== required.bundleDigest)
|
|
477
|
+
blockers.push('definition-bundle digest mismatch');
|
|
478
|
+
if (consumer.lossless !== true)
|
|
479
|
+
blockers.push('codec does not guarantee lossless round trips');
|
|
480
|
+
if (!Array.isArray(consumer.features))
|
|
481
|
+
blockers.push('absent consumer feature metadata');
|
|
482
|
+
else {
|
|
483
|
+
for (const feature of required.requiredFeatures) {
|
|
484
|
+
if (!WORKER_KNOWN_FEATURES.includes(feature))
|
|
485
|
+
blockers.push(`unknown required feature '${feature}'`);
|
|
486
|
+
else if (!consumer.features.includes(feature))
|
|
487
|
+
blockers.push(`missing required feature '${feature}'`);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
return blockers;
|
|
491
|
+
}
|
|
492
|
+
export function negotiateWorkerSupport(consumer, required) {
|
|
493
|
+
return workerSupportBlockers(consumer, required).length ? 'OG_CONTRACT_UNSUPPORTED' : 'supported';
|
|
494
|
+
}
|
|
495
|
+
/** Block old consumers before payload handling or spawn. */
|
|
496
|
+
export function assertWorkerConsumerSupported(consumer, required) {
|
|
497
|
+
const blockers = workerSupportBlockers(consumer, required);
|
|
498
|
+
if (blockers.length)
|
|
499
|
+
throw new WorkerContractError('OG_CONTRACT_UNSUPPORTED', `OG_CONTRACT_UNSUPPORTED: ${blockers.join('; ')}.`);
|
|
500
|
+
}
|
|
501
|
+
function gateRequest(gate) {
|
|
502
|
+
return {
|
|
503
|
+
requiredFeatures: [...(gate.requiredFeatures ?? WORKER_BASELINE_FEATURES)],
|
|
504
|
+
bundleDigest: gate.bundleDigest ?? workerDefinitionBundleDigest(),
|
|
505
|
+
};
|
|
506
|
+
}
|
|
507
|
+
/** Trusted registration for the codec only; runtime consumers remain unqualified. */
|
|
508
|
+
function canonicalWorkerSupport() {
|
|
509
|
+
return { revision: WORKER_CONTRACT_REVISION, bundleDigest: workerDefinitionBundleDigest(), features: [...WORKER_KNOWN_FEATURES], lossless: true };
|
|
510
|
+
}
|
|
511
|
+
function workerDispatchRecord(value) {
|
|
512
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
513
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Invalid workerDispatch: must be an object.');
|
|
514
|
+
}
|
|
515
|
+
return value;
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Existing Mission/native owners call this before cloning, projection or launch.
|
|
519
|
+
* Only the canonical codec has qualified worker preservation. An open native
|
|
520
|
+
* extension map never qualifies an executor, renderer or runtime by itself.
|
|
521
|
+
*/
|
|
522
|
+
export function assertMissionWorkerHandoff(input, consumer) {
|
|
523
|
+
const seen = new Set();
|
|
524
|
+
const seenRecords = new Set();
|
|
525
|
+
function visitExtensions(value) {
|
|
526
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || seen.has(value))
|
|
527
|
+
return;
|
|
528
|
+
seen.add(value);
|
|
529
|
+
const record = value;
|
|
530
|
+
if (Object.hasOwn(record, 'aiwg.worker-dispatch')) {
|
|
531
|
+
const dispatch = workerDispatchRecord(record['aiwg.worker-dispatch']);
|
|
532
|
+
if (dispatch.version !== WORKER_CONTRACT_REVISION) {
|
|
533
|
+
throw new WorkerContractError('OG_CONTRACT_UNSUPPORTED', `OG_CONTRACT_UNSUPPORTED: unsupported worker revision ${String(dispatch.version)}.`);
|
|
534
|
+
}
|
|
535
|
+
const support = canonicalWorkerSupport();
|
|
536
|
+
assertWorkerConsumerSupported({ ...support, lossless: consumer === 'canonical', ...(consumer === 'canonical' ? {} : { features: [] }) }, {
|
|
537
|
+
requiredFeatures: Array.isArray(dispatch.requiredFeatures) ? dispatch.requiredFeatures : [...WORKER_BASELINE_FEATURES],
|
|
538
|
+
bundleDigest: workerDefinitionBundleDigest(),
|
|
539
|
+
});
|
|
540
|
+
assertValidWorkerPayload('workerDispatch', dispatch);
|
|
541
|
+
}
|
|
542
|
+
// aiwg.source.* contains preserved native extension maps and records.
|
|
543
|
+
// Other vendor values and output data are opaque, even if their keys
|
|
544
|
+
// happen to match a worker extension name.
|
|
545
|
+
for (const [key, child] of Object.entries(record))
|
|
546
|
+
if (key.startsWith('aiwg.source.')) {
|
|
547
|
+
visitExtensions(child);
|
|
548
|
+
visitRecord(child);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
function visitRecord(value) {
|
|
552
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || seenRecords.has(value))
|
|
553
|
+
return;
|
|
554
|
+
seenRecords.add(value);
|
|
555
|
+
const record = value;
|
|
556
|
+
// All mission-v1 carriers: /extensions and /status/artifacts/*/extensions.
|
|
557
|
+
visitExtensions(record.extensions);
|
|
558
|
+
const status = record.status;
|
|
559
|
+
if (Array.isArray(status?.artifacts))
|
|
560
|
+
for (const artifact of status.artifacts)
|
|
561
|
+
visitRecord(artifact);
|
|
562
|
+
// Native protocols carry extensions in metadata and top-level artifacts.
|
|
563
|
+
visitExtensions(record.metadata);
|
|
564
|
+
if (Array.isArray(record.artifacts))
|
|
565
|
+
for (const artifact of record.artifacts)
|
|
566
|
+
visitRecord(artifact);
|
|
567
|
+
}
|
|
568
|
+
visitRecord(input);
|
|
569
|
+
}
|
|
570
|
+
function assertWorkerPayloadHandoff(definition, value, gate) {
|
|
571
|
+
const consumer = gate?.consumer ?? canonicalWorkerSupport();
|
|
572
|
+
const request = gate ? gateRequest(gate) : { requiredFeatures: [...WORKER_BASELINE_FEATURES], bundleDigest: workerDefinitionBundleDigest() };
|
|
573
|
+
assertWorkerConsumerSupported(consumer, request);
|
|
574
|
+
if (definition === 'workerDispatch') {
|
|
575
|
+
const dispatch = workerDispatchRecord(value);
|
|
576
|
+
if (dispatch.version !== WORKER_CONTRACT_REVISION)
|
|
577
|
+
throw new WorkerContractError('OG_CONTRACT_UNSUPPORTED', 'OG_CONTRACT_UNSUPPORTED: unsupported worker revision.');
|
|
578
|
+
// Payload requirements are authoritative even if a caller supplies a gate
|
|
579
|
+
// with a weaker list. Negotiation cannot be bypassed through options.
|
|
580
|
+
assertWorkerConsumerSupported(consumer, {
|
|
581
|
+
...request,
|
|
582
|
+
requiredFeatures: [...new Set([...request.requiredFeatures, ...(Array.isArray(dispatch.requiredFeatures) ? dispatch.requiredFeatures : [])])],
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
assertValidWorkerPayload(definition, value);
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Validate and losslessly clone a worker payload for transmission. When a
|
|
589
|
+
* consumer gate is absent, the trusted codec registration is used. Unsupported
|
|
590
|
+
* consumers are blocked before validation/cloning. Decoding mirrors encoding:
|
|
591
|
+
* no normalization is applied,
|
|
592
|
+
* so decode(encode(value)) is deep-equal to value.
|
|
593
|
+
*/
|
|
594
|
+
export function encodeWorkerPayload(definition, value, gate) {
|
|
595
|
+
assertWorkerPayloadHandoff(definition, value, gate);
|
|
596
|
+
return structuredClone(value);
|
|
597
|
+
}
|
|
598
|
+
export function decodeWorkerPayload(definition, value, gate) {
|
|
599
|
+
assertWorkerPayloadHandoff(definition, value, gate);
|
|
600
|
+
return structuredClone(value);
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* 05 §5.1 / 06 §7: retain hashed bytes and link a successor rather than rewrite
|
|
604
|
+
* the pending version. Tier 0 callers can keep these in context; Tier 1 owners
|
|
605
|
+
* persist them in existing Mission/Flow/Ralph artifacts (S2+).
|
|
606
|
+
*/
|
|
607
|
+
export function encodeWorkerPayloadSnapshot(definition, value, previous) {
|
|
608
|
+
const encoded = encodeWorkerPayload(definition, value);
|
|
609
|
+
if (previous) {
|
|
610
|
+
if (previous.definition !== definition || `sha256:${createHash('sha256').update(previous.bytes, 'utf8').digest('hex')}` !== previous.artifact.digest) {
|
|
611
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Prior worker snapshot definition or digest does not match its preserved bytes.');
|
|
612
|
+
}
|
|
613
|
+
const prior = decodeWorkerPayload(definition, JSON.parse(previous.bytes));
|
|
614
|
+
const next = encoded;
|
|
615
|
+
for (const identity of ['taskId', 'requestId', 'invocationId', 'briefDigest']) {
|
|
616
|
+
if (prior[identity] !== next[identity])
|
|
617
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', `Worker successor changes ${identity}.`);
|
|
618
|
+
}
|
|
619
|
+
if (definition === 'workerDispatch') {
|
|
620
|
+
const link = next.previousRecord;
|
|
621
|
+
if (link?.digest !== previous.artifact.digest || link?.ref !== previous.artifact.ref) {
|
|
622
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Worker dispatch successor must link previousRecord to its prior snapshot.');
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const bytes = JSON.stringify(encoded);
|
|
627
|
+
const digest = `sha256:${createHash('sha256').update(bytes, 'utf8').digest('hex')}`;
|
|
628
|
+
return Object.freeze({ definition, bytes, artifact: Object.freeze({ ref: `artifact:${digest}`, digest }), previousRecord: previous ? Object.freeze({ ...previous.artifact }) : null });
|
|
629
|
+
}
|
|
630
|
+
// S1 health-to-lease and Mission reconciliation mapping (B-OG-54 / #2916,
|
|
631
|
+
// 05 §8, ADR-OG-10/13). Pure functions over validated shapes: no scheduler,
|
|
632
|
+
// store, Mission runtime or managed import. S2 owners (B-OG-12/16) wire these
|
|
633
|
+
// verdicts into actual renew/fence/stop/settle/release runtime.
|
|
634
|
+
const HEALTH_TERMINAL_LIKE = new Set(['completed', 'failed', 'cancelled', 'crashed', 'failed-to-start']);
|
|
635
|
+
/**
|
|
636
|
+
* Classify supervision evidence into a distinct shared-trace kind. Quiet,
|
|
637
|
+
* stalled and uncertain map distinctly. Heartbeat/lease-only observation
|
|
638
|
+
* yields activity-only, never productive work. A crashed or failed-to-start
|
|
639
|
+
* worker without confirmed termination is terminal-unconfirmed: fail-stop is
|
|
640
|
+
* claimed but a live descendant may survive, so it is never stop proof
|
|
641
|
+
* (ADR-OG-10). Precondition: schema-valid workerHealthSummary.
|
|
642
|
+
*/
|
|
643
|
+
export function classifyHealthTrace(health) {
|
|
644
|
+
if (health.termination === 'confirmed' && HEALTH_TERMINAL_LIKE.has(health.state))
|
|
645
|
+
return 'terminal-proof';
|
|
646
|
+
if (HEALTH_TERMINAL_LIKE.has(health.state))
|
|
647
|
+
return 'terminal-unconfirmed';
|
|
648
|
+
// Confirmed termination with a non-terminal state is contradictory owner
|
|
649
|
+
// evidence (finish() never produces it): downgrade, never productive.
|
|
650
|
+
if (health.termination === 'confirmed')
|
|
651
|
+
return 'uncertain';
|
|
652
|
+
if (health.termination === 'uncertain')
|
|
653
|
+
return 'uncertain';
|
|
654
|
+
switch (health.state) {
|
|
655
|
+
case 'launching':
|
|
656
|
+
case 'started':
|
|
657
|
+
return 'setup';
|
|
658
|
+
case 'active':
|
|
659
|
+
return health.lastProgressAt === null ? 'activity-only' : 'productive';
|
|
660
|
+
case 'quiet':
|
|
661
|
+
return 'quiet';
|
|
662
|
+
case 'stalled':
|
|
663
|
+
case 'hung':
|
|
664
|
+
return 'stalled';
|
|
665
|
+
default:
|
|
666
|
+
return 'uncertain';
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
const TRACE_TRUNCATION_MARKER = '…[truncated]';
|
|
670
|
+
/** Strip control/format characters and cap at maxUtf8Bytes (marker included). */
|
|
671
|
+
export function boundTraceText(text, maxUtf8Bytes) {
|
|
672
|
+
const clean = String(text).replace(/[\u0000-\u001F\u007F-\u009F\u2028\u2029]/g, '');
|
|
673
|
+
if (maxUtf8Bytes <= 0)
|
|
674
|
+
return '';
|
|
675
|
+
if (Buffer.byteLength(clean, 'utf8') <= maxUtf8Bytes)
|
|
676
|
+
return clean;
|
|
677
|
+
const markerBytes = Buffer.byteLength(TRACE_TRUNCATION_MARKER, 'utf8');
|
|
678
|
+
if (maxUtf8Bytes < markerBytes) {
|
|
679
|
+
// The budget cannot hold even the marker: return the longest
|
|
680
|
+
// character-boundary prefix of the marker that fits, so the output
|
|
681
|
+
// never exceeds the budget.
|
|
682
|
+
let bytes = 0;
|
|
683
|
+
let end = 0;
|
|
684
|
+
for (const char of TRACE_TRUNCATION_MARKER) {
|
|
685
|
+
const size = Buffer.byteLength(char, 'utf8');
|
|
686
|
+
if (bytes + size > maxUtf8Bytes)
|
|
687
|
+
break;
|
|
688
|
+
bytes += size;
|
|
689
|
+
end += char.length;
|
|
690
|
+
}
|
|
691
|
+
return TRACE_TRUNCATION_MARKER.slice(0, end);
|
|
692
|
+
}
|
|
693
|
+
let bytes = 0;
|
|
694
|
+
let end = 0;
|
|
695
|
+
for (const char of clean) {
|
|
696
|
+
const size = Buffer.byteLength(char, 'utf8');
|
|
697
|
+
if (bytes + size > maxUtf8Bytes - markerBytes)
|
|
698
|
+
break;
|
|
699
|
+
bytes += size;
|
|
700
|
+
end += char.length;
|
|
701
|
+
}
|
|
702
|
+
return clean.slice(0, end) + TRACE_TRUNCATION_MARKER;
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Allowlisted reason label for the shared trace. Unlisted codes (including
|
|
706
|
+
* any free text carrying secret material) project as UNCLASSIFIED#digest:
|
|
707
|
+
* stable and bounded, never verbatim.
|
|
708
|
+
*/
|
|
709
|
+
export function healthReasonLabel(reasonCode) {
|
|
710
|
+
if (HEALTH_KNOWN_REASON_CODES.includes(reasonCode))
|
|
711
|
+
return reasonCode;
|
|
712
|
+
const digest = createHash('sha256').update(reasonCode, 'utf8').digest('hex').slice(0, 12);
|
|
713
|
+
return `UNCLASSIFIED#${digest}`;
|
|
714
|
+
}
|
|
715
|
+
/**
|
|
716
|
+
* Digest-only lifecycle reference for the effect ledger. Only enum, counter,
|
|
717
|
+
* digest and timestamp fields enter the digest; no payload content crosses.
|
|
718
|
+
*/
|
|
719
|
+
export function healthLifecycleDigest(health) {
|
|
720
|
+
assertValidWorkerPayload('workerHealthSummary', health);
|
|
721
|
+
const lifecycle = {
|
|
722
|
+
version: health.version,
|
|
723
|
+
state: health.state,
|
|
724
|
+
coverage: health.coverage,
|
|
725
|
+
handshake: health.handshake,
|
|
726
|
+
termination: health.termination,
|
|
727
|
+
confidence: health.confidence,
|
|
728
|
+
observerGeneration: health.observerGeneration,
|
|
729
|
+
transitionCount: health.transitionCount,
|
|
730
|
+
restartCount: health.restartCount,
|
|
731
|
+
lastObservedAt: health.lastObservedAt,
|
|
732
|
+
lastActivityAt: health.lastActivityAt,
|
|
733
|
+
lastProgressAt: health.lastProgressAt,
|
|
734
|
+
};
|
|
735
|
+
const digest = `sha256:${createHash('sha256').update(stableJson(lifecycle), 'utf8').digest('hex')}`;
|
|
736
|
+
return { ref: `artifact:${digest}`, digest };
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* Closed observed-state vocabulary for the previous-state channel. Free
|
|
740
|
+
* text (including leaked bearer material) never interpolates into the
|
|
741
|
+
* shared trace.
|
|
742
|
+
*/
|
|
743
|
+
const HEALTH_OBSERVED_STATES = new Set([
|
|
744
|
+
'launching', 'started', 'active', 'quiet', 'stalled', 'hung',
|
|
745
|
+
'crashed', 'failed-to-start', 'completed', 'failed', 'cancelled',
|
|
746
|
+
]);
|
|
747
|
+
/**
|
|
748
|
+
* Parse an ISO timestamp into epoch milliseconds for chronological
|
|
749
|
+
* comparison. Comparison must never fall back to string ordering: distinct
|
|
750
|
+
* representations of the same instant (e.g. '.000Z' vs 'Z') must compare
|
|
751
|
+
* equal, not greater/lesser. Unparseable values reject as having no
|
|
752
|
+
* timestamp evidence (null), never throw.
|
|
753
|
+
*/
|
|
754
|
+
function parseProgressTimestampMs(value) {
|
|
755
|
+
if (value === null)
|
|
756
|
+
return null;
|
|
757
|
+
const ms = Date.parse(value);
|
|
758
|
+
return Number.isFinite(ms) ? ms : null;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Fresh-progress gate: credit requires genuinely new progress evidence.
|
|
762
|
+
* The health shape carries no progress sequence/key, so freshness is a
|
|
763
|
+
* NONDECREASING credit watermark: `previousWatermark` is not merely "the
|
|
764
|
+
* last observation" but the highest-ever CREDITED lastProgressAt, compared
|
|
765
|
+
* by parsed epoch milliseconds (never raw strings). Out-of-order or
|
|
766
|
+
* replayed snapshots (older than, or equal to, the watermark) earn no
|
|
767
|
+
* credit and never move the watermark backward; heartbeats (which advance
|
|
768
|
+
* activity/observed timestamps while the watermark is frozen) never
|
|
769
|
+
* re-credit historical progress. Unparseable timestamps earn no credit.
|
|
770
|
+
* Callers thread the watermark forward with `advanceProgressWatermark`.
|
|
771
|
+
*/
|
|
772
|
+
function isFreshProgressEvidence(health, previousWatermark) {
|
|
773
|
+
const currentMs = parseProgressTimestampMs(health.lastProgressAt);
|
|
774
|
+
if (currentMs === null)
|
|
775
|
+
return false;
|
|
776
|
+
if (previousWatermark === null || previousWatermark.lastProgressAt === null)
|
|
777
|
+
return true;
|
|
778
|
+
const watermarkMs = parseProgressTimestampMs(previousWatermark.lastProgressAt);
|
|
779
|
+
if (watermarkMs === null)
|
|
780
|
+
return false;
|
|
781
|
+
return currentMs > watermarkMs;
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Pure watermark reducer (no store): callers thread the returned value into
|
|
785
|
+
* the next observation's `previousHealth`/watermark argument instead of
|
|
786
|
+
* re-passing the raw last observation. The watermark advances ONLY when the
|
|
787
|
+
* current observation was actually credited; a non-credited observation
|
|
788
|
+
* (replay, heartbeat, or an out-of-order/older snapshot) leaves the
|
|
789
|
+
* watermark exactly where it was. This is what makes real A -> B -> A -> B
|
|
790
|
+
* delivery sequences credit true,true,false,false instead of re-crediting
|
|
791
|
+
* B the second time it arrives: a naive caller that re-threads "the last
|
|
792
|
+
* observation" (rather than this reducer's output) regresses to that bug.
|
|
793
|
+
*/
|
|
794
|
+
export function advanceProgressWatermark(previousWatermark, credited, health) {
|
|
795
|
+
if (credited)
|
|
796
|
+
return { lastProgressAt: health.lastProgressAt };
|
|
797
|
+
return previousWatermark ?? { lastProgressAt: null };
|
|
798
|
+
}
|
|
799
|
+
/**
|
|
800
|
+
* Project one health observation into the shared trace. The activity summary
|
|
801
|
+
* is built only from enum/counter/generation fields plus the allowlisted
|
|
802
|
+
* reason label; completionCredit is always false because Mission/Flow
|
|
803
|
+
* completion gates stay authoritative. previousState must be a closed
|
|
804
|
+
* observed state (or null); anything else is rejected, never interpolated.
|
|
805
|
+
* progressCredit requires fresh progress evidence relative to
|
|
806
|
+
* previousHealth, which callers must maintain as the nondecreasing credit
|
|
807
|
+
* watermark (see advanceProgressWatermark) rather than the raw last
|
|
808
|
+
* observation (null for a first observation / no watermark yet).
|
|
809
|
+
*/
|
|
810
|
+
export function projectHealthTrace(health, previousState = null, previousHealth = null) {
|
|
811
|
+
assertValidWorkerPayload('workerHealthSummary', health);
|
|
812
|
+
if (previousState !== null && !HEALTH_OBSERVED_STATES.has(previousState)) {
|
|
813
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Invalid previous health state.');
|
|
814
|
+
}
|
|
815
|
+
const kind = classifyHealthTrace(health);
|
|
816
|
+
const summary = `health.${health.state}:${health.termination}`
|
|
817
|
+
+ ` gen=${health.observerGeneration ?? 'none'} transitions=${health.transitionCount}`
|
|
818
|
+
+ ` prev=${previousState ?? 'none'} reason=${healthReasonLabel(health.reasonCode)}`;
|
|
819
|
+
return {
|
|
820
|
+
kind,
|
|
821
|
+
progressCredit: kind === 'productive' && isFreshProgressEvidence(health, previousHealth),
|
|
822
|
+
completionCredit: false,
|
|
823
|
+
activitySummary: boundTraceText(summary, HEALTH_ACTIVITY_SUMMARY_MAX_UTF8_BYTES),
|
|
824
|
+
effectRef: healthLifecycleDigest(health),
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Derive stop proof from owner-observed supervision evidence (ADR-OG-10).
|
|
829
|
+
* Only a terminal state with owner-confirmed termination is stop proof;
|
|
830
|
+
* terminal-unconfirmed and uncertain observations stay uncertain, and the
|
|
831
|
+
* absence of evidence is none. There is no free-form proof parameter:
|
|
832
|
+
* callers pass the health summary (or null) and the proof is derived.
|
|
833
|
+
* A late final after cancel-disconnect therefore never releases while
|
|
834
|
+
* termination is uncertain (ADR-OG-13: late results stay unaccepted
|
|
835
|
+
* evidence).
|
|
836
|
+
*/
|
|
837
|
+
export function stopProofFromHealth(health) {
|
|
838
|
+
if (health === null)
|
|
839
|
+
return 'none';
|
|
840
|
+
assertValidWorkerPayload('workerHealthSummary', health);
|
|
841
|
+
if (health.termination === 'confirmed') {
|
|
842
|
+
return HEALTH_TERMINAL_LIKE.has(health.state) ? 'confirmed' : 'uncertain';
|
|
843
|
+
}
|
|
844
|
+
if (HEALTH_TERMINAL_LIKE.has(health.state) || health.termination === 'uncertain')
|
|
845
|
+
return 'uncertain';
|
|
846
|
+
return 'none';
|
|
847
|
+
}
|
|
848
|
+
/**
|
|
849
|
+
* The generation the stop-proof evidence actually belongs to, bridging a
|
|
850
|
+
* missing observerGeneration to 0 (the bootstrap generation) for fencing —
|
|
851
|
+
* mirroring the same bridge decideLeaseMapping already applies to the
|
|
852
|
+
* caller-supplied leaseGeneration/observedGeneration fields. This is bound
|
|
853
|
+
* to the health summary's OWN observerGeneration field, which the caller
|
|
854
|
+
* cannot set independently of the evidence: a caller cannot claim generation
|
|
855
|
+
* 3 while attaching generation-2 evidence and have it treated as current.
|
|
856
|
+
*/
|
|
857
|
+
function stopProofGeneration(health) {
|
|
858
|
+
if (health === null)
|
|
859
|
+
return null;
|
|
860
|
+
return health.observerGeneration ?? 0;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* Pure renewal/reconcile/release verdict over a mocked scheduler boundary.
|
|
864
|
+
* Fencing binds every action to the current generation: a stale observation
|
|
865
|
+
* denies renew, cancel, settle and accept alike. Expiry or cancel-disconnect
|
|
866
|
+
* without derived confirmed stop proof retains the uncertain reservation
|
|
867
|
+
* and releases nothing. Confirmed stop proof settles exactly once — and only
|
|
868
|
+
* when that proof's OWN generation (not merely the caller-asserted
|
|
869
|
+
* observedGeneration) matches the current leaseGeneration; a confirmed
|
|
870
|
+
* proof carried over from a stale generation never releases.
|
|
871
|
+
*/
|
|
872
|
+
export function decideLeaseMapping(input) {
|
|
873
|
+
let record;
|
|
874
|
+
try {
|
|
875
|
+
record = object(input, 'Lease mapping input');
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Invalid lease mapping input.');
|
|
879
|
+
}
|
|
880
|
+
const leaseStates = ['admitted', 'expired', 'cancelled-disconnected'];
|
|
881
|
+
if (typeof record.leaseState !== 'string' || !leaseStates.includes(record.leaseState)
|
|
882
|
+
|| !Number.isSafeInteger(record.leaseGeneration) || record.leaseGeneration < 0
|
|
883
|
+
|| !Number.isSafeInteger(record.observedGeneration) || record.observedGeneration < 0
|
|
884
|
+
|| typeof record.settled !== 'boolean'
|
|
885
|
+
|| !('health' in record)
|
|
886
|
+
|| (record.health !== null && !validateWorkerPayload('workerHealthSummary', record.health).valid)) {
|
|
887
|
+
throw new WorkerContractError('OG_CONTRACT_INVALID', 'Invalid lease mapping input.');
|
|
888
|
+
}
|
|
889
|
+
const mapping = input;
|
|
890
|
+
if (mapping.observedGeneration !== mapping.leaseGeneration)
|
|
891
|
+
return 'deny-stale';
|
|
892
|
+
if (mapping.settled)
|
|
893
|
+
return 'deny-already-settled';
|
|
894
|
+
const stopProof = stopProofFromHealth(mapping.health);
|
|
895
|
+
const proofIsCurrent = stopProofGeneration(mapping.health) === mapping.leaseGeneration;
|
|
896
|
+
if (stopProof === 'confirmed' && proofIsCurrent)
|
|
897
|
+
return 'release-once';
|
|
898
|
+
if (stopProof === 'uncertain' || (stopProof === 'confirmed' && !proofIsCurrent) || mapping.leaseState !== 'admitted') {
|
|
899
|
+
return 'retain-uncertain';
|
|
900
|
+
}
|
|
901
|
+
return 'renew';
|
|
902
|
+
}
|
|
265
903
|
//# sourceMappingURL=codecs.js.map
|