@aiwg/cli 2026.10.5 → 2026.10.7
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/addons/composition-engine/lib/runtime.mjs +1401 -0
- package/agentic/code/addons/composition-engine/lib/validator.mjs +513 -0
- package/agentic/code/addons/composition-engine/schemas/flow-graph.schema.json +372 -0
- package/agentic/code/providers/model-capabilities.v1.json +74 -50
- package/bin/aiwg.mjs +7 -3
- package/dist/src/a2a/agent-card.js +11 -0
- package/dist/src/api/index.d.ts +1 -0
- package/dist/src/api/index.js +1 -0
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +57 -6
- package/dist/src/artifacts/fortemi-core-query-adapter.js +2 -0
- package/dist/src/artifacts/fortemi-core-sync.js +26 -8
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/contract.json +103 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-config.schema.json +212 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-embedding-set.schema.json +240 -0
- package/dist/src/artifacts/fortemi-shard-contract/2.2.0/full-v1-manifest.schema.json +451 -0
- package/dist/src/artifacts/fortemi-shard-export.js +284 -0
- package/dist/src/artifacts/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +143 -14
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +2 -0
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +114 -55
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/memory-maintenance.js +503 -0
- package/dist/src/artifacts/prebuilt-relations.js +3 -1
- package/dist/src/artifacts/query-engine.js +263 -108
- package/dist/src/artifacts/relations-builder.js +2 -2
- package/dist/src/artifacts/source-graph.js +2 -2
- package/dist/src/artifacts/types.js +9 -1
- package/dist/src/auth/client.js +5 -1
- package/dist/src/auth/credential-store.js +5 -1
- package/dist/src/auth/sensitive-input.js +151 -0
- package/dist/src/channel/manager.mjs +3 -0
- package/dist/src/cli/handlers/agentcard.js +47 -25
- package/dist/src/cli/handlers/auth.js +22 -2
- package/dist/src/cli/handlers/decision.js +22 -3
- package/dist/src/cli/handlers/effect.js +4 -84
- package/dist/src/cli/handlers/guided-setup.js +470 -0
- package/dist/src/cli/handlers/help.js +2 -2
- package/dist/src/cli/handlers/housekeeper.js +1110 -0
- package/dist/src/cli/handlers/index.js +4 -2
- package/dist/src/cli/handlers/init.js +41 -6
- package/dist/src/cli/handlers/install.js +6 -0
- package/dist/src/cli/handlers/local-executor.js +5 -3
- package/dist/src/cli/handlers/marketplace.js +15 -4
- package/dist/src/cli/handlers/mc.js +509 -53
- package/dist/src/cli/handlers/models.js +32 -2
- package/dist/src/cli/handlers/ralph-launcher.js +76 -4
- package/dist/src/cli/handlers/rules.js +1 -1
- package/dist/src/cli/handlers/script-runner.js +73 -2
- package/dist/src/cli/handlers/serve.js +5 -3
- package/dist/src/cli/handlers/sessions.js +1 -0
- package/dist/src/cli/handlers/setup.js +432 -1
- package/dist/src/cli/handlers/skill-lint.js +1 -1
- package/dist/src/cli/handlers/subcommands.js +118 -8
- package/dist/src/cli/handlers/use.js +335 -107
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +81 -0
- package/dist/src/cli/router.js +16 -1
- package/dist/src/config/aiwg-config.js +42 -0
- package/dist/src/config/cli.js +7 -3
- package/dist/src/config/decision-config.js +14 -1
- package/dist/src/config/gitignore.js +4 -0
- package/dist/src/config/native-sandbox-warnings.js +25 -0
- package/dist/src/config/workspace.js +2 -1
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/adapters/jev.js +20 -31
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/ask.js +6 -1
- package/dist/src/decision/benchmark.js +482 -0
- package/dist/src/decision/ledger.js +22 -3
- package/dist/src/decision/pricing.js +147 -0
- package/dist/src/decision/qualification/gate-evidence.js +23 -18
- package/dist/src/decision/qualification/load-staged.js +576 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +12 -5
- package/dist/src/decision/review/service.js +6 -4
- package/dist/src/decision/review/validate.js +23 -2
- package/dist/src/decision/savings.js +8 -3
- package/dist/src/discovery/index.js +492 -0
- package/dist/src/discovery/interview.js +238 -0
- package/dist/src/discovery/plan-apply.js +567 -0
- package/dist/src/discovery/plan-store.js +171 -0
- package/dist/src/discovery/plan-types.js +2 -0
- package/dist/src/discovery/plan-validation.js +139 -0
- package/dist/src/discovery/types.js +2 -0
- package/dist/src/effects/host.js +91 -0
- package/dist/src/extensions/commands/definitions.js +49 -9
- package/dist/src/extensions/manifest.js +71 -44
- package/dist/src/extensions/project-local-discovery.js +1 -1
- package/dist/src/extensions/project-local-gitignore.js +2 -0
- package/dist/src/extensions/project-quickref.js +18 -17
- package/dist/src/extensions/validation.js +32 -29
- package/dist/src/housekeeper/daemon-config.js +148 -0
- package/dist/src/housekeeper/ipc.js +84 -0
- package/dist/src/housekeeper/lifecycle.js +148 -0
- package/dist/src/housekeeper/platform.js +35 -0
- package/dist/src/housekeeper/status.js +70 -0
- package/dist/src/housekeeper/units.js +114 -0
- package/dist/src/issues/cli.js +16 -5
- package/dist/src/issues/live.js +44 -12
- package/dist/src/jobs/flow.js +22 -21
- package/dist/src/lint/rule-activation.js +20 -5
- package/dist/src/lint/runner.js +1 -1
- package/dist/src/marketplace/exchange.js +18 -6
- package/dist/src/marketplace/provenance.js +3 -2
- package/dist/src/mcp/adapters/codex-runtime.js +46 -76
- package/dist/src/mcp/cli.mjs +22 -96
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- package/dist/src/mcp/grok-build-config.mjs +2 -1
- package/dist/src/mcp/muse-native-config.mjs +2 -1
- package/dist/src/mcp/profiles.js +18 -31
- package/dist/src/mcp/profiles.mjs +18 -32
- package/dist/src/mcp/registry.js +7 -27
- package/dist/src/mcp/registry.mjs +7 -28
- package/dist/src/mcp/server.mjs +34 -1
- package/dist/src/mcp/tools/agentic-sandbox.mjs +8 -7
- package/dist/src/mcp/tools/command-run.mjs +3 -2
- package/dist/src/mcp/tools/decision.mjs +49 -48
- package/dist/src/mcp/tools/discovery.mjs +46 -21
- package/dist/src/mcp/tools/orchestration.mjs +22 -22
- package/dist/src/mcp/tools/subsystems.mjs +9 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +30 -6
- package/dist/src/mcp/tools/writer-profiles.mjs +1 -1
- package/dist/src/memory/audit.js +719 -0
- package/dist/src/memory/canonical-context.js +392 -13
- package/dist/src/memory/canonical-maintenance.js +95 -0
- package/dist/src/memory/cli-result.js +22 -0
- package/dist/src/memory/cli.js +176 -0
- package/dist/src/memory/context-pack.js +57 -6
- package/dist/src/memory/cost.js +512 -0
- package/dist/src/memory/firewall-classification.d.mts +7 -0
- package/dist/src/memory/firewall-classification.mjs +32 -0
- package/dist/src/memory/index.js +8 -0
- package/dist/src/memory/line-maintenance.js +68 -0
- package/dist/src/memory/maintenance-cli.js +286 -0
- package/dist/src/memory/maintenance-contract.js +136 -0
- package/dist/src/memory/maintenance-index.js +18 -0
- package/dist/src/memory/maintenance-owner.js +2 -0
- package/dist/src/memory/maintenance-record.js +138 -0
- package/dist/src/memory/maintenance.js +254 -0
- package/dist/src/memory/project-registry.js +2 -1
- package/dist/src/memory/provider-memory-adapter.js +154 -0
- package/dist/src/memory/recommend-cli.js +83 -0
- package/dist/src/memory/recommend.js +269 -0
- package/dist/src/memory/scheduled.js +81 -0
- package/dist/src/memory/taxonomy.js +44 -0
- package/dist/src/mission-protocol/codecs.js +19 -9
- package/dist/src/mission-protocol/worker-status.js +91 -1
- package/dist/src/models/config-loader.js +11 -1
- package/dist/src/models/model-capabilities.v1.json +74 -50
- package/dist/src/models/provider-policy.js +125 -62
- package/dist/src/models/resolver.js +45 -8
- package/dist/src/models/worker-installation.d.mts +8 -1
- package/dist/src/models/worker-installation.mjs +231 -38
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/packages/adapters/git.js +99 -23
- package/dist/src/packages/adapters/gitea.js +8 -27
- package/dist/src/packages/adapters/github.js +7 -25
- package/dist/src/packages/registry.js +4 -4
- package/dist/src/plugin/metadata-validator.js +1 -1
- package/dist/src/process/cli-command.d.mts +5 -0
- package/dist/src/process/cli-command.mjs +42 -0
- package/dist/src/providers/deploy-version.d.mts +1 -0
- package/dist/src/providers/deploy-version.mjs +17 -0
- package/dist/src/providers/provider-definitions.js +19 -16
- package/dist/src/providers/skill-ownership.d.mts +11 -2
- package/dist/src/providers/skill-ownership.mjs +35 -17
- package/dist/src/rules/activation.d.mts +11 -2
- package/dist/src/rules/activation.mjs +82 -6
- package/dist/src/rules/inventory.js +1 -1
- package/dist/src/rules/rule-frontmatter.schema.json +2 -0
- package/dist/src/schema/zod-validation.d.mts +6 -0
- package/dist/src/schema/zod-validation.mjs +50 -0
- package/dist/src/serve/executor-registry.js +297 -20
- package/dist/src/serve/executor-token.d.mts +1 -0
- package/dist/src/serve/executor-token.mjs +8 -0
- package/dist/src/serve/shared-host-scheduler.js +1102 -31
- package/dist/src/serve/worker-invocation.js +124 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +10 -9
- package/dist/src/sessions/adapters/copilot.js +14 -13
- package/dist/src/sessions/adapters/cursor.js +19 -18
- package/dist/src/sessions/adapters/deepseek-harness.js +9 -8
- package/dist/src/sessions/adapters/factory.js +11 -10
- package/dist/src/sessions/adapters/generic.js +32 -23
- package/dist/src/sessions/adapters/grokbot.js +35 -35
- package/dist/src/sessions/adapters/hermes.js +20 -19
- package/dist/src/sessions/adapters/muse.js +50 -49
- package/dist/src/sessions/adapters/omp.js +5 -4
- package/dist/src/sessions/adapters/openclaw.js +30 -29
- package/dist/src/sessions/adapters/opencode.js +26 -26
- package/dist/src/sessions/adapters/openhuman.js +24 -23
- package/dist/src/sessions/adapters/pi.js +5 -4
- package/dist/src/sessions/adapters/windsurf.js +8 -8
- package/dist/src/sessions/analytics.js +10 -9
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +63 -53
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/workspace-discovery.js +3 -4
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +46 -10
- package/dist/src/smiths/context-pipeline/context-file-safety.js +330 -9
- package/dist/src/smiths/context-pipeline/finalization.js +3 -2
- package/dist/src/smiths/context-pipeline/generator.js +7 -19
- package/dist/src/smiths/context-pipeline/legacy-inject.js +3 -10
- package/dist/src/smiths/context-pipeline/managed-hook.js +5 -5
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +110 -22
- package/dist/src/tracker/project-inputs.js +2 -0
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- package/dist/src/update/notifier.mjs +2 -4
- package/dist/src/writing/exemplar-selection.js +6 -6
- package/dist/src/writing/voice-evaluation.js +24 -23
- package/dist/src/writing/writer-migration.js +14 -13
- package/dist/src/writing/writer-profile.js +19 -18
- package/dist/src/writing/writing-brief.js +18 -17
- package/dist/src/writing/writing-receipt.js +47 -46
- package/package.json +5 -2
- package/schemas/decision/DecisionReview.schema.json +78 -1
- package/schemas/mission-v1.schema.json +3 -0
- package/tools/agents/deploy-agents.mjs +67 -39
- package/tools/agents/providers/antigravity.mjs +6 -1
- package/tools/agents/providers/base.mjs +24 -14
- package/tools/agents/providers/claude.mjs +6 -3
- package/tools/agents/providers/codex.mjs +54 -57
- package/tools/agents/providers/context-write.mjs +28 -0
- package/tools/agents/providers/copilot.mjs +13 -4
- package/tools/agents/providers/cursor.mjs +1 -1
- package/tools/agents/providers/deepseek-harness.mjs +1 -1
- package/tools/agents/providers/factory.mjs +7 -1
- package/tools/agents/providers/grok-build.mjs +18 -2
- package/tools/agents/providers/grokbot.mjs +1 -1
- package/tools/agents/providers/hermes.mjs +12 -15
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +31 -5
- package/tools/agents/providers/opencode.mjs +14 -5
- package/tools/agents/providers/openhuman.mjs +1 -1
- package/tools/agents/providers/pi.mjs +1 -1
- package/tools/agents/providers/provider-prepare-coordinator.mjs +29 -9
- package/tools/agents/providers/rule-delivery.mjs +289 -80
- package/tools/agents/providers/warp.mjs +4 -1
- package/tools/agents/providers/windsurf.mjs +13 -3
- package/tools/cli/workspace-status.mjs +5 -0
- package/tools/commands/deploy-prompts-codex.mjs +11 -8
- package/tools/ralph-external/lib/codex-adapter.mjs +6 -2
- package/tools/ralph-external/lib/consecutive-failure-circuit-breaker.mjs +26 -0
- package/tools/ralph-external/lib/launcher-recovery-evidence.mjs +7 -1
- package/tools/ralph-external/lib/muse-adapter.mjs +12 -2
- package/tools/ralph-external/lib/recovery-probe.mjs +14 -4
- package/tools/ralph-external/lib/worker-health.mjs +1 -1
- package/tools/ralph-external/lib/worker-isolation.mjs +65 -9
- package/tools/ralph-external/process-monitor.mjs +49 -8
- package/tools/ralph-external/session-launcher.mjs +405 -35
- package/tools/skills/deploy-skills-codex.mjs +11 -7
- package/vscode-extension/schemas/aiwg.config.v1.json +11 -1
|
@@ -34,9 +34,9 @@
|
|
|
34
34
|
import { StringDecoder } from 'node:string_decoder';
|
|
35
35
|
import { spawn } from 'child_process';
|
|
36
36
|
import { createWriteStream, mkdirSync, existsSync, readFileSync, copyFileSync, constants } from 'fs';
|
|
37
|
-
import { open, mkdir, rename, unlink, lstat, readdir, realpath, link } from 'node:fs/promises';
|
|
37
|
+
import { open, mkdir, rename, unlink, lstat, readdir, realpath, link, access } from 'node:fs/promises';
|
|
38
38
|
import { randomUUID, createHash } from 'node:crypto';
|
|
39
|
-
import { fileURLToPath } from 'node:url';
|
|
39
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
40
40
|
import { basename, dirname, join, relative, isAbsolute } from 'path';
|
|
41
41
|
import { EventEmitter } from 'events';
|
|
42
42
|
import { homedir, constants as osConstants } from 'os';
|
|
@@ -49,7 +49,7 @@ import { ScopedFailureBreaker, attributeFailureScope } from './lib/consecutive-f
|
|
|
49
49
|
import { buildSessionEnvironment } from './lib/session-environment.mjs';
|
|
50
50
|
import { launcherRecoveryEvidence, observeLauncherRecoveryOutcome, recordLauncherRecoveryHint } from './lib/launcher-recovery-evidence.mjs';
|
|
51
51
|
export { observeLauncherRecoveryOutcome } from './lib/launcher-recovery-evidence.mjs';
|
|
52
|
-
import { checkWorkerDispatch, detectWorkerIsolation, launchIsolatedWorker, getWorkerIsolationStatus, stopWorkerUnit, readUnitControlGroup } from './lib/worker-isolation.mjs';
|
|
52
|
+
import { workerNeedsNativeNamespaces, probeWorkerNativeSandbox, nativeSandboxFailure, nativeSandboxProbeMessage, NATIVE_SANDBOX_UNAVAILABLE, checkWorkerDispatch, detectWorkerIsolation, launchIsolatedWorker, getWorkerIsolationStatus, stopWorkerUnit, readUnitControlGroup } from './lib/worker-isolation.mjs';
|
|
53
53
|
const { signalChild } = await importImpl(import.meta.url, 'process/signal-child.mjs');
|
|
54
54
|
|
|
55
55
|
// Resolve the same installation owner as CLI/MCP inputs in source and packaged layouts.
|
|
@@ -413,6 +413,84 @@ function subscribeWorkerRecovery(owner, breaker) {
|
|
|
413
413
|
_workerRecoverySubscriptions.set(owner, () => reporter.owners.delete(owner));
|
|
414
414
|
}
|
|
415
415
|
|
|
416
|
+
/** Reuse the MC authority and its registered one-shot identity at every spawn boundary. */
|
|
417
|
+
export async function assertManagedWorkerLaunch(options, invocation, inputs, projectRoot = process.cwd()) {
|
|
418
|
+
if (!options.managedAdmission || invocation.mode !== 'managed'
|
|
419
|
+
|| inputs.snapshot.config.parallelism?.worker_mode !== 'managed') throw new Error('Managed worker admission is required');
|
|
420
|
+
const { createMcAdmissionScheduler } = await importImpl(import.meta.url, 'cli/handlers/mc.js');
|
|
421
|
+
const { loadLauncherRegistry } = await importImpl(import.meta.url, 'cli/handlers/ralph-launcher.js');
|
|
422
|
+
const { workerModelKey } = await importImpl(import.meta.url, 'models/provider-policy.js');
|
|
423
|
+
const scheduler = await createMcAdmissionScheduler(projectRoot, inputs.snapshot.config);
|
|
424
|
+
const { requestId, generation } = options.managedAdmission;
|
|
425
|
+
const snapshot = scheduler.snapshot();
|
|
426
|
+
const record = snapshot.records[requestId];
|
|
427
|
+
const deadlineAtMs = Math.min(...[record?.accounting?.runKey, record?.accounting?.taskKey]
|
|
428
|
+
.map(key => snapshot.allowances?.[key]?.deadlineAt).filter(Boolean).map(Date.parse));
|
|
429
|
+
const registered = loadLauncherRegistry(projectRoot).loops[process.env.RALPH_LOOP_ID];
|
|
430
|
+
const worker = record?.managed;
|
|
431
|
+
const reservation = record?.accounting?.reservation;
|
|
432
|
+
if (!record || record.state !== 'admitted' || record.leaseGeneration !== generation
|
|
433
|
+
|| !record.launchClaimedAt || !record.metadata?.invocationId || Date.parse(record.leaseExpiresAt ?? '') <= Date.now()
|
|
434
|
+
|| deadlineAtMs <= Date.now()
|
|
435
|
+
|| worker?.kind !== 'external' || worker.group !== invocation.selected.group
|
|
436
|
+
|| worker.member !== invocation.selected.member || worker.cli !== invocation.selected.cli
|
|
437
|
+
|| worker.model !== workerModelKey(invocation.selected.inferenceProvider, invocation.selected.model)
|
|
438
|
+
|| reservation?.tokens != null && !(invocation.limits.tokens <= reservation.tokens)
|
|
439
|
+
|| reservation?.costUsd != null && !(invocation.limits.costUsd !== null && invocation.limits.costUsd <= reservation.costUsd)
|
|
440
|
+
|| registered?.managedAdmission?.requestId !== requestId || registered.managedAdmission.generation !== generation
|
|
441
|
+
|| registered.managedWorker?.invocationPath !== options.invocationPath
|
|
442
|
+
|| registered.managedWorker?.runDir !== options.runDir) {
|
|
443
|
+
throw new Error('Managed worker launch differs from the current claimed admission');
|
|
444
|
+
}
|
|
445
|
+
if (record.execution) {
|
|
446
|
+
const identity = sampleWorkerIdentity(process.pid);
|
|
447
|
+
if (record.execution.pid !== process.pid || !identity?.bootId || !identity.startTicks
|
|
448
|
+
|| record.execution.startedAt !== `${identity.bootId}:${identity.startTicks}:${identity.uid}`) {
|
|
449
|
+
throw new Error('Managed worker supervisor identity differs');
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return { ...record, deadlineAtMs };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
async function workerDispatchPath(filename) {
|
|
456
|
+
let candidate = filename;
|
|
457
|
+
while (dirname(candidate) !== candidate) {
|
|
458
|
+
try {
|
|
459
|
+
if (!(await lstat(candidate)).isDirectory()) return filename;
|
|
460
|
+
await access(candidate, constants.R_OK | constants.X_OK);
|
|
461
|
+
return candidate;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
if (error.code !== 'ENOENT') return filename;
|
|
464
|
+
candidate = dirname(candidate);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
// Keep an unusable path for fail-closed sampling rather than falling back to /.
|
|
468
|
+
return filename;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function workerDispatchRefusal(gate) {
|
|
472
|
+
const bounded = value => String(value ?? '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 256);
|
|
473
|
+
const reasons = (gate.failures ?? []).slice(0, 8)
|
|
474
|
+
.map(failure => `${bounded(failure.signal)}: ${bounded(failure.reason)}`).join('; ').slice(0, 1024);
|
|
475
|
+
return new Error(`Worker dispatch resource gate refused launch${reasons ? `: ${reasons}` : ''}`);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function workerLaunchRefusal(outcome) {
|
|
479
|
+
const bounded = value => String(value ?? '').replace(/[\x00-\x1f\x7f]/g, ' ').slice(0, 256);
|
|
480
|
+
const reasons = (outcome?.failures ?? []).slice(0, 8)
|
|
481
|
+
.map(failure => `${bounded(failure.signal)}: ${bounded(failure.reason)}`);
|
|
482
|
+
for (const detail of [outcome?.diagnostic, outcome?.error]) {
|
|
483
|
+
if (detail) reasons.push(bounded(detail));
|
|
484
|
+
}
|
|
485
|
+
return new Error(reasons.length
|
|
486
|
+
? `Worker launch refused: ${reasons.join('; ').slice(0, 1024)}`
|
|
487
|
+
: 'Worker launch did not return an attached child');
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Only the admitted direct owner can supply inputs for an automatic correction.
|
|
491
|
+
// Neither worker output nor a caller-supplied result can populate this context.
|
|
492
|
+
const directStartupCorrectionContext = new WeakMap();
|
|
493
|
+
|
|
416
494
|
async function launchOneWorker(owner, options) {
|
|
417
495
|
const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan: validate } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
|
|
418
496
|
const { qualificationDigest } = await importImpl(import.meta.url, 'models/qualification-digest.mjs');
|
|
@@ -425,6 +503,11 @@ async function launchOneWorker(owner, options) {
|
|
|
425
503
|
const clock = () => typeof options.now === 'function' ? options.now() : options.now ?? new Date().toISOString();
|
|
426
504
|
const now = clock();
|
|
427
505
|
const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now);
|
|
506
|
+
if (invocation.mode === 'managed' || options.managedAdmission) {
|
|
507
|
+
const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
|
|
508
|
+
options = { ...options, scopedBreaker: null, managedInvocationId: admitted.metadata.invocationId,
|
|
509
|
+
managedDeadlineAtMs: admitted.deadlineAtMs };
|
|
510
|
+
}
|
|
428
511
|
if (invocation.transport !== 'shell') throw new Error('One-shot capture requires shell transport; use the qualified MCP descriptor directly');
|
|
429
512
|
if (process.platform !== 'linux') throw new Error('One-shot worker capture requires qualified Linux process identity');
|
|
430
513
|
const runDir = options.runDir;
|
|
@@ -475,9 +558,24 @@ async function launchOneWorker(owner, options) {
|
|
|
475
558
|
// document on the stdin pipe, never brief text in argv (#2911).
|
|
476
559
|
const { workerStdinBytes } = await importImpl(import.meta.url, 'serve/worker-stdin-frame.mjs');
|
|
477
560
|
const stdinBytes = workerStdinBytes(invocation, brief, inputs.drivers[invocation.selected.cli]);
|
|
478
|
-
let gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config
|
|
479
|
-
|
|
480
|
-
|
|
561
|
+
let gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
|
|
562
|
+
paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
|
|
563
|
+
if (!gate.ok) throw workerDispatchRefusal(gate);
|
|
564
|
+
const { WorkerPolicyError, assertWorkerLaunchMode } = await importImpl(import.meta.url, 'models/provider-policy.js');
|
|
565
|
+
const permissions = inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution ?? {};
|
|
566
|
+
const adapter = inputs.drivers[invocation.selected.cli].qualification.adapter;
|
|
567
|
+
assertWorkerLaunchMode(adapter, permissions);
|
|
568
|
+
if (options.isolation?.mode === 'native-sandbox' && invocation.isolation !== 'native-sandbox') {
|
|
569
|
+
throw new WorkerPolicyError('OG_POLICY_RELAXATION', 'native-sandbox requires the pinned project execution policy');
|
|
570
|
+
}
|
|
571
|
+
const isolation = invocation.isolation === 'native-sandbox' ? { mode: 'native-sandbox' }
|
|
572
|
+
: options.isolation ?? detectWorkerIsolation();
|
|
573
|
+
if (isolation.mode === 'systemd-unit' && workerNeedsNativeNamespaces(adapter, permissions)) {
|
|
574
|
+
const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ cwd: invocation.cwd,
|
|
575
|
+
envNames: invocation.envNames, env: Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
|
|
576
|
+
.map(name => [name, process.env[name]])) });
|
|
577
|
+
if (!probe.ok) throw new WorkerPolicyError(NATIVE_SANDBOX_UNAVAILABLE, nativeSandboxProbeMessage(probe));
|
|
578
|
+
}
|
|
481
579
|
if (isolation.mode === 'degraded' && !options.allowDegraded) {
|
|
482
580
|
throw new Error('Worker isolation unavailable; explicitly select --allow-degraded-worker or use manual observation');
|
|
483
581
|
}
|
|
@@ -570,6 +668,7 @@ async function launchOneWorker(owner, options) {
|
|
|
570
668
|
// worker-writable captureRoot fell back to the in-process ledger) is
|
|
571
669
|
// folded in up front so every exit path reports it.
|
|
572
670
|
const ledgerDiagnostics = admission.ledgerDiagnostic ? [admission.ledgerDiagnostic] : [];
|
|
671
|
+
const startupQualification = structuredClone(inputs.drivers[invocation.selected.cli].qualification);
|
|
573
672
|
const settleLedger = async (outcome) => {
|
|
574
673
|
const diagnostic = await settleDirectWorker(admission, outcome);
|
|
575
674
|
if (diagnostic) ledgerDiagnostics.push(diagnostic);
|
|
@@ -653,11 +752,22 @@ async function launchOneWorker(owner, options) {
|
|
|
653
752
|
// writes; `.catch(() => {})` keeps a prior failure from poisoning it, and
|
|
654
753
|
// the per-call promise returned to the caller still reflects this write's
|
|
655
754
|
// own outcome.
|
|
755
|
+
let latestProcessRecord = null;
|
|
656
756
|
const publish = (name, value) => {
|
|
757
|
+
if (name === 'process.json') {
|
|
758
|
+
value = { ...value, recovery: { ...admission.breaker.recoveryStatus(), observedAt: new Date().toISOString() } };
|
|
759
|
+
if (options.startupCorrectionRecord) value.startupCorrection = options.startupCorrectionRecord;
|
|
760
|
+
latestProcessRecord = value;
|
|
761
|
+
}
|
|
657
762
|
const write = publishing.catch(() => {}).then(() => writeRecord(runDir, name, value, inspect));
|
|
658
763
|
publishing = write.catch(error => { publishFailures.push({ name, error: error?.message ?? String(error) }); });
|
|
659
764
|
return write;
|
|
660
765
|
};
|
|
766
|
+
const publishRecovery = () => {
|
|
767
|
+
if (latestProcessRecord) return publish('process.json', latestProcessRecord);
|
|
768
|
+
};
|
|
769
|
+
const onScopeTransition = () => { publishRecovery()?.catch(() => {}); };
|
|
770
|
+
admission.breaker.on('transition', onScopeTransition);
|
|
661
771
|
const handles = [];
|
|
662
772
|
let outcome;
|
|
663
773
|
let artifacts;
|
|
@@ -692,7 +802,7 @@ async function launchOneWorker(owner, options) {
|
|
|
692
802
|
prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
|
|
693
803
|
worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
|
|
694
804
|
health: invocation.health ?? null, transport: invocation.transport,
|
|
695
|
-
isolation: { mode: isolation.mode, memoryMaxGb: options.memoryMaxGb ?? 4 },
|
|
805
|
+
isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
|
|
696
806
|
limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
|
|
697
807
|
await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
|
|
698
808
|
launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
|
|
@@ -704,8 +814,9 @@ async function launchOneWorker(owner, options) {
|
|
|
704
814
|
// Recheck policy, prompt, installation and exact bindings after file preparation.
|
|
705
815
|
inputs = await loadInputs();
|
|
706
816
|
await validate(saved, inputs.snapshot, inputs.drivers, clock());
|
|
707
|
-
gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config
|
|
708
|
-
|
|
817
|
+
gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
|
|
818
|
+
paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
|
|
819
|
+
if (!gate.ok) throw workerDispatchRefusal(gate);
|
|
709
820
|
if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
|
|
710
821
|
const environment = Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
|
|
711
822
|
.map(name => [name, process.env[name]]));
|
|
@@ -766,12 +877,12 @@ async function launchOneWorker(owner, options) {
|
|
|
766
877
|
// would in production).
|
|
767
878
|
const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
|
|
768
879
|
command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
|
|
769
|
-
memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation,
|
|
880
|
+
memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation, workerGroup: invocation.selected.group,
|
|
770
881
|
allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
|
|
771
882
|
env: environment, envNames: invocation.envNames, stdin: invocation.stdin, runDir,
|
|
772
883
|
...(onDispatchCommit ? { onDispatchCommit } : {}) });
|
|
773
884
|
outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
|
|
774
|
-
if (!outcome
|
|
885
|
+
if (!outcome?.child) throw workerLaunchRefusal(outcome);
|
|
775
886
|
// #2915 review 4 (finding 3): when THIS attempt is the admitted canary
|
|
776
887
|
// for one or more covering scopes, execution itself must be clipped to
|
|
777
888
|
// the canary's own deadline (min(task deadline, admission+300s)) — not
|
|
@@ -819,6 +930,8 @@ async function launchOneWorker(owner, options) {
|
|
|
819
930
|
// accumulated across this attempt's lifecycle on the returned result,
|
|
820
931
|
// mirroring `publishFailures` — never silently discarded.
|
|
821
932
|
if (ledgerDiagnostics.length && result) result.ledgerDiagnostics = ledgerDiagnostics.slice();
|
|
933
|
+
directStartupCorrectionContext.set(owner, { saved, invocation, loadInputs, clock, dispatchGuard,
|
|
934
|
+
ledger: admission.ledger, inspect, qualification: startupQualification });
|
|
822
935
|
return result;
|
|
823
936
|
} catch (error) {
|
|
824
937
|
if (!outcome?.child) {
|
|
@@ -831,7 +944,7 @@ async function launchOneWorker(owner, options) {
|
|
|
831
944
|
// canary deadline.
|
|
832
945
|
await settleLedger({ kind: 'uncertain', confirmedStop: true });
|
|
833
946
|
await publish('exit.json', { invocationId, exitCode: null, signal: null,
|
|
834
|
-
endedAt: new Date().toISOString(), termination: 'confirmed', descendants: 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
|
|
947
|
+
endedAt: new Date().toISOString(), isolation: isolation.mode, termination: 'confirmed', descendants: 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
|
|
835
948
|
} else if (admission.gated) {
|
|
836
949
|
// #2915 review 5 (finding 2a, "leaked capacity"), hardened by #2915
|
|
837
950
|
// review 6 (finding 4, "capture-setup exception after spawn"): a
|
|
@@ -890,6 +1003,8 @@ async function launchOneWorker(owner, options) {
|
|
|
890
1003
|
if (ledgerDiagnostics.length) error.ledgerDiagnostics = ledgerDiagnostics;
|
|
891
1004
|
throw error;
|
|
892
1005
|
} finally {
|
|
1006
|
+
admission.breaker.removeListener('transition', onScopeTransition);
|
|
1007
|
+
await publishing;
|
|
893
1008
|
try { await artifacts?.cleanup(); } finally {
|
|
894
1009
|
// Never chmod a worker-replaced pathname; only the inodes we opened.
|
|
895
1010
|
for (const handle of handles) { await handle.chmod(0o600).catch(() => {}); await handle.close().catch(() => {}); }
|
|
@@ -900,6 +1015,38 @@ async function launchOneWorker(owner, options) {
|
|
|
900
1015
|
}
|
|
901
1016
|
}
|
|
902
1017
|
|
|
1018
|
+
// #3489: process exit/close notification can be observed before the final
|
|
1019
|
+
// pipe bytes are delivered as stream 'data' events (the same class as #3488,
|
|
1020
|
+
// which waited for IPC disconnect as well as exit). The child 'data'
|
|
1021
|
+
// handlers are the ONLY path that feeds the health observer and accumulates
|
|
1022
|
+
// the `output` buffer adapter extraction consumes, so settling health on
|
|
1023
|
+
// the process-level event alone classifies a completed exit-0 run as
|
|
1024
|
+
// `failed` (missing terminal event, FINAL_INVALID) whenever it wins the
|
|
1025
|
+
// race under load. Drain any already-buffered stdout/stderr bytes — bounded
|
|
1026
|
+
// by the termination grace — before destroying the source streams and
|
|
1027
|
+
// snapshotting output/observer state. Streams with no buffered bytes
|
|
1028
|
+
// resolve immediately, so never-ending stdio (timer-faked stand-ins, held
|
|
1029
|
+
// pipes) never delays settle; the bound only covers genuinely in-flight
|
|
1030
|
+
// data. The timer stays referenced so a bounded drain always completes,
|
|
1031
|
+
// mirroring the already-referenced exit-grace `later()` timer.
|
|
1032
|
+
function drainChildStdio(child, timeoutMs) {
|
|
1033
|
+
const pending = [];
|
|
1034
|
+
for (const stream of [child?.stdout, child?.stderr]) {
|
|
1035
|
+
if (!stream || typeof stream.once !== 'function' || stream.readableEnded || stream.destroyed) continue;
|
|
1036
|
+
if (!(stream.readableLength > 0)) continue;
|
|
1037
|
+
try { stream.resume?.(); } catch { continue; }
|
|
1038
|
+
pending.push(new Promise(resolve => {
|
|
1039
|
+
const done = () => { clearTimeout(timer); resolve(); };
|
|
1040
|
+
const timer = setTimeout(done, Math.max(0, timeoutMs ?? 0));
|
|
1041
|
+
stream.once('end', done);
|
|
1042
|
+
stream.once('close', done);
|
|
1043
|
+
stream.once('error', done);
|
|
1044
|
+
}));
|
|
1045
|
+
}
|
|
1046
|
+
if (!pending.length) return Promise.resolve();
|
|
1047
|
+
return Promise.all(pending);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
903
1050
|
async function captureOneWorker(owner, ctx) {
|
|
904
1051
|
const { child, outcome, invocation, invocationId, supervisor, launchTime, monotonicStart, publish, paths, handles, options,
|
|
905
1052
|
canaryDeadlineMs = null, canaryNowMs = () => Date.now(), recoveryGated = false } = ctx;
|
|
@@ -968,7 +1115,16 @@ async function captureOneWorker(owner, ctx) {
|
|
|
968
1115
|
};
|
|
969
1116
|
const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
|
|
970
1117
|
const signal = value => {
|
|
971
|
-
if (settled
|
|
1118
|
+
if (settled) return false;
|
|
1119
|
+
let delivered = signalOwnedWorker(identity, value, { supervisor });
|
|
1120
|
+
// Startup can consume the whole deadline before the transport spawns
|
|
1121
|
+
// its worker or publishes an owned group. We still own this attached
|
|
1122
|
+
// ChildProcess: stop it before it can launch work after finalization.
|
|
1123
|
+
// This proves no descendant termination; degraded mode stays uncertain.
|
|
1124
|
+
if (!delivered && !identity && hasTransport && outcome.mode !== 'systemd-unit' && !sawExit) {
|
|
1125
|
+
try { delivered = signalChild(child, value); } catch { /* already exited or no child handle */ }
|
|
1126
|
+
}
|
|
1127
|
+
if (!delivered) return false;
|
|
972
1128
|
deliveredSignal = value;
|
|
973
1129
|
return true;
|
|
974
1130
|
};
|
|
@@ -1007,8 +1163,19 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1007
1163
|
}
|
|
1008
1164
|
signal('SIGTERM');
|
|
1009
1165
|
const remainingGrace = Math.min(graceMs, hardRemaining());
|
|
1010
|
-
|
|
1011
|
-
|
|
1166
|
+
const escalate = () => {
|
|
1167
|
+
signal('SIGKILL');
|
|
1168
|
+
// Startup may have no worker identity yet. Wait for our transport's
|
|
1169
|
+
// exit status before settling: TERM can have killed it even if KILL
|
|
1170
|
+
// was also delivered before Node observes exit (#3532). Use the unused
|
|
1171
|
+
// grace plus the existing 100ms settle tick, keeping the total
|
|
1172
|
+
// termination budget at graceMs + 100 on the tracked launcher timers.
|
|
1173
|
+
const exitWaitMs = !identity && hasTransport && outcome.mode !== 'systemd-unit'
|
|
1174
|
+
? graceMs - remainingGrace + 100 : 100;
|
|
1175
|
+
later(() => finish(), exitWaitMs);
|
|
1176
|
+
};
|
|
1177
|
+
if (remainingGrace === 0) escalate();
|
|
1178
|
+
else later(escalate, remainingGrace);
|
|
1012
1179
|
};
|
|
1013
1180
|
owner.currentProcess = child;
|
|
1014
1181
|
owner.workerStop = stop;
|
|
@@ -1023,9 +1190,15 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1023
1190
|
owner.emit('stdout', chunk);
|
|
1024
1191
|
});
|
|
1025
1192
|
let stderrBytes = 0;
|
|
1193
|
+
let sandboxStderrTail = '';
|
|
1026
1194
|
let recoveryReason = null;
|
|
1027
1195
|
child.stderr.on('data', chunk => {
|
|
1028
1196
|
observer.feed(chunk, { kind: 'stderr', id: 'stderr', generation: 0, offset: stderrOffset }); stderrOffset += chunk.length;
|
|
1197
|
+
if (workerNeedsNativeNamespaces(driver.qualification.adapter, ctx.inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution)) {
|
|
1198
|
+
const observedStderr = sandboxStderrTail + chunk.toString('utf8');
|
|
1199
|
+
if (nativeSandboxFailure(observedStderr)) errorCode = NATIVE_SANDBOX_UNAVAILABLE;
|
|
1200
|
+
sandboxStderrTail = observedStderr.slice(-1024);
|
|
1201
|
+
}
|
|
1029
1202
|
const hint = classifyWorkerHealthFailure(chunk.toString('utf8'), { source: { kind: 'stderr' } })?.reasonCode;
|
|
1030
1203
|
if (['RATE_LIMIT', 'NETWORK'].includes(hint)) recoveryReason = hint;
|
|
1031
1204
|
const remaining = Math.max(0, 10485760 - stderrBytes);
|
|
@@ -1055,6 +1228,10 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1055
1228
|
timers.clear();
|
|
1056
1229
|
process.off('SIGTERM', onSignal); process.off('SIGINT', onSignal);
|
|
1057
1230
|
owner.currentProcess = null; owner.workerStop = null;
|
|
1231
|
+
// #3489: drain in-flight stdio before destroying the source streams —
|
|
1232
|
+
// destroying first discards bytes never delivered as 'data', settling
|
|
1233
|
+
// health on a partial stream (exit observed before drain).
|
|
1234
|
+
await drainChildStdio(child, graceMs);
|
|
1058
1235
|
child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
|
|
1059
1236
|
for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
|
|
1060
1237
|
const native = outcome.mode === 'systemd-unit' ? readNative() : null;
|
|
@@ -1415,12 +1592,15 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1415
1592
|
|
|
1416
1593
|
// ===========================================================================
|
|
1417
1594
|
// BASED ON a4085b86a (#2915 round 8, D6).
|
|
1418
|
-
//
|
|
1419
|
-
// respects the winning stop cause
|
|
1595
|
+
// Shared lifecycle fixes: finish() snapshots timeout classification before
|
|
1596
|
+
// cleanup and respects the winning stop cause; stop() can signal the owned
|
|
1597
|
+
// transport before worker identity is available, and then uses the unused
|
|
1598
|
+
// termination grace to observe its exit before falling back to the last
|
|
1599
|
+
// delivered signal (#3532).
|
|
1420
1600
|
// The test/fixtures frozen reference remains unchanged.
|
|
1421
1601
|
//
|
|
1422
1602
|
// `launchOneWorkerBaseline` and `captureOneWorkerBaseline` reproduce
|
|
1423
|
-
// `launchOneWorker`/`captureOneWorker`, apart from the
|
|
1603
|
+
// `launchOneWorker`/`captureOneWorker`, apart from the lifecycle fixes above,
|
|
1424
1604
|
// as they existed at base commit `a4085b86a` (`git show
|
|
1425
1605
|
// a4085b86a:tools/ralph-external/session-launcher.mjs`) — before any
|
|
1426
1606
|
// #2915 direct-recovery code existed in this file. `launchOneWorker`
|
|
@@ -1472,7 +1652,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1472
1652
|
if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
|
|
1473
1653
|
await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
1474
1654
|
try { await handle.chmod(0o700); } finally { await handle.close(); } });
|
|
1475
|
-
const invocationId = randomUUID();
|
|
1655
|
+
const invocationId = options.managedAdmission ? options.managedInvocationId : randomUUID();
|
|
1476
1656
|
const reservation = await withWorkerPath(join(runDir, '.launch-owner'), target => open(target, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600));
|
|
1477
1657
|
try { await reservation.writeFile(invocationId); await reservation.sync(); } finally { await reservation.close(); }
|
|
1478
1658
|
const supervisor = sampleWorkerIdentity(process.pid);
|
|
@@ -1492,11 +1672,12 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1492
1672
|
const handles = [];
|
|
1493
1673
|
let outcome;
|
|
1494
1674
|
let artifacts;
|
|
1675
|
+
let managedSpawnAttempted = false;
|
|
1495
1676
|
try {
|
|
1496
1677
|
artifacts = await materializeWorkerArtifacts(invocation, runDir);
|
|
1497
1678
|
const displayArgv = artifacts.argv.map((token, i) => i === 0 ? token : `<argument:${bytesDigest(token)}>`);
|
|
1498
1679
|
await publish('invocation.json', { version: 1, origin: 'launcher', invocationId, attemptId: invocationId,
|
|
1499
|
-
previousAttemptId: verifiedPreviousAttemptId ?? null, requestId: invocationId, taskDigest: qualificationDigest(saved.request), role: saved.request.role,
|
|
1680
|
+
previousAttemptId: verifiedPreviousAttemptId ?? null, requestId: options.managedAdmission?.requestId ?? invocationId, taskDigest: qualificationDigest(saved.request), role: saved.request.role,
|
|
1500
1681
|
selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
|
|
1501
1682
|
policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
|
|
1502
1683
|
expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
|
|
@@ -1509,7 +1690,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1509
1690
|
prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
|
|
1510
1691
|
worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
|
|
1511
1692
|
health: invocation.health ?? null, transport: invocation.transport,
|
|
1512
|
-
isolation: { mode: isolation.mode, memoryMaxGb: options.memoryMaxGb ?? 4 },
|
|
1693
|
+
isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
|
|
1513
1694
|
limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
|
|
1514
1695
|
await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
|
|
1515
1696
|
launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
|
|
@@ -1521,8 +1702,9 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1521
1702
|
// Recheck policy, prompt, installation and exact bindings after file preparation.
|
|
1522
1703
|
inputs = await loadInputs();
|
|
1523
1704
|
await validate(saved, inputs.snapshot, inputs.drivers, clock());
|
|
1524
|
-
localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config
|
|
1525
|
-
|
|
1705
|
+
localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
|
|
1706
|
+
paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
|
|
1707
|
+
if (!localGate.ok) throw workerDispatchRefusal(localGate);
|
|
1526
1708
|
if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
|
|
1527
1709
|
const environment = Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
|
|
1528
1710
|
.map(name => [name, process.env[name]]));
|
|
@@ -1539,16 +1721,24 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1539
1721
|
const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
|
|
1540
1722
|
command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
|
|
1541
1723
|
memoryMaxGb: options.memoryMaxGb ?? 4, gate: localGate, config: inputs.snapshot.config, isolation,
|
|
1724
|
+
workerGroup: invocation.selected.group,
|
|
1542
1725
|
allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
|
|
1726
|
+
...(options.managedAdmission ? { onDispatchCommit: async () => {
|
|
1727
|
+
const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
|
|
1728
|
+
options.managedDeadlineAtMs = Math.min(options.managedDeadlineAtMs, admitted.deadlineAtMs);
|
|
1729
|
+
} } : {}),
|
|
1730
|
+
...(options.managedAdmission ? { onSpawnAttempt: () => { managedSpawnAttempted = true; } } : {}),
|
|
1543
1731
|
env: environment, envNames: invocation.envNames, stdin: invocation.stdin, runDir });
|
|
1544
1732
|
outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
|
|
1545
|
-
if (!outcome
|
|
1733
|
+
if (!outcome?.child) throw workerLaunchRefusal(outcome);
|
|
1546
1734
|
return await captureOneWorkerBaseline(owner, { child: outcome.child, outcome, invocation, invocationId, supervisor,
|
|
1547
1735
|
launchTime, monotonicStart, publish, publishFailures, paths, handles, inspect, inputs, profile, baselineContent, options,
|
|
1548
1736
|
brief: ctx.brief, stdinBytes: ctx.stdinBytes, runDir });
|
|
1549
1737
|
} catch (error) {
|
|
1550
1738
|
if (!outcome?.child) await publish('exit.json', { invocationId, exitCode: null, signal: null,
|
|
1551
|
-
|
|
1739
|
+
...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
|
|
1740
|
+
endedAt: new Date().toISOString(), isolation: isolation.mode, termination: managedSpawnAttempted ? 'uncertain' : 'confirmed',
|
|
1741
|
+
descendants: managedSpawnAttempted ? 'unknown' : 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
|
|
1552
1742
|
throw error;
|
|
1553
1743
|
} finally {
|
|
1554
1744
|
try { await artifacts?.cleanup(); } finally {
|
|
@@ -1588,9 +1778,13 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1588
1778
|
let sawClose = false;
|
|
1589
1779
|
const driver = ctx.inputs.drivers[invocation.selected.cli];
|
|
1590
1780
|
const observationOptions = { provider: driver.qualification.adapter.provider, profile: ctx.profile,
|
|
1591
|
-
effectiveHealth:
|
|
1781
|
+
effectiveHealth: options.managedAdmission ? { ...invocation.health,
|
|
1782
|
+
hardTimeoutSeconds: Math.min(invocation.health?.hardTimeoutSeconds ?? invocation.limits.wallSeconds,
|
|
1783
|
+
(options.managedDeadlineAtMs - Date.parse(launchTime)) / 1000) } : invocation.health,
|
|
1784
|
+
invocationId, startedAt: Number(BigInt(monotonicStart)) / 1e6,
|
|
1592
1785
|
launchTime, contentDigest: ctx.baselineContent, sessionId: options.healthSessionLog?.sessionId ?? null,
|
|
1593
|
-
runId: options.healthSessionLog?.runId ?? null, maxOutputBytes: invocation.maxOutputBytes
|
|
1786
|
+
runId: options.healthSessionLog?.runId ?? null, maxOutputBytes: invocation.maxOutputBytes,
|
|
1787
|
+
...(options.managedAdmission ? { observerGeneration: options.managedAdmission.generation, requestId: options.managedAdmission.requestId } : {}) };
|
|
1594
1788
|
const observer = driver.createHealthObserver?.(observationOptions) ?? createWorkerHealthObserver(observationOptions);
|
|
1595
1789
|
const tickRate = qualifyWorkerCpuTickRate();
|
|
1596
1790
|
let previousCpu = null;
|
|
@@ -1611,7 +1805,16 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1611
1805
|
};
|
|
1612
1806
|
const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
|
|
1613
1807
|
const signal = value => {
|
|
1614
|
-
if (settled
|
|
1808
|
+
if (settled) return false;
|
|
1809
|
+
let delivered = signalOwnedWorker(identity, value, { supervisor });
|
|
1810
|
+
// Startup can consume the whole deadline before the transport spawns
|
|
1811
|
+
// its worker or publishes an owned group. We still own this attached
|
|
1812
|
+
// ChildProcess: stop it before it can launch work after finalization.
|
|
1813
|
+
// This proves no descendant termination; degraded mode stays uncertain.
|
|
1814
|
+
if (!delivered && !identity && hasTransport && outcome.mode !== 'systemd-unit' && !sawExit) {
|
|
1815
|
+
try { delivered = signalChild(child, value); } catch { /* already exited or no child handle */ }
|
|
1816
|
+
}
|
|
1817
|
+
if (!delivered) return false;
|
|
1615
1818
|
deliveredSignal = value;
|
|
1616
1819
|
return true;
|
|
1617
1820
|
};
|
|
@@ -1640,8 +1843,19 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1640
1843
|
}
|
|
1641
1844
|
signal('SIGTERM');
|
|
1642
1845
|
const remainingGrace = Math.min(graceMs, hardRemaining());
|
|
1643
|
-
|
|
1644
|
-
|
|
1846
|
+
const escalate = () => {
|
|
1847
|
+
signal('SIGKILL');
|
|
1848
|
+
// Startup may have no worker identity yet. Wait for our transport's
|
|
1849
|
+
// exit status before settling: TERM can have killed it even if KILL
|
|
1850
|
+
// was also delivered before Node observes exit (#3532). Use the unused
|
|
1851
|
+
// grace plus the existing 100ms settle tick, keeping the total
|
|
1852
|
+
// termination budget at graceMs + 100 on the tracked launcher timers.
|
|
1853
|
+
const exitWaitMs = !identity && hasTransport && outcome.mode !== 'systemd-unit'
|
|
1854
|
+
? graceMs - remainingGrace + 100 : 100;
|
|
1855
|
+
later(() => finish(), exitWaitMs);
|
|
1856
|
+
};
|
|
1857
|
+
if (remainingGrace === 0) escalate();
|
|
1858
|
+
else later(escalate, remainingGrace);
|
|
1645
1859
|
};
|
|
1646
1860
|
owner.currentProcess = child;
|
|
1647
1861
|
owner.workerStop = stop;
|
|
@@ -1656,8 +1870,14 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1656
1870
|
owner.emit('stdout', chunk);
|
|
1657
1871
|
});
|
|
1658
1872
|
let stderrBytes = 0;
|
|
1873
|
+
let sandboxStderrTail = '';
|
|
1659
1874
|
child.stderr.on('data', chunk => {
|
|
1660
1875
|
observer.feed(chunk, { kind: 'stderr', id: 'stderr', generation: 0, offset: stderrOffset }); stderrOffset += chunk.length;
|
|
1876
|
+
if (workerNeedsNativeNamespaces(driver.qualification.adapter, ctx.inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution)) {
|
|
1877
|
+
const observedStderr = sandboxStderrTail + chunk.toString('utf8');
|
|
1878
|
+
if (nativeSandboxFailure(observedStderr)) errorCode = NATIVE_SANDBOX_UNAVAILABLE;
|
|
1879
|
+
sandboxStderrTail = observedStderr.slice(-1024);
|
|
1880
|
+
}
|
|
1661
1881
|
const remaining = Math.max(0, 10485760 - stderrBytes);
|
|
1662
1882
|
streams[1].write(chunk.subarray(0, remaining)); streams[3].write(chunk.subarray(0, remaining)); stderrBytes += chunk.length;
|
|
1663
1883
|
if (stderrBytes > 10485760) { errorCode = 'OUTPUT_LIMIT'; stop(); }
|
|
@@ -1685,6 +1905,11 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1685
1905
|
timers.clear();
|
|
1686
1906
|
process.off('SIGTERM', onSignal); process.off('SIGINT', onSignal);
|
|
1687
1907
|
owner.currentProcess = null; owner.workerStop = null;
|
|
1908
|
+
// #3489: drain in-flight stdio before destroying the source streams —
|
|
1909
|
+
// destroying first discards bytes never delivered as 'data', settling
|
|
1910
|
+
// health on a partial stream (exit observed before drain). Ordering-only;
|
|
1911
|
+
// recovery semantics in this frozen baseline are unchanged.
|
|
1912
|
+
await drainChildStdio(child, graceMs);
|
|
1688
1913
|
child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
|
|
1689
1914
|
for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
|
|
1690
1915
|
const native = outcome.mode === 'systemd-unit' ? readNative() : null;
|
|
@@ -1707,6 +1932,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1707
1932
|
const exitCode = resultSignal ? null
|
|
1708
1933
|
: outcome.mode === 'systemd-unit' && native?.execMainCode === 1 ? native.execMainStatus : waitCode;
|
|
1709
1934
|
const result = { invocationId, exitCode, signal: resultSignal,
|
|
1935
|
+
...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
|
|
1710
1936
|
endedAt: new Date().toISOString(), termination: confirmed ? 'confirmed' : 'uncertain',
|
|
1711
1937
|
descendants: confirmed ? 'stopped' : 'unknown', timedOut: resultTimedOut,
|
|
1712
1938
|
cancelled: stopReason === 'CANCELLED', stopReason, errorCode: errorCode ?? null, capturedBytes: Math.min(total, output.length),
|
|
@@ -1772,8 +1998,11 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1772
1998
|
sawExit = true; waitCode = code; waitSignal = sig; later(() => finish(), graceMs);
|
|
1773
1999
|
});
|
|
1774
2000
|
child.once('close', () => { sawClose = true; finish(); });
|
|
1775
|
-
const hardRemaining = () => Math.max(0, Math.min(
|
|
1776
|
-
invocation.limits.wallSeconds
|
|
2001
|
+
const hardRemaining = () => Math.max(0, Math.min(
|
|
2002
|
+
Math.min(invocation.health?.hardTimeoutSeconds ?? invocation.limits.wallSeconds, invocation.limits.wallSeconds,
|
|
2003
|
+
options.managedAdmission ? (options.managedDeadlineAtMs - Date.parse(launchTime)) / 1000 : Infinity) * 1000
|
|
2004
|
+
- (Number(process.hrtime.bigint() - BigInt(monotonicStart)) / 1e6),
|
|
2005
|
+
options.managedAdmission ? options.managedDeadlineAtMs - Date.now() : Infinity));
|
|
1777
2006
|
later(() => {
|
|
1778
2007
|
observer.check();
|
|
1779
2008
|
reportHealth(); stop('HARD_DEADLINE');
|
|
@@ -3707,6 +3936,102 @@ export function classifyAttemptForSettlement(invocation, result, { quotaAccount
|
|
|
3707
3936
|
inferenceProvider: selected.inferenceProvider ?? null, quotaAccount, installationQualifier };
|
|
3708
3937
|
}
|
|
3709
3938
|
|
|
3939
|
+
/** Refresh only a demonstrated stale executable binding. Installation, login,
|
|
3940
|
+
* permissions, models, working directories and policy are never repaired here.
|
|
3941
|
+
* One retry shares the original task clock and the existing atomic ledgers. */
|
|
3942
|
+
async function correctDirectWorkerStartup(owner, options, previous) {
|
|
3943
|
+
const context = directStartupCorrectionContext.get(owner);
|
|
3944
|
+
if (!context || options.correctStartup !== undefined || previous?.errorCode !== 'ENOENT'
|
|
3945
|
+
|| previous.termination !== 'confirmed' || previous.exitCode != null || previous.signal != null
|
|
3946
|
+
|| previous.cancelled || previous.timedOut || previous.publishFailures?.length
|
|
3947
|
+
|| classifyLaunchOutcomeForRecovery(previous).reasonCode !== 'NOT_FOUND') return previous;
|
|
3948
|
+
const { saved, invocation, loadInputs, clock, dispatchGuard, ledger, inspect, qualification } = context;
|
|
3949
|
+
const record = { action: 'refresh-executable-binding', reasonCode: 'NOT_FOUND', state: 'stopped',
|
|
3950
|
+
previousInvocationId: previous.invocationId };
|
|
3951
|
+
const recordAt = async (runDir, invocationId) => {
|
|
3952
|
+
const processRecord = JSON.parse((await boundedPrivateRead(join(runDir, 'process.json'), 65536, inspect)).toString('utf8'));
|
|
3953
|
+
if (processRecord.origin !== 'launcher' || processRecord.invocationId !== invocationId) throw new Error('Correction record identity changed');
|
|
3954
|
+
await (options.publishRecord ?? publishWorkerRecord)(runDir, 'process.json', { ...processRecord, startupCorrection: { ...record } }, inspect);
|
|
3955
|
+
};
|
|
3956
|
+
const stop = async code => {
|
|
3957
|
+
record.stopCode = code;
|
|
3958
|
+
previous.startupCorrection = { ...record };
|
|
3959
|
+
await recordAt(options.runDir, previous.invocationId);
|
|
3960
|
+
return previous;
|
|
3961
|
+
};
|
|
3962
|
+
const guardStop = () => dispatchGuard?.isCancelled?.() === true ? 'CANCELLED'
|
|
3963
|
+
: dispatchGuard?.deadlineAtMs != null && (dispatchGuard.nowMs?.() ?? Date.now()) >= dispatchGuard.deadlineAtMs ? 'DEADLINE' : null;
|
|
3964
|
+
if (ledger?.n >= ledger?.maxAttempts) return stop('ATTEMPT_LIMIT');
|
|
3965
|
+
if (guardStop()) return stop(guardStop());
|
|
3966
|
+
let inputs;
|
|
3967
|
+
let plan;
|
|
3968
|
+
const retryId = randomUUID();
|
|
3969
|
+
const finalPath = `${invocation.finalPath}.recovery-${retryId}`;
|
|
3970
|
+
try {
|
|
3971
|
+
inputs = await loadInputs();
|
|
3972
|
+
const { resolveWorkerInvocation } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
|
|
3973
|
+
plan = await resolveWorkerInvocation(inputs.snapshot, { ...saved.request, final_output: finalPath }, inputs.drivers, clock());
|
|
3974
|
+
} catch { return stop('QUALIFICATION_REFUSED'); }
|
|
3975
|
+
if (guardStop()) return stop(guardStop());
|
|
3976
|
+
if (plan?.status !== 'eligible' || plan.mode !== 'direct' || plan.invocation?.readiness !== 'launchable') return stop('QUALIFICATION_REFUSED');
|
|
3977
|
+
if (plan.invocation.argv[0] === invocation.argv[0]) return stop('UNCHANGED_EXECUTABLE');
|
|
3978
|
+
// A freshly requalified installation may have new binary/proof digests.
|
|
3979
|
+
// Preserve every effective adapter declaration and installation boundary.
|
|
3980
|
+
const effectiveQualification = value => {
|
|
3981
|
+
const copy = structuredClone(value);
|
|
3982
|
+
const stripEvidence = evidence => {
|
|
3983
|
+
if (evidence) for (const key of ['scopeDigest', 'fixtureDigest', 'verifiedAt', 'evidenceRef']) delete evidence[key];
|
|
3984
|
+
};
|
|
3985
|
+
stripEvidence(copy.adapter?.qualification);
|
|
3986
|
+
for (const profile of Object.values(copy.adapter?.invocationProfiles ?? {})) stripEvidence(profile.installationQualification);
|
|
3987
|
+
if (copy.installation) {
|
|
3988
|
+
delete copy.installation.executable;
|
|
3989
|
+
delete copy.installation.evidence;
|
|
3990
|
+
}
|
|
3991
|
+
if (copy.stdinFramerBinding) delete copy.stdinFramerBinding.storeDigest;
|
|
3992
|
+
return JSON.stringify(copy);
|
|
3993
|
+
};
|
|
3994
|
+
if (effectiveQualification(qualification) !== effectiveQualification(inputs.drivers[invocation.selected.cli]?.qualification)) return stop('BINDING_CHANGED');
|
|
3995
|
+
// The resolver is authoritative for the replacement executable. Everything
|
|
3996
|
+
// else must remain identical, including qualified selection, policy, limits,
|
|
3997
|
+
// framing, environment, controls, prompt digest and workspace. Only fresh
|
|
3998
|
+
// output names and expiration are expected to differ between attempts.
|
|
3999
|
+
const binding = value => {
|
|
4000
|
+
const copy = structuredClone(value);
|
|
4001
|
+
const replacements = ['stderrPath', 'stdoutPath', 'finalPath'].map(key => [copy[key], `<${key}>`]);
|
|
4002
|
+
const normalize = value => replacements.find(([path]) => path === value)?.[1] ?? value;
|
|
4003
|
+
copy.argv = copy.argv.map((value, index) => index === 0 ? '<executable>' : normalize(value));
|
|
4004
|
+
delete copy.selected.qualificationDigest;
|
|
4005
|
+
for (const key of ['stdoutPath', 'stderrPath', 'finalPath', 'expiresAt', 'shellCommand', 'profileDigest']) delete copy[key];
|
|
4006
|
+
return JSON.stringify(copy);
|
|
4007
|
+
};
|
|
4008
|
+
if (binding(plan.invocation) !== binding(invocation)) return stop('BINDING_CHANGED');
|
|
4009
|
+
const invocationPath = join(options.runDir, `recovery-plan-${retryId}.json`);
|
|
4010
|
+
const runDir = `${options.runDir}.recovery-${retryId}`;
|
|
4011
|
+
if (guardStop()) return stop(guardStop());
|
|
4012
|
+
let retry;
|
|
4013
|
+
try {
|
|
4014
|
+
const planHandle = await withWorkerPath(invocationPath, target => open(target,
|
|
4015
|
+
constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, 0o600));
|
|
4016
|
+
try { await planHandle.writeFile(JSON.stringify(plan)); await planHandle.sync(); } finally { await planHandle.close(); }
|
|
4017
|
+
// Publication happens before any retry launch. A record failure stops work.
|
|
4018
|
+
record.state = 'applied';
|
|
4019
|
+
await recordAt(options.runDir, previous.invocationId);
|
|
4020
|
+
retry = await launchOneWorker(owner, { ...options, invocationPath, runDir, dispatchGuard,
|
|
4021
|
+
inputs: undefined, loadInputs, retryOf: { invocationId: previous.invocationId, runDir: options.runDir },
|
|
4022
|
+
correctStartup: { reasonCode: 'NOT_FOUND' }, startupCorrectionRecord: { ...record } });
|
|
4023
|
+
} catch {
|
|
4024
|
+
record.state = 'stopped';
|
|
4025
|
+
return stop(guardStop() ?? 'RETRY_REFUSED');
|
|
4026
|
+
}
|
|
4027
|
+
record.retryInvocationId = retry.invocationId;
|
|
4028
|
+
if (retry.attemptOutcome !== 'succeeded') { record.state = 'stopped'; record.stopCode = 'RETRY_FAILED'; }
|
|
4029
|
+
retry.startupCorrection = { ...record };
|
|
4030
|
+
await recordAt(options.runDir, previous.invocationId);
|
|
4031
|
+
await recordAt(runDir, retry.invocationId);
|
|
4032
|
+
return retry;
|
|
4033
|
+
}
|
|
4034
|
+
|
|
3710
4035
|
// #2915 round 8 (D6, "lazy public fields still differ after mixed use"):
|
|
3711
4036
|
// `lastAttemptResult`/`lastAttemptInvocationId` are informational
|
|
3712
4037
|
// bookkeeping only (#2915 step 4 — the retry gate no longer reads them;
|
|
@@ -3778,8 +4103,9 @@ export class SessionLauncher extends EventEmitter {
|
|
|
3778
4103
|
* `options.correctStartup: { reasonCode, correctionsUsed }` enforces the
|
|
3779
4104
|
* one-qualified-startup-correction budget on a conductor-declared
|
|
3780
4105
|
* correction retry (startupCorrectionGate): unqualified causes and second
|
|
3781
|
-
* corrections are refused, within the task's existing allowance.
|
|
3782
|
-
*
|
|
4106
|
+
* corrections are refused, within the task's existing allowance. The only
|
|
4107
|
+
* automatic correction refreshes a changed qualified executable binding
|
|
4108
|
+
* after a launcher-observed ENOENT with confirmed stop, once per task chain.
|
|
3783
4109
|
*
|
|
3784
4110
|
* Both checks are gated recovery behavior, not a general launch
|
|
3785
4111
|
* precondition: enforcement happens entirely INSIDE `launchOneWorker`,
|
|
@@ -3806,8 +4132,13 @@ export class SessionLauncher extends EventEmitter {
|
|
|
3806
4132
|
// `options.retryOf`-driven and disk-verified every time) is set inside
|
|
3807
4133
|
// `launchOneWorker` itself, gated on whether recovery actually admitted
|
|
3808
4134
|
// this attempt (#2915 review 6, finding 6) — never here unconditionally.
|
|
3809
|
-
|
|
4135
|
+
directStartupCorrectionContext.delete(this);
|
|
4136
|
+
try {
|
|
4137
|
+
const result = await launchOneWorker(this, options);
|
|
4138
|
+
return await correctDirectWorkerStartup(this, options, result);
|
|
4139
|
+
}
|
|
3810
4140
|
finally {
|
|
4141
|
+
directStartupCorrectionContext.delete(this);
|
|
3811
4142
|
_workerRecoverySubscriptions.get(this)?.();
|
|
3812
4143
|
_workerRecoverySubscriptions.delete(this);
|
|
3813
4144
|
this.workerLaunchPending = false;
|
|
@@ -5055,3 +5386,42 @@ export function planDirectWorkerLaunch({
|
|
|
5055
5386
|
}
|
|
5056
5387
|
|
|
5057
5388
|
export default SessionLauncher;
|
|
5389
|
+
|
|
5390
|
+
/** The packaged session owner accepts only the parent-bound managed one-shot contract. */
|
|
5391
|
+
export function parseManagedWorkerLaunchArgs(args) {
|
|
5392
|
+
const result = {};
|
|
5393
|
+
const seen = new Set();
|
|
5394
|
+
for (let index = 0; index < args.length; index++) {
|
|
5395
|
+
const flag = args[index];
|
|
5396
|
+
if (seen.has(flag)) throw new Error(`Duplicate managed worker option: ${flag}`);
|
|
5397
|
+
seen.add(flag);
|
|
5398
|
+
if (flag === '--launch-worker') continue;
|
|
5399
|
+
const key = { '--invocation': 'invocationPath', '--run-dir': 'runDir',
|
|
5400
|
+
'--managed-request': 'requestId', '--managed-generation': 'generation' }[flag];
|
|
5401
|
+
if (!key) throw new Error(`Unsupported managed worker option: ${flag}`);
|
|
5402
|
+
const value = args[++index];
|
|
5403
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
5404
|
+
result[key] = value;
|
|
5405
|
+
}
|
|
5406
|
+
if (!seen.has('--launch-worker') || !result.invocationPath || !result.runDir || !result.requestId || !result.generation) {
|
|
5407
|
+
throw new Error('Managed worker launch requires invocation, run directory and fenced admission');
|
|
5408
|
+
}
|
|
5409
|
+
const generation = Number(result.generation);
|
|
5410
|
+
if (!Number.isSafeInteger(generation) || generation < 1) throw new Error('Invalid managed admission generation');
|
|
5411
|
+
return { invocationPath: result.invocationPath, runDir: result.runDir,
|
|
5412
|
+
managedAdmission: { requestId: result.requestId, generation } };
|
|
5413
|
+
}
|
|
5414
|
+
|
|
5415
|
+
async function runManagedWorkerEntry() {
|
|
5416
|
+
const result = await new SessionLauncher().launchWorker(parseManagedWorkerLaunchArgs(process.argv.slice(2)));
|
|
5417
|
+
console.log(JSON.stringify(result));
|
|
5418
|
+
process.exitCode = result.termination !== 'confirmed' ? 2
|
|
5419
|
+
: result.errorCode || result.timedOut || result.exitCode !== 0 ? 1 : 0;
|
|
5420
|
+
}
|
|
5421
|
+
|
|
5422
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
5423
|
+
runManagedWorkerEntry().catch(error => {
|
|
5424
|
+
console.error(error.message);
|
|
5425
|
+
process.exitCode = 1;
|
|
5426
|
+
});
|
|
5427
|
+
}
|