@aiwg/cli 2026.9.20 → 2026.9.21
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 +58 -6
- package/agentic/code/providers/model-capabilities.v1.json +34 -0
- package/agentic/code/providers/model-catalog.v1.json +55 -13
- package/bin/aiwg.mjs +2 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/enrichment/cli.js +1 -1
- package/dist/src/artifacts/views/cli.js +1 -1
- package/dist/src/audit/operator-decision.js +10 -0
- package/dist/src/auth/credential-store.js +127 -27
- package/dist/src/catalog/builtin-models.json +272 -40
- package/dist/src/catalog/cli.js +1 -1
- package/dist/src/catalog/cli.mjs +1 -1
- package/dist/src/cli/handlers/effect.js +872 -0
- package/dist/src/cli/handlers/help.js +2 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/models.js +41 -10
- package/dist/src/cli/handlers/ralph.js +1 -1
- package/dist/src/cli/handlers/sessions.js +113 -37
- package/dist/src/cli/handlers/subcommands.js +31 -0
- package/dist/src/cli/handlers/use.js +46 -9
- package/dist/src/cli/handlers/utilities.js +10 -0
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/scope-resolver.js +112 -9
- package/dist/src/cli/services/deployment-verification.js +5 -0
- package/dist/src/config/aiwg-config.js +3 -0
- package/dist/src/decision/acceptance-rollout.js +93 -0
- package/dist/src/decision/acceptance.js +175 -0
- package/dist/src/decision/adapters/jev.js +234 -13
- package/dist/src/decision/adapters/llm-subagent.js +60 -12
- package/dist/src/decision/admission.js +550 -0
- package/dist/src/decision/batch-benchmark.js +47 -0
- package/dist/src/decision/batch-receipts/accounting.js +75 -0
- package/dist/src/decision/batch-receipts/export.js +40 -0
- package/dist/src/decision/batch-receipts/index.js +9 -0
- package/dist/src/decision/batch-receipts/protection.js +40 -0
- package/dist/src/decision/batch-receipts/receipt.js +44 -0
- package/dist/src/decision/batch-receipts/result-store.js +341 -0
- package/dist/src/decision/batch-receipts/store.js +362 -0
- package/dist/src/decision/batch-receipts/types.js +2 -0
- package/dist/src/decision/batch-receipts/validate.js +127 -0
- package/dist/src/decision/batch.js +68 -0
- package/dist/src/decision/calibration/index.js +4 -0
- package/dist/src/decision/calibration/receipts.js +130 -0
- package/dist/src/decision/calibration/registry.js +258 -0
- package/dist/src/decision/calibration/types.js +2 -0
- package/dist/src/decision/compile-cache/benchmark.js +62 -0
- package/dist/src/decision/compile-cache/compatibility.js +65 -0
- package/dist/src/decision/compile-cache/file-store.js +280 -0
- package/dist/src/decision/compile-cache/identity.js +26 -0
- package/dist/src/decision/compile-cache/index.js +9 -0
- package/dist/src/decision/compile-cache/prefix.js +21 -0
- package/dist/src/decision/compile-cache/records.js +86 -0
- package/dist/src/decision/compile-cache/runtime.js +68 -0
- package/dist/src/decision/compile-cache/store.js +108 -0
- package/dist/src/decision/compile-cache/types.js +2 -0
- package/dist/src/decision/context-plan.js +234 -0
- package/dist/src/decision/context-qualification.js +60 -0
- package/dist/src/decision/ensemble/aggregate.js +215 -0
- package/dist/src/decision/ensemble/contract.js +316 -0
- package/dist/src/decision/ensemble/drift.js +51 -0
- package/dist/src/decision/ensemble/index.js +6 -0
- package/dist/src/decision/ensemble/report.js +71 -0
- package/dist/src/decision/ensemble/types.js +2 -0
- package/dist/src/decision/evaluate.js +1024 -78
- package/dist/src/decision/file-lifecycle-store.js +168 -0
- package/dist/src/decision/graph-beam.js +40 -0
- package/dist/src/decision/graph-budget.js +110 -0
- package/dist/src/decision/graph-decision-bridge.js +93 -0
- package/dist/src/decision/graph-evidence.js +123 -0
- package/dist/src/decision/graph-flow-adapter.js +56 -0
- package/dist/src/decision/graph-flow.js +105 -0
- package/dist/src/decision/graph-parallel.js +79 -0
- package/dist/src/decision/graph-patterns.js +20 -0
- package/dist/src/decision/graph-run-store.js +66 -0
- package/dist/src/decision/graph-run.js +166 -0
- package/dist/src/decision/graph-skill-bridge.js +91 -0
- package/dist/src/decision/graph-templates.js +56 -0
- package/dist/src/decision/graph.js +115 -0
- package/dist/src/decision/index.js +35 -0
- package/dist/src/decision/job-accounting.js +93 -0
- package/dist/src/decision/job-contract.js +186 -0
- package/dist/src/decision/job-effects.js +95 -0
- package/dist/src/decision/job-evaluate.js +80 -0
- package/dist/src/decision/job-gateway.js +121 -0
- package/dist/src/decision/job-http.js +194 -0
- package/dist/src/decision/job-payload-store.js +258 -0
- package/dist/src/decision/job-poll-limiter.js +92 -0
- package/dist/src/decision/job-quota.js +158 -0
- package/dist/src/decision/job-runtime.js +240 -0
- package/dist/src/decision/job-scheduler.js +26 -0
- package/dist/src/decision/job-service.js +117 -0
- package/dist/src/decision/job-store.js +264 -0
- package/dist/src/decision/job-worker.js +141 -0
- package/dist/src/decision/lifecycle.js +148 -0
- package/dist/src/decision/patterns/artifacts.js +173 -0
- package/dist/src/decision/patterns/catalog.js +108 -0
- package/dist/src/decision/patterns/durable-review.js +237 -0
- package/dist/src/decision/patterns/index.js +8 -0
- package/dist/src/decision/patterns/playground.js +401 -0
- package/dist/src/decision/patterns/recorded.js +43 -0
- package/dist/src/decision/patterns/specs.js +174 -0
- package/dist/src/decision/patterns/types.js +2 -0
- package/dist/src/decision/portable-secrets.js +53 -0
- package/dist/src/decision/projection.js +210 -0
- package/dist/src/decision/qualification/capture.js +67 -0
- package/dist/src/decision/qualification/drift.js +68 -0
- package/dist/src/decision/qualification/gate-evidence.js +172 -0
- package/dist/src/decision/qualification/gates.js +83 -0
- package/dist/src/decision/qualification/index.js +12 -0
- package/dist/src/decision/qualification/load.js +275 -0
- package/dist/src/decision/qualification/manifest.js +37 -0
- package/dist/src/decision/qualification/privacy.js +41 -0
- package/dist/src/decision/qualification/quality.js +213 -0
- package/dist/src/decision/qualification/release.js +116 -0
- package/dist/src/decision/qualification/runner.js +408 -0
- package/dist/src/decision/qualification/types.js +2 -0
- package/dist/src/decision/receipts.js +30 -7
- package/dist/src/decision/result-cache/index.js +6 -0
- package/dist/src/decision/result-cache/integrity.js +28 -0
- package/dist/src/decision/result-cache/key.js +28 -0
- package/dist/src/decision/result-cache/service.js +92 -0
- package/dist/src/decision/result-cache/store.js +327 -0
- package/dist/src/decision/result-cache/types.js +3 -0
- package/dist/src/decision/review/authorization.js +110 -0
- package/dist/src/decision/review/effect-ledger-adapter.js +321 -0
- package/dist/src/decision/review/index.js +12 -0
- package/dist/src/decision/review/migrate.js +34 -0
- package/dist/src/decision/review/operator-audit.js +59 -0
- package/dist/src/decision/review/recovery-adapters.js +252 -0
- package/dist/src/decision/review/recovery.js +33 -0
- package/dist/src/decision/review/service.js +498 -0
- package/dist/src/decision/review/store.js +219 -0
- package/dist/src/decision/review/types.js +2 -0
- package/dist/src/decision/review/validate.js +205 -0
- package/dist/src/decision/scheduler.js +115 -0
- package/dist/src/decision/telemetry/context.js +42 -0
- package/dist/src/decision/telemetry/debug-sidecar.js +152 -0
- package/dist/src/decision/telemetry/exporter.js +77 -0
- package/dist/src/decision/telemetry/file-debug-backend.js +112 -0
- package/dist/src/decision/telemetry/index.js +13 -0
- package/dist/src/decision/telemetry/mapping.js +146 -0
- package/dist/src/decision/telemetry/metrics.js +164 -0
- package/dist/src/decision/telemetry/otlp-http.js +183 -0
- package/dist/src/decision/telemetry/redaction.js +93 -0
- package/dist/src/decision/telemetry/retention.js +109 -0
- package/dist/src/decision/telemetry/runtime.js +310 -0
- package/dist/src/decision/telemetry/trace.js +158 -0
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/validate.js +135 -5
- package/dist/src/effects/checkpoint-sinks.js +94 -0
- package/dist/src/effects/checkpoint.js +76 -0
- package/dist/src/effects/decision-links.js +54 -0
- package/dist/src/effects/errors.js +48 -0
- package/dist/src/effects/identity.js +178 -0
- package/dist/src/effects/index.js +25 -0
- package/dist/src/effects/keyring.js +160 -0
- package/dist/src/effects/keys.js +130 -0
- package/dist/src/effects/ledger.js +435 -0
- package/dist/src/effects/lifecycle.js +110 -0
- package/dist/src/effects/lock-recovery.js +269 -0
- package/dist/src/effects/reader.js +103 -0
- package/dist/src/effects/records.js +114 -0
- package/dist/src/effects/redaction.js +51 -0
- package/dist/src/effects/schema.js +73 -0
- package/dist/src/effects/store.js +239 -0
- package/dist/src/effects/types.js +49 -0
- package/dist/src/effects/verifiers/decision.js +168 -0
- package/dist/src/effects/verifiers/file.js +106 -0
- package/dist/src/effects/verifiers/git.js +206 -0
- package/dist/src/effects/verifiers/index.js +90 -0
- package/dist/src/effects/verifiers/registry.js +148 -0
- package/dist/src/effects/verifiers/review.js +123 -0
- package/dist/src/effects/verifiers/tracker.js +299 -0
- package/dist/src/effects/verifiers/types.js +21 -0
- package/dist/src/effects/verify.js +295 -0
- package/dist/src/evidence/bundle.js +2 -0
- package/dist/src/extensions/commands/definitions.js +28 -0
- package/dist/src/extensions/manifest.js +1 -0
- package/dist/src/marketplace/provenance.js +25 -27
- package/dist/src/mcp/cli.mjs +11 -1
- package/dist/src/mcp/grok-build-config.mjs +189 -0
- package/dist/src/mcp/registry.js +21 -1
- package/dist/src/mcp/registry.mjs +20 -1
- package/dist/src/models/grok-build-models.js +356 -0
- package/dist/src/models/index.js +1 -0
- package/dist/src/models/model-capabilities.v1.json +34 -0
- package/dist/src/models/model-catalog.v1.json +55 -13
- package/dist/src/models/model-discovery.js +41 -5
- package/dist/src/models/provider-policy.js +3 -2
- package/dist/src/plugin/skill-command-translator.js +1 -0
- package/dist/src/providers/capability-matrix.yaml +58 -6
- package/dist/src/providers/grok-build-acp.js +192 -0
- package/dist/src/providers/grok-build-dispatch.js +251 -0
- package/dist/src/providers/grok-build-headless.js +170 -0
- package/dist/src/providers/muse-paths.js +105 -0
- package/dist/src/providers/provider-definitions.js +93 -7
- package/dist/src/providers/provider-definitions.mjs +21 -1
- package/dist/src/providers/provider-inventory.js +3 -0
- package/dist/src/security/artifact-attestation.js +4 -8
- package/dist/src/security/signing.js +119 -0
- package/dist/src/sessions/adapters/grok-build.js +187 -0
- package/dist/src/sessions/adapters/muse.js +412 -0
- package/dist/src/sessions/contracts.js +1 -1
- package/dist/src/sessions/grok-build-cli.js +196 -0
- package/dist/src/sessions/index.js +3 -0
- package/dist/src/sessions/workspace-discovery.js +9 -5
- package/dist/src/skills/deployer.js +33 -5
- package/dist/src/smiths/context-pipeline/claude-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/context-file-safety.js +18 -0
- package/dist/src/smiths/context-pipeline/finalization.js +1 -32
- package/dist/src/smiths/context-pipeline/generator.js +12 -0
- package/dist/src/smiths/context-pipeline/index.js +1 -1
- package/dist/src/smiths/context-pipeline/managed-hook.js +2 -0
- package/dist/src/smiths/context-pipeline/muse-bridge.js +50 -0
- package/dist/src/smiths/context-pipeline/provider-policy.js +5 -0
- package/dist/src/smiths/context-pipeline/workspace-context.js +22 -16
- package/dist/src/smiths/hook-bridge/grok-build-translator.js +64 -0
- package/dist/src/smiths/hook-bridge/index.js +3 -0
- package/dist/src/smiths/hook-bridge/shim.js +2 -0
- package/dist/src/smiths/hook-bridge/types.js +5 -0
- package/dist/src/smiths/skillsmith/collision-detector.js +1 -0
- package/dist/src/storage/protected-files.js +102 -0
- package/dist/src/tracker/forge-http.js +371 -0
- package/dist/src/tracker/project-inputs.js +41 -0
- package/package.json +2 -1
- package/schemas/decision/BatchReceipt.v1.schema.json +50 -0
- package/schemas/decision/CalibrationArtifact.v1.schema.json +76 -0
- package/schemas/decision/CalibrationCompatibility.v1.schema.json +18 -0
- package/schemas/decision/CalibrationGovernanceReceipt.v1.schema.json +44 -0
- package/schemas/decision/DecisionAdmissionEvidence.v1.schema.json +43 -0
- package/schemas/decision/DecisionBinding.schema.json +309 -0
- package/schemas/decision/DecisionBinding.v1alpha2.schema.json +369 -0
- package/schemas/decision/DecisionCacheTelemetry.v1.schema.json +20 -0
- package/schemas/decision/DecisionChampionChallenger.v1.schema.json +100 -0
- package/schemas/decision/DecisionCompileCacheEntry.v1.schema.json +35 -0
- package/schemas/decision/DecisionCompileCacheIdentity.v1.schema.json +26 -0
- package/schemas/decision/DecisionCompileCacheTombstone.v1.schema.json +31 -0
- package/schemas/decision/DecisionContextEvidence.v1.schema.json +136 -0
- package/schemas/decision/DecisionDefinition.schema.json +159 -0
- package/schemas/decision/DecisionDefinition.v1alpha2.schema.json +203 -0
- package/schemas/decision/DecisionDriftResponse.v1.schema.json +55 -0
- package/schemas/decision/DecisionEnsembleAggregate.v1.schema.json +86 -0
- package/schemas/decision/DecisionEnsembleIntegrityReport.v1.schema.json +64 -0
- package/schemas/decision/DecisionEnsemblePolicy.v1.schema.json +130 -0
- package/schemas/decision/DecisionGraph.v1.schema.json +77 -0
- package/schemas/decision/DecisionJob.v1.schema.json +71 -0
- package/schemas/decision/DecisionLifecyclePolicy.v1.schema.json +52 -0
- package/schemas/decision/DecisionProjectionPolicy.v1.schema.json +65 -0
- package/schemas/decision/DecisionProviderPrefixCompatibility.v1.schema.json +45 -0
- package/schemas/decision/DecisionProviderPrefixEvidence.v1.schema.json +17 -0
- package/schemas/decision/DecisionProviderPrefixIdentity.v1.schema.json +36 -0
- package/schemas/decision/DecisionResult.schema.json +524 -0
- package/schemas/decision/DecisionResult.v1alpha2.schema.json +619 -0
- package/schemas/decision/DecisionResultCacheEntry.v1.schema.json +53 -0
- package/schemas/decision/DecisionReview.schema.json +29 -0
- package/schemas/decision/DecisionRuleset.schema.json +313 -0
- package/schemas/decision/DecisionRuleset.v1alpha2.schema.json +313 -0
- package/schemas/decision/DecisionTelemetryTrace.v1.schema.json +95 -0
- package/schemas/decision/RulesetResult.schema.json +721 -0
- package/schemas/decision/RulesetResult.v1alpha2.schema.json +891 -0
- package/schemas/security/pr-evidence-receipt.v1.schema.json +77 -0
- package/schemas/security/threat-assessment-report.v1.schema.json +71 -1
- package/tools/agents/deploy-agents.mjs +21 -6
- package/tools/agents/providers/base.mjs +7 -4
- package/tools/agents/providers/codex.mjs +1 -1
- package/tools/agents/providers/grok-build.d.mts +7 -0
- package/tools/agents/providers/grok-build.mjs +249 -9
- package/tools/agents/providers/model-role.mjs +6 -3
- package/tools/agents/providers/muse-hooks.mjs +785 -0
- package/tools/agents/providers/muse-paths.mjs +112 -0
- package/tools/agents/providers/muse.mjs +336 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { lstat, mkdir, readFile, rm } from 'node:fs/promises';
|
|
3
|
+
import { acquireDirectoryLock } from '../artifacts/prebuilt-build-lock.js';
|
|
4
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
5
|
+
import { canonicalizeInitialJob, FileJobStore, JobConflictError } from './job-store.js';
|
|
6
|
+
const quantities = ['queued', 'running', 'retainedItems', 'retainedBytes', 'tokens', 'costMicros', 'calls', 'jobs'];
|
|
7
|
+
/** Explicit operator-gated local recovery; never steal a live or unverified lock. */
|
|
8
|
+
export async function recoverStaleJobQuotaLock(directory, authorize) {
|
|
9
|
+
if (typeof authorize !== 'function')
|
|
10
|
+
throw new JobConflictError('Job lock recovery requires authorization');
|
|
11
|
+
const root = await lstat(directory);
|
|
12
|
+
if (!root.isDirectory() || root.isSymbolicLink() || (root.mode & 0o077) !== 0)
|
|
13
|
+
throw new JobConflictError('Job storage root must be private');
|
|
14
|
+
const guard = join(directory, '.quota-recover');
|
|
15
|
+
try {
|
|
16
|
+
await mkdir(guard, { mode: 0o700 });
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
if (error.code === 'EEXIST')
|
|
20
|
+
return false;
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
const lock = join(directory, '.quota-lock');
|
|
25
|
+
let info;
|
|
26
|
+
try {
|
|
27
|
+
info = await lstat(lock);
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code === 'ENOENT')
|
|
31
|
+
return false;
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
if (!info.isDirectory() || info.isSymbolicLink())
|
|
35
|
+
throw new JobConflictError('Invalid job lock');
|
|
36
|
+
const ownerFile = join(lock, 'owner');
|
|
37
|
+
const ownerInfo = await lstat(ownerFile);
|
|
38
|
+
if (!ownerInfo.isFile() || (ownerInfo.mode & 0o077) !== 0)
|
|
39
|
+
throw new JobConflictError('Invalid job lock owner');
|
|
40
|
+
const token = (await readFile(ownerFile, 'utf8')).trim();
|
|
41
|
+
const match = /^([1-9][0-9]*):([0-9a-f-]{36})$/.exec(token);
|
|
42
|
+
if (!match || !Number.isSafeInteger(Number(match[1])))
|
|
43
|
+
throw new JobConflictError('Invalid job lock owner');
|
|
44
|
+
const pid = Number(match[1]);
|
|
45
|
+
const isDead = () => {
|
|
46
|
+
try {
|
|
47
|
+
process.kill(pid, 0);
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error.code === 'ESRCH')
|
|
52
|
+
return true;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
if (!isDead())
|
|
57
|
+
return false;
|
|
58
|
+
let approved = false;
|
|
59
|
+
try {
|
|
60
|
+
approved = await authorize({ pid, token });
|
|
61
|
+
}
|
|
62
|
+
catch { /* deny recovery */ }
|
|
63
|
+
if (!approved || !isDead() || (await readFile(ownerFile, 'utf8')).trim() !== token)
|
|
64
|
+
return false;
|
|
65
|
+
await rm(lock, { recursive: true });
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rm(guard, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Same-host, local-filesystem transactional quota gate. All writers to the store must use this wrapper. */
|
|
73
|
+
export class FileJobQuotaStore {
|
|
74
|
+
payloadBytes;
|
|
75
|
+
journal;
|
|
76
|
+
lockPath;
|
|
77
|
+
limits;
|
|
78
|
+
constructor(directory, limits, externallyDeleted, payloadBytes) {
|
|
79
|
+
this.payloadBytes = payloadBytes;
|
|
80
|
+
this.journal = new FileJobStore(directory, externallyDeleted);
|
|
81
|
+
this.lockPath = join(directory, '.quota-lock');
|
|
82
|
+
this.limits = structuredClone(limits);
|
|
83
|
+
for (const tier of [this.limits.principal, this.limits.project])
|
|
84
|
+
for (const quantity of quantities) {
|
|
85
|
+
if (!Number.isSafeInteger(tier[quantity]) || tier[quantity] < 0)
|
|
86
|
+
throw new JobConflictError('Invalid job quota policy');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
read(scope, id) { return this.journal.read(scope, id); }
|
|
90
|
+
/** Host-only listing; the gateway filters and authenticates every returned object. */
|
|
91
|
+
listSnapshots() { return this.journal.listSnapshots(); }
|
|
92
|
+
/** Call only after the independent D10 tombstone and authorized deletion. */
|
|
93
|
+
async purgeDeleted(scope, id) {
|
|
94
|
+
await this.journal.read(scope, id);
|
|
95
|
+
await this.transaction(() => this.journal.purgeDeleted(scope, id));
|
|
96
|
+
}
|
|
97
|
+
async acquire(job) {
|
|
98
|
+
job = canonicalizeInitialJob(job);
|
|
99
|
+
await this.journal.read(job.scope, job.id);
|
|
100
|
+
return this.transaction(async () => {
|
|
101
|
+
const existing = await this.journal.read(job.scope, job.id);
|
|
102
|
+
if (!existing)
|
|
103
|
+
await this.check(await this.journal.listSnapshots(), { revision: 1, job, deleted: false });
|
|
104
|
+
return this.journal.acquire(job);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
async compareAndSwap(previous, next) {
|
|
108
|
+
await this.journal.read(previous.job.scope, previous.job.id);
|
|
109
|
+
return this.transaction(async () => {
|
|
110
|
+
const current = await this.journal.read(previous.job.scope, previous.job.id);
|
|
111
|
+
if (!current || canonicalJson(current) !== canonicalJson(previous))
|
|
112
|
+
return false;
|
|
113
|
+
await this.check(await this.journal.listSnapshots(), next, previous);
|
|
114
|
+
return this.journal.compareAndSwap(previous, next);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async transaction(fn) {
|
|
118
|
+
// Fail closed rather than stealing a stale/slow owner's lock; operator must reconcile a crash.
|
|
119
|
+
const release = await acquireDirectoryLock(this.lockPath, { timeoutMs: 5000, pollMs: 20 });
|
|
120
|
+
try {
|
|
121
|
+
return await fn();
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
await release();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
async check(records, proposed, previous) {
|
|
128
|
+
const scope = proposed.job.scope;
|
|
129
|
+
const effective = records.filter(record => !previous || canonicalJson(record.job.scope) !== canonicalJson(scope) || record.job.id !== proposed.job.id);
|
|
130
|
+
effective.push(proposed);
|
|
131
|
+
const tiers = [
|
|
132
|
+
['project', this.limits.project, job => job.scope.tenantId === scope.tenantId && job.scope.projectId === scope.projectId],
|
|
133
|
+
['principal', this.limits.principal, job => job.scope.tenantId === scope.tenantId && job.scope.projectId === scope.projectId &&
|
|
134
|
+
job.scope.workspaceId === scope.workspaceId && job.scope.principalId === scope.principalId],
|
|
135
|
+
];
|
|
136
|
+
for (const [tier, limits, belongs] of tiers) {
|
|
137
|
+
const totals = Object.fromEntries(quantities.map(quantity => [quantity, 0]));
|
|
138
|
+
for (const record of effective) {
|
|
139
|
+
if (record.deleted || !belongs(record.job))
|
|
140
|
+
continue;
|
|
141
|
+
const job = record.job;
|
|
142
|
+
totals.jobs++;
|
|
143
|
+
totals.queued += job.summary.queued + job.summary['retryable-failed'];
|
|
144
|
+
totals.running += job.summary.running;
|
|
145
|
+
totals.retainedItems += job.items.length;
|
|
146
|
+
totals.retainedBytes += Buffer.byteLength(canonicalJson(job), 'utf8');
|
|
147
|
+
totals.tokens += job.budget.maxTokens;
|
|
148
|
+
totals.costMicros += job.budget.maxCostMicros;
|
|
149
|
+
totals.calls += job.items.length * job.budget.maxAttempts;
|
|
150
|
+
}
|
|
151
|
+
if (this.payloadBytes)
|
|
152
|
+
totals.retainedBytes += await this.payloadBytes(scope, tier);
|
|
153
|
+
if (quantities.some(quantity => !Number.isSafeInteger(totals[quantity]) || totals[quantity] > limits[quantity]))
|
|
154
|
+
throw new JobConflictError('Job capacity unavailable');
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=job-quota.js.map
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { ITEM_STATES, validateDecisionJob } from './job-contract.js';
|
|
2
|
+
import { DecisionTraceBuilder } from './telemetry/trace.js';
|
|
3
|
+
import { accountDecisionJob } from './job-accounting.js';
|
|
4
|
+
import { JobConflictError } from './job-store.js';
|
|
5
|
+
const DIGEST = /^sha256:[a-f0-9]{64}$/;
|
|
6
|
+
const EFFECT_ID = /^eff1_[a-z2-7]{51}[aq]$/;
|
|
7
|
+
function acceptedResolution(value) {
|
|
8
|
+
return !!value && value.reason === 'digest-match' && ['succeeded', 'abstained', 'review'].includes(value.state) &&
|
|
9
|
+
typeof value.receiptDigest === 'string' && DIGEST.test(value.receiptDigest) &&
|
|
10
|
+
typeof value.resultDigest === 'string' && DIGEST.test(value.resultDigest) &&
|
|
11
|
+
typeof value.effectId === 'string' && EFFECT_ID.test(value.effectId);
|
|
12
|
+
}
|
|
13
|
+
export class DecisionJobRuntime {
|
|
14
|
+
store;
|
|
15
|
+
now;
|
|
16
|
+
telemetry;
|
|
17
|
+
constructor(store, now = Date.now, telemetry) {
|
|
18
|
+
this.store = store;
|
|
19
|
+
this.now = now;
|
|
20
|
+
this.telemetry = telemetry;
|
|
21
|
+
}
|
|
22
|
+
async trace(snapshot, operation) {
|
|
23
|
+
if (!this.telemetry)
|
|
24
|
+
return;
|
|
25
|
+
try {
|
|
26
|
+
const builder = new DecisionTraceBuilder(undefined, this.now);
|
|
27
|
+
const safeOperation = ['submit', 'transition', 'retry', 'cancel', 'reconcile', 'expire', 'delete', 'hold', 'release-hold']
|
|
28
|
+
.includes(operation) ? operation : 'transition';
|
|
29
|
+
const span = builder.startSpan('decision.job', { attributes: {
|
|
30
|
+
'aiwg.job.operation': safeOperation, 'aiwg.job.status': snapshot.job.state,
|
|
31
|
+
'aiwg.job.revision': snapshot.revision, 'aiwg.job.item_count': snapshot.job.items.length,
|
|
32
|
+
'aiwg.job.unknown_count': snapshot.job.summary['execution-unknown'],
|
|
33
|
+
}, provenance: {
|
|
34
|
+
'aiwg.job.operation': 'client-derived', 'aiwg.job.status': 'client-derived',
|
|
35
|
+
'aiwg.job.revision': 'client-derived', 'aiwg.job.item_count': 'client-derived',
|
|
36
|
+
'aiwg.job.unknown_count': 'client-derived',
|
|
37
|
+
} });
|
|
38
|
+
builder.endSpan(span);
|
|
39
|
+
await this.telemetry.emit(builder.build().spans[0]);
|
|
40
|
+
}
|
|
41
|
+
catch { /* observability must never change durable state or authorize actions */ }
|
|
42
|
+
}
|
|
43
|
+
async submit(job, actor) {
|
|
44
|
+
this.authorize(actor, job.scope);
|
|
45
|
+
if (this.now() >= job.expiresAtEpochMs)
|
|
46
|
+
throw new JobConflictError('Expired job');
|
|
47
|
+
const acquired = await this.store.acquire(job);
|
|
48
|
+
if (acquired.owner)
|
|
49
|
+
await this.trace(acquired.snapshot, 'submit');
|
|
50
|
+
return acquired.snapshot;
|
|
51
|
+
}
|
|
52
|
+
async poll(actor, id) {
|
|
53
|
+
const snapshot = await this.store.read(actor, id);
|
|
54
|
+
return snapshot && !snapshot.deleted ? snapshot : null;
|
|
55
|
+
}
|
|
56
|
+
async accounting(actor, id, receipts, batches) {
|
|
57
|
+
const snapshot = await this.poll(actor, id);
|
|
58
|
+
return snapshot ? accountDecisionJob(snapshot.job, receipts, batches) : null;
|
|
59
|
+
}
|
|
60
|
+
async items(actor, id, offset = 0, limit = 100) {
|
|
61
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || !Number.isSafeInteger(limit) || limit < 1 || limit > 100)
|
|
62
|
+
throw new JobConflictError('Invalid page');
|
|
63
|
+
const snapshot = await this.poll(actor, id);
|
|
64
|
+
return snapshot ? structuredClone(snapshot.job.items.slice(offset, offset + limit)) : null;
|
|
65
|
+
}
|
|
66
|
+
async advance(actor, id, expected, next, operation = 'transition') {
|
|
67
|
+
this.authorize(actor, expected.job.scope);
|
|
68
|
+
if (expected.job.id !== id || this.now() >= expected.job.expiresAtEpochMs)
|
|
69
|
+
throw new JobConflictError('Job expired or mismatched');
|
|
70
|
+
const proposed = { revision: expected.revision + 1, job: structuredClone(next), deleted: false, legalHold: expected.legalHold ?? false };
|
|
71
|
+
if (!await this.store.compareAndSwap(expected, proposed))
|
|
72
|
+
throw new JobConflictError('Concurrent job update');
|
|
73
|
+
await this.trace(proposed, operation);
|
|
74
|
+
return proposed;
|
|
75
|
+
}
|
|
76
|
+
/** Requeue one eligible failure only; no dispatch occurs here. Previous attempts remain immutable. */
|
|
77
|
+
async retry(actor, id, itemId) {
|
|
78
|
+
const previous = await this.poll(actor, id);
|
|
79
|
+
if (!previous)
|
|
80
|
+
return null;
|
|
81
|
+
const item = previous.job.items.find(candidate => candidate.id === itemId);
|
|
82
|
+
if (!item || item.state !== 'retryable-failed' || item.attempts.length >= previous.job.budget.maxAttempts ||
|
|
83
|
+
!['running', 'partially-completed'].includes(previous.job.state))
|
|
84
|
+
throw new JobConflictError('Item not retryable');
|
|
85
|
+
const next = structuredClone(previous.job);
|
|
86
|
+
next.items.find(candidate => candidate.id === itemId).state = 'queued';
|
|
87
|
+
recount(next);
|
|
88
|
+
return this.advance(actor, id, previous, next, 'retry');
|
|
89
|
+
}
|
|
90
|
+
async cancel(actor, id) {
|
|
91
|
+
const previous = await this.poll(actor, id);
|
|
92
|
+
if (!previous)
|
|
93
|
+
return null;
|
|
94
|
+
if (['completed', 'canceled', 'expired', 'failed', 'cancel-requested'].includes(previous.job.state))
|
|
95
|
+
return previous;
|
|
96
|
+
const next = structuredClone(previous.job);
|
|
97
|
+
next.state = previous.job.state === 'validating' ? 'canceled' : 'cancel-requested';
|
|
98
|
+
// Running items remain in-flight; they must be reconciled rather than falsely marked canceled.
|
|
99
|
+
next.items.forEach(item => { if (item.state === 'queued' || item.state === 'retryable-failed')
|
|
100
|
+
item.state = 'canceled'; });
|
|
101
|
+
recount(next);
|
|
102
|
+
return this.advance(actor, id, previous, next, 'cancel');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Explicit offline restart reconciliation. Never re-dispatch an attempt whose transport may have started.
|
|
106
|
+
* With `resolveUnknown` (opt-in, D16), the reconciled `execution-unknown` items are then offered to the
|
|
107
|
+
* resolver; without it the result is exactly the default reconciliation.
|
|
108
|
+
*/
|
|
109
|
+
async reconcile(actor, id, options = {}) {
|
|
110
|
+
const reconciled = await this.reconcileDispatches(actor, id);
|
|
111
|
+
if (!reconciled || !options.resolveUnknown)
|
|
112
|
+
return reconciled;
|
|
113
|
+
return this.resolveUnknown(actor, id, options.resolveUnknown);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* D16 opt-in resolver (#2722): promote `execution-unknown` items whose resolver returns a verified
|
|
117
|
+
* `digest-match` resolution, through the one gated contract transition out of `execution-unknown`.
|
|
118
|
+
* The job state is kept; absent, unknown, `state-match` or a failing resolver leave the item as it is.
|
|
119
|
+
* Never dispatches.
|
|
120
|
+
*/
|
|
121
|
+
async resolveUnknown(actor, id, resolver) {
|
|
122
|
+
const previous = await this.poll(actor, id);
|
|
123
|
+
if (!previous)
|
|
124
|
+
return null;
|
|
125
|
+
this.authorize(actor, previous.job.scope);
|
|
126
|
+
const next = structuredClone(previous.job);
|
|
127
|
+
let resolved = 0;
|
|
128
|
+
for (const item of next.items) {
|
|
129
|
+
const attempt = item.attempts.at(-1);
|
|
130
|
+
if (item.state !== 'execution-unknown' || !attempt || attempt.outcome !== 'execution-unknown')
|
|
131
|
+
continue;
|
|
132
|
+
let resolution = null;
|
|
133
|
+
try {
|
|
134
|
+
resolution = await resolver({ job: structuredClone(previous.job), item: structuredClone(item), attempt: structuredClone(attempt) });
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
resolution = null;
|
|
138
|
+
}
|
|
139
|
+
if (!acceptedResolution(resolution))
|
|
140
|
+
continue;
|
|
141
|
+
attempt.outcome = 'succeeded';
|
|
142
|
+
attempt.receiptDigest = resolution.receiptDigest;
|
|
143
|
+
attempt.resolution = { method: 'effect-ledger', effectId: resolution.effectId, reason: 'digest-match', receiptDigest: resolution.receiptDigest };
|
|
144
|
+
item.state = resolution.state;
|
|
145
|
+
item.resultDigest = resolution.resultDigest;
|
|
146
|
+
resolved++;
|
|
147
|
+
}
|
|
148
|
+
if (!resolved)
|
|
149
|
+
return previous;
|
|
150
|
+
recount(next);
|
|
151
|
+
validateDecisionJob(next);
|
|
152
|
+
const proposed = { revision: previous.revision + 1, job: next, deleted: false, legalHold: previous.legalHold ?? false };
|
|
153
|
+
if (!await this.store.compareAndSwap(previous, proposed))
|
|
154
|
+
throw new JobConflictError('Concurrent job update');
|
|
155
|
+
await this.trace(proposed, 'reconcile');
|
|
156
|
+
return proposed;
|
|
157
|
+
}
|
|
158
|
+
async reconcileDispatches(actor, id) {
|
|
159
|
+
const previous = await this.poll(actor, id);
|
|
160
|
+
if (!previous)
|
|
161
|
+
return null;
|
|
162
|
+
if (!['running', 'partially-completed', 'cancel-requested'].includes(previous.job.state))
|
|
163
|
+
return previous;
|
|
164
|
+
const next = structuredClone(previous.job);
|
|
165
|
+
next.state = 'failed';
|
|
166
|
+
next.items.forEach(item => {
|
|
167
|
+
if (item.state === 'queued' || item.state === 'retryable-failed')
|
|
168
|
+
item.state = 'canceled';
|
|
169
|
+
if (item.state === 'running') {
|
|
170
|
+
const attempt = item.attempts.at(-1);
|
|
171
|
+
item.state = attempt?.outcome === 'dispatched' ? 'execution-unknown' : 'canceled';
|
|
172
|
+
if (attempt?.outcome === 'dispatched')
|
|
173
|
+
attempt.outcome = 'execution-unknown';
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
recount(next);
|
|
177
|
+
return this.advance(actor, id, previous, next, 'reconcile');
|
|
178
|
+
}
|
|
179
|
+
async expire(actor, id) {
|
|
180
|
+
const previous = await this.poll(actor, id);
|
|
181
|
+
if (!previous)
|
|
182
|
+
return null;
|
|
183
|
+
if (this.now() < previous.job.expiresAtEpochMs || ['expired', 'canceled', 'completed', 'failed'].includes(previous.job.state))
|
|
184
|
+
return previous;
|
|
185
|
+
// Expiration must not erase the evidence of an in-flight request.
|
|
186
|
+
const next = structuredClone(previous.job);
|
|
187
|
+
next.state = 'expired';
|
|
188
|
+
next.items.forEach(item => {
|
|
189
|
+
if (item.state === 'queued' || item.state === 'retryable-failed')
|
|
190
|
+
item.state = 'expired';
|
|
191
|
+
if (item.state === 'running') {
|
|
192
|
+
const attempt = item.attempts.at(-1);
|
|
193
|
+
item.state = attempt?.outcome === 'dispatched' ? 'execution-unknown' : 'expired';
|
|
194
|
+
if (attempt?.outcome === 'dispatched')
|
|
195
|
+
attempt.outcome = 'execution-unknown';
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
recount(next);
|
|
199
|
+
const proposed = { revision: previous.revision + 1, job: next, deleted: false, legalHold: previous.legalHold ?? false };
|
|
200
|
+
if (!await this.store.compareAndSwap(previous, proposed))
|
|
201
|
+
throw new JobConflictError('Concurrent job update');
|
|
202
|
+
await this.trace(proposed, 'expire');
|
|
203
|
+
return proposed;
|
|
204
|
+
}
|
|
205
|
+
async setLegalHold(actor, id, enabled) {
|
|
206
|
+
if (typeof enabled !== 'boolean')
|
|
207
|
+
throw new JobConflictError('Invalid legal hold');
|
|
208
|
+
const previous = await this.poll(actor, id);
|
|
209
|
+
if (!previous)
|
|
210
|
+
return null;
|
|
211
|
+
if (Boolean(previous.legalHold) === enabled)
|
|
212
|
+
return previous;
|
|
213
|
+
const next = { ...previous, revision: previous.revision + 1, legalHold: enabled };
|
|
214
|
+
if (!await this.store.compareAndSwap(previous, next))
|
|
215
|
+
throw new JobConflictError('Concurrent job update');
|
|
216
|
+
await this.trace(next, enabled ? 'hold' : 'release-hold');
|
|
217
|
+
return next;
|
|
218
|
+
}
|
|
219
|
+
async remove(actor, id) {
|
|
220
|
+
const previous = await this.poll(actor, id);
|
|
221
|
+
if (!previous)
|
|
222
|
+
return false;
|
|
223
|
+
if (previous.legalHold)
|
|
224
|
+
throw new JobConflictError('Legal hold prohibits deletion');
|
|
225
|
+
const next = { ...previous, revision: previous.revision + 1, deleted: true, legalHold: false };
|
|
226
|
+
if (!await this.store.compareAndSwap(previous, next))
|
|
227
|
+
throw new JobConflictError('Concurrent job update');
|
|
228
|
+
await this.trace(next, 'delete');
|
|
229
|
+
return true;
|
|
230
|
+
}
|
|
231
|
+
authorize(actor, scope) {
|
|
232
|
+
if (actor.tenantId !== scope.tenantId || actor.projectId !== scope.projectId ||
|
|
233
|
+
actor.workspaceId !== scope.workspaceId || actor.principalId !== scope.principalId)
|
|
234
|
+
throw new JobConflictError('Job unavailable');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
export function recount(job) {
|
|
238
|
+
job.summary = Object.fromEntries(ITEM_STATES.map(state => [state, job.items.filter(item => item.state === state).length]));
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=job-runtime.js.map
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { runBoundedFair } from './scheduler.js';
|
|
2
|
+
import { JobConflictError } from './job-store.js';
|
|
3
|
+
/** Offline round-robin across authenticated principal/project lanes; not a distributed admission authority. */
|
|
4
|
+
export class OfflineJobScheduler {
|
|
5
|
+
worker;
|
|
6
|
+
concurrency;
|
|
7
|
+
maxQueuedItems;
|
|
8
|
+
constructor(worker, concurrency, maxQueuedItems) {
|
|
9
|
+
this.worker = worker;
|
|
10
|
+
this.concurrency = concurrency;
|
|
11
|
+
this.maxQueuedItems = maxQueuedItems;
|
|
12
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1 ||
|
|
13
|
+
!Number.isSafeInteger(maxQueuedItems) || maxQueuedItems < concurrency)
|
|
14
|
+
throw new JobConflictError('Invalid job scheduler limits');
|
|
15
|
+
}
|
|
16
|
+
async run(work, options = {}) {
|
|
17
|
+
if (work.length > this.maxQueuedItems)
|
|
18
|
+
throw new JobConflictError('Job queue capacity exceeded');
|
|
19
|
+
const keys = work.map(item => JSON.stringify([item.actor, item.jobId, item.itemId]));
|
|
20
|
+
if (new Set(keys).size !== keys.length)
|
|
21
|
+
throw new JobConflictError('Duplicate scheduled job item');
|
|
22
|
+
return runBoundedFair(work.map(item => ({ value: item,
|
|
23
|
+
lane: JSON.stringify([item.actor.tenantId, item.actor.projectId, item.actor.principalId]) })), this.concurrency, item => this.worker.run(item.actor, item.jobId, item.itemId, item.executor, item.reservation), options);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=job-scheduler.js.map
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { DecisionJobGateway } from './job-gateway.js';
|
|
2
|
+
import { admittedJobItemExecutor } from './job-evaluate.js';
|
|
3
|
+
import { artifactDigest } from './validate.js';
|
|
4
|
+
import { FileJobPollLimiter } from './job-poll-limiter.js';
|
|
5
|
+
import { FileJobPayloadStore } from './job-payload-store.js';
|
|
6
|
+
import { FileJobQuotaStore } from './job-quota.js';
|
|
7
|
+
import { DecisionJobRuntime } from './job-runtime.js';
|
|
8
|
+
import { OfflineJobScheduler } from './job-scheduler.js';
|
|
9
|
+
import { OfflineJobWorker } from './job-worker.js';
|
|
10
|
+
import { FileJobStore, JobConflictError } from './job-store.js';
|
|
11
|
+
import { canonicalJson } from '../security/artifact-trust.js';
|
|
12
|
+
import { validateDecisionLifecyclePolicy } from './lifecycle.js';
|
|
13
|
+
/** Disabled-by-default offline assembly. Host must supply authenticated actors and admitted item executors. */
|
|
14
|
+
export function createOfflineDecisionJobService(config) {
|
|
15
|
+
if (typeof config.externallyDeleted !== 'function' || typeof config.authorizeExport !== 'function')
|
|
16
|
+
throw new Error('Independent lifecycle and export authorization required');
|
|
17
|
+
validateDecisionLifecyclePolicy(config.lifecyclePolicy);
|
|
18
|
+
if (config.lifecyclePolicy.surfaces.job.backup !== 'expire-with-primary' ||
|
|
19
|
+
config.lifecyclePolicy.surfaces.job.deletion !== 'erase')
|
|
20
|
+
throw new JobConflictError('Persisted job requires erasure and backup expiry policy');
|
|
21
|
+
const now = config.now ?? Date.now;
|
|
22
|
+
let payloads;
|
|
23
|
+
const store = new FileJobQuotaStore(config.directory, config.quota, config.externallyDeleted, (scope, tier) => payloads.usage(scope, tier));
|
|
24
|
+
const metadataBytes = async (scope, tier) => {
|
|
25
|
+
const records = await store.listSnapshots();
|
|
26
|
+
return records.filter(record => !record.deleted && record.job.scope.tenantId === scope.tenantId &&
|
|
27
|
+
record.job.scope.projectId === scope.projectId && (tier === 'project' ||
|
|
28
|
+
record.job.scope.workspaceId === scope.workspaceId && record.job.scope.principalId === scope.principalId))
|
|
29
|
+
.reduce((sum, record) => sum + Buffer.byteLength(canonicalJson(record.job), 'utf8'), 0);
|
|
30
|
+
};
|
|
31
|
+
payloads = new FileJobPayloadStore(config.directory, config.payloadKey, new FileJobStore(config.directory, config.externallyDeleted), config.externallyDeleted, { itemBytes: config.payloadMaxItemBytes, principalBytes: config.quota.principal.retainedBytes,
|
|
32
|
+
projectBytes: config.quota.project.retainedBytes }, now, metadataBytes);
|
|
33
|
+
const runtime = new DecisionJobRuntime(store, now, config.telemetry);
|
|
34
|
+
const worker = new OfflineJobWorker(runtime);
|
|
35
|
+
const scheduler = new OfflineJobScheduler(worker, config.scheduler.concurrency, config.scheduler.maxQueuedItems);
|
|
36
|
+
const polling = new FileJobPollLimiter(config.directory, config.polls.windowMs, config.polls.perPrincipal, config.polls.perProject, config.polls.maxLanes, now);
|
|
37
|
+
const admitJob = (job) => {
|
|
38
|
+
const duration = job.expiresAtEpochMs - job.createdAtEpochMs;
|
|
39
|
+
if (!Number.isSafeInteger(duration) || duration <= 0 || duration > config.lifecyclePolicy.surfaces.job.retentionMs)
|
|
40
|
+
throw new JobConflictError('Job exceeds D10 retention policy');
|
|
41
|
+
};
|
|
42
|
+
const gateway = new DecisionJobGateway(runtime, config.handleKey, now, actor => polling.check(actor), {
|
|
43
|
+
listSnapshots: () => store.listSnapshots(), authorizeExport: async (actor, snapshot) => config.lifecyclePolicy.surfaces.job.export === 'sanitized' && await config.authorizeExport(actor, snapshot),
|
|
44
|
+
}, admitJob);
|
|
45
|
+
/** Host maintenance: bound queued dwell by the pinned expiry even after a restart. */
|
|
46
|
+
const sweepExpired = async () => {
|
|
47
|
+
let expired = 0;
|
|
48
|
+
for (const record of await store.listSnapshots()) {
|
|
49
|
+
if (record.deleted || now() < record.job.expiresAtEpochMs ||
|
|
50
|
+
['expired', 'canceled', 'completed', 'failed'].includes(record.job.state))
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
if ((await runtime.expire(record.job.scope, record.job.id))?.job.state === 'expired')
|
|
54
|
+
expired++;
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
if (!(error instanceof JobConflictError) || error.message !== 'Concurrent job update')
|
|
58
|
+
throw error;
|
|
59
|
+
// Another process owns the winning revision. Do not retry or dispatch here.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return expired;
|
|
63
|
+
};
|
|
64
|
+
/** D10 eraser callback: publish D10 tombstone first, then call this; retries are safe after partial erasure. */
|
|
65
|
+
const eraseJob = async (scope, id) => {
|
|
66
|
+
if (!await config.externallyDeleted(scope, id))
|
|
67
|
+
throw new JobConflictError('Independent job tombstone required');
|
|
68
|
+
const raw = await new FileJobStore(config.directory).read(scope, id);
|
|
69
|
+
if (raw && (!raw.deleted || raw.legalHold))
|
|
70
|
+
throw new JobConflictError('Authorized job deletion required');
|
|
71
|
+
await payloads.purgeDeleted(scope, id);
|
|
72
|
+
await store.purgeDeleted(scope, id);
|
|
73
|
+
};
|
|
74
|
+
/** Recover a completed result from its durable D03 receipt, never by replaying the provider. */
|
|
75
|
+
const materializeResult = async (actor, id, itemId, receipts) => {
|
|
76
|
+
const snapshot = await runtime.poll(actor, id);
|
|
77
|
+
const item = snapshot?.job.items.find(entry => entry.id === itemId);
|
|
78
|
+
const attempt = item?.attempts.at(-1);
|
|
79
|
+
if (!snapshot || !item || !['succeeded', 'review', 'abstained'].includes(item.state) ||
|
|
80
|
+
!item.resultDigest || !attempt?.receiptDigest || attempt.outcome !== 'succeeded')
|
|
81
|
+
throw new JobConflictError('Validated job result unavailable');
|
|
82
|
+
const receipt = await receipts.read(attempt.id, actor.projectId);
|
|
83
|
+
if (!receipt || receipt.state !== 'completed' || !receipt.result ||
|
|
84
|
+
artifactDigest(receipt) !== attempt.receiptDigest || artifactDigest(receipt.result) !== item.resultDigest)
|
|
85
|
+
throw new JobConflictError('Validated job receipt unavailable');
|
|
86
|
+
await payloads.put(actor, id, itemId, 'result', receipt.result);
|
|
87
|
+
return true;
|
|
88
|
+
};
|
|
89
|
+
/** Dispatches only the already admitted synchronous evaluator using protected pinned input. */
|
|
90
|
+
const runAdmittedItem = async (actor, id, itemId, requestFor, reservation) => {
|
|
91
|
+
const snapshot = await runtime.poll(actor, id);
|
|
92
|
+
if (!snapshot)
|
|
93
|
+
throw new JobConflictError('Job unavailable');
|
|
94
|
+
let receipts;
|
|
95
|
+
const execute = async (item, signal) => {
|
|
96
|
+
const input = await payloads.get(actor, id, item.id, 'input');
|
|
97
|
+
if (input === null)
|
|
98
|
+
throw new JobConflictError('Protected job input unavailable');
|
|
99
|
+
return admittedJobItemExecutor(snapshot.job, (candidate, active) => {
|
|
100
|
+
const request = requestFor(candidate, input, active);
|
|
101
|
+
receipts = request.receiptStore;
|
|
102
|
+
return request;
|
|
103
|
+
})(item, signal);
|
|
104
|
+
};
|
|
105
|
+
execute.requiresReservation = true;
|
|
106
|
+
const finished = await worker.run(actor, id, itemId, execute, reservation);
|
|
107
|
+
const item = finished.job.items.find(candidate => candidate.id === itemId);
|
|
108
|
+
if (item && ['succeeded', 'review', 'abstained'].includes(item.state)) {
|
|
109
|
+
if (!receipts)
|
|
110
|
+
throw new JobConflictError('Durable decision receipts required');
|
|
111
|
+
await materializeResult(actor, id, itemId, receipts);
|
|
112
|
+
}
|
|
113
|
+
return finished;
|
|
114
|
+
};
|
|
115
|
+
return { gateway, runtime, worker, scheduler, payloads, eraseJob, sweepExpired, runAdmittedItem, materializeResult };
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=job-service.js.map
|