@aiwg/cli 2026.10.4 → 2026.10.6
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 +3 -1
- package/dist/src/artifacts/browser-export.js +1 -0
- package/dist/src/artifacts/citation-parser.js +8 -1
- package/dist/src/artifacts/cli.js +72 -9
- 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/graph-query.js +18 -8
- package/dist/src/artifacts/index-adapters.js +339 -0
- package/dist/src/artifacts/index-builder.js +146 -16
- package/dist/src/artifacts/index-files.js +15 -4
- package/dist/src/artifacts/index-preparation.js +91 -26
- package/dist/src/artifacts/index-reader.js +44 -0
- package/dist/src/artifacts/index-refresh.js +130 -56
- package/dist/src/artifacts/index-status.js +20 -5
- package/dist/src/artifacts/index-worker.mjs +9 -7
- 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/cli/handlers/decision.js +19 -1
- 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/local-executor.js +5 -3
- 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 +40 -6
- package/dist/src/cli/handlers/setup.js +428 -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 +279 -93
- package/dist/src/cli/handlers/workspace.js +3 -1
- package/dist/src/cli/prompt-utils.js +8 -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/native-sandbox-warnings.js +25 -0
- package/dist/src/contributors/validation.js +6 -5
- package/dist/src/decision/admission.js +7 -3
- package/dist/src/decision/benchmark.js +469 -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 +18 -15
- package/dist/src/decision/qualification/load-staged.js +539 -0
- package/dist/src/decision/qualification/load.js +109 -33
- package/dist/src/decision/qualification/runner.js +5 -2
- 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 +233 -0
- package/dist/src/discovery/plan-apply.js +571 -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/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-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/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/mcp/cli.mjs +5 -6
- package/dist/src/mcp/config-file.mjs +6 -2
- package/dist/src/mcp/config-layers.mjs +23 -7
- 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 +17 -4
- 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/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/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 +7 -0
- package/dist/src/models/worker-installation.mjs +169 -37
- package/dist/src/models/wrapper-route.js +1 -1
- package/dist/src/output-modes/registry.js +8 -7
- package/dist/src/plugin/metadata-validator.js +1 -1
- 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 +114 -15
- package/dist/src/sessions/adapters/claude.js +19 -18
- package/dist/src/sessions/adapters/codex.js +33 -19
- 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/batch-contracts.js +1 -1
- package/dist/src/sessions/batch-import.js +202 -2
- package/dist/src/sessions/candidates.js +7 -6
- package/dist/src/sessions/contracts.js +73 -54
- package/dist/src/sessions/coverage-preflight.js +26 -1
- package/dist/src/sessions/output-registration.js +11 -10
- package/dist/src/sessions/policy.js +18 -7
- package/dist/src/sessions/refresh.js +2 -3
- package/dist/src/sessions/repository.js +4 -2
- package/dist/src/sessions/workspace-discovery.js +92 -26
- package/dist/src/smiths/context-pipeline/aiwg-md.js +18 -1
- package/dist/src/smiths/context-pipeline/claude-hook.js +42 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +72 -5
- package/dist/src/smiths/context-pipeline/workspace-context.js +106 -10
- package/dist/src/update/checker.d.mts +4 -0
- package/dist/src/update/checker.mjs +14 -0
- 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 +19 -13
- package/tools/agents/providers/claude.mjs +1 -1
- package/tools/agents/providers/codex.mjs +50 -56
- package/tools/agents/providers/copilot.mjs +8 -2
- 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 +7 -13
- package/tools/agents/providers/muse-hooks.mjs +21 -14
- package/tools/agents/providers/muse.mjs +7 -2
- package/tools/agents/providers/omp.mjs +28 -4
- 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 +263 -71
- package/tools/agents/providers/windsurf.mjs +7 -0
- 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/muse-adapter.mjs +12 -2
- 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 +7 -4
- package/tools/ralph-external/session-launcher.mjs +280 -32
- 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,80 @@ 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
|
+
|
|
416
490
|
async function launchOneWorker(owner, options) {
|
|
417
491
|
const { inspectWorkerPath: inspect, revalidateWorkerInvocationPlan: validate } = await importImpl(import.meta.url, 'serve/worker-invocation.js');
|
|
418
492
|
const { qualificationDigest } = await importImpl(import.meta.url, 'models/qualification-digest.mjs');
|
|
@@ -425,6 +499,11 @@ async function launchOneWorker(owner, options) {
|
|
|
425
499
|
const clock = () => typeof options.now === 'function' ? options.now() : options.now ?? new Date().toISOString();
|
|
426
500
|
const now = clock();
|
|
427
501
|
const invocation = await validate(saved, inputs.snapshot, inputs.drivers, now);
|
|
502
|
+
if (invocation.mode === 'managed' || options.managedAdmission) {
|
|
503
|
+
const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
|
|
504
|
+
options = { ...options, scopedBreaker: null, managedInvocationId: admitted.metadata.invocationId,
|
|
505
|
+
managedDeadlineAtMs: admitted.deadlineAtMs };
|
|
506
|
+
}
|
|
428
507
|
if (invocation.transport !== 'shell') throw new Error('One-shot capture requires shell transport; use the qualified MCP descriptor directly');
|
|
429
508
|
if (process.platform !== 'linux') throw new Error('One-shot worker capture requires qualified Linux process identity');
|
|
430
509
|
const runDir = options.runDir;
|
|
@@ -475,9 +554,24 @@ async function launchOneWorker(owner, options) {
|
|
|
475
554
|
// document on the stdin pipe, never brief text in argv (#2911).
|
|
476
555
|
const { workerStdinBytes } = await importImpl(import.meta.url, 'serve/worker-stdin-frame.mjs');
|
|
477
556
|
const stdinBytes = workerStdinBytes(invocation, brief, inputs.drivers[invocation.selected.cli]);
|
|
478
|
-
let gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config
|
|
479
|
-
|
|
480
|
-
|
|
557
|
+
let gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
|
|
558
|
+
paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
|
|
559
|
+
if (!gate.ok) throw workerDispatchRefusal(gate);
|
|
560
|
+
const { WorkerPolicyError, assertWorkerLaunchMode } = await importImpl(import.meta.url, 'models/provider-policy.js');
|
|
561
|
+
const permissions = inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution ?? {};
|
|
562
|
+
const adapter = inputs.drivers[invocation.selected.cli].qualification.adapter;
|
|
563
|
+
assertWorkerLaunchMode(adapter, permissions);
|
|
564
|
+
if (options.isolation?.mode === 'native-sandbox' && invocation.isolation !== 'native-sandbox') {
|
|
565
|
+
throw new WorkerPolicyError('OG_POLICY_RELAXATION', 'native-sandbox requires the pinned project execution policy');
|
|
566
|
+
}
|
|
567
|
+
const isolation = invocation.isolation === 'native-sandbox' ? { mode: 'native-sandbox' }
|
|
568
|
+
: options.isolation ?? detectWorkerIsolation();
|
|
569
|
+
if (isolation.mode === 'systemd-unit' && workerNeedsNativeNamespaces(adapter, permissions)) {
|
|
570
|
+
const probe = await (options.probeNativeSandbox ?? probeWorkerNativeSandbox)({ cwd: invocation.cwd,
|
|
571
|
+
envNames: invocation.envNames, env: Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
|
|
572
|
+
.map(name => [name, process.env[name]])) });
|
|
573
|
+
if (!probe.ok) throw new WorkerPolicyError(NATIVE_SANDBOX_UNAVAILABLE, nativeSandboxProbeMessage(probe));
|
|
574
|
+
}
|
|
481
575
|
if (isolation.mode === 'degraded' && !options.allowDegraded) {
|
|
482
576
|
throw new Error('Worker isolation unavailable; explicitly select --allow-degraded-worker or use manual observation');
|
|
483
577
|
}
|
|
@@ -692,7 +786,7 @@ async function launchOneWorker(owner, options) {
|
|
|
692
786
|
prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
|
|
693
787
|
worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
|
|
694
788
|
health: invocation.health ?? null, transport: invocation.transport,
|
|
695
|
-
isolation: { mode: isolation.mode, memoryMaxGb: options.memoryMaxGb ?? 4 },
|
|
789
|
+
isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
|
|
696
790
|
limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
|
|
697
791
|
await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
|
|
698
792
|
launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
|
|
@@ -704,8 +798,9 @@ async function launchOneWorker(owner, options) {
|
|
|
704
798
|
// Recheck policy, prompt, installation and exact bindings after file preparation.
|
|
705
799
|
inputs = await loadInputs();
|
|
706
800
|
await validate(saved, inputs.snapshot, inputs.drivers, clock());
|
|
707
|
-
gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config
|
|
708
|
-
|
|
801
|
+
gate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
|
|
802
|
+
paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
|
|
803
|
+
if (!gate.ok) throw workerDispatchRefusal(gate);
|
|
709
804
|
if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
|
|
710
805
|
const environment = Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
|
|
711
806
|
.map(name => [name, process.env[name]]));
|
|
@@ -766,12 +861,12 @@ async function launchOneWorker(owner, options) {
|
|
|
766
861
|
// would in production).
|
|
767
862
|
const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
|
|
768
863
|
command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
|
|
769
|
-
memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation,
|
|
864
|
+
memoryMaxGb: options.memoryMaxGb ?? 4, gate, config: inputs.snapshot.config, isolation, workerGroup: invocation.selected.group,
|
|
770
865
|
allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
|
|
771
866
|
env: environment, envNames: invocation.envNames, stdin: invocation.stdin, runDir,
|
|
772
867
|
...(onDispatchCommit ? { onDispatchCommit } : {}) });
|
|
773
868
|
outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
|
|
774
|
-
if (!outcome
|
|
869
|
+
if (!outcome?.child) throw workerLaunchRefusal(outcome);
|
|
775
870
|
// #2915 review 4 (finding 3): when THIS attempt is the admitted canary
|
|
776
871
|
// for one or more covering scopes, execution itself must be clipped to
|
|
777
872
|
// the canary's own deadline (min(task deadline, admission+300s)) — not
|
|
@@ -831,7 +926,7 @@ async function launchOneWorker(owner, options) {
|
|
|
831
926
|
// canary deadline.
|
|
832
927
|
await settleLedger({ kind: 'uncertain', confirmedStop: true });
|
|
833
928
|
await publish('exit.json', { invocationId, exitCode: null, signal: null,
|
|
834
|
-
endedAt: new Date().toISOString(), termination: 'confirmed', descendants: 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
|
|
929
|
+
endedAt: new Date().toISOString(), isolation: isolation.mode, termination: 'confirmed', descendants: 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
|
|
835
930
|
} else if (admission.gated) {
|
|
836
931
|
// #2915 review 5 (finding 2a, "leaked capacity"), hardened by #2915
|
|
837
932
|
// review 6 (finding 4, "capture-setup exception after spawn"): a
|
|
@@ -900,6 +995,38 @@ async function launchOneWorker(owner, options) {
|
|
|
900
995
|
}
|
|
901
996
|
}
|
|
902
997
|
|
|
998
|
+
// #3489: process exit/close notification can be observed before the final
|
|
999
|
+
// pipe bytes are delivered as stream 'data' events (the same class as #3488,
|
|
1000
|
+
// which waited for IPC disconnect as well as exit). The child 'data'
|
|
1001
|
+
// handlers are the ONLY path that feeds the health observer and accumulates
|
|
1002
|
+
// the `output` buffer adapter extraction consumes, so settling health on
|
|
1003
|
+
// the process-level event alone classifies a completed exit-0 run as
|
|
1004
|
+
// `failed` (missing terminal event, FINAL_INVALID) whenever it wins the
|
|
1005
|
+
// race under load. Drain any already-buffered stdout/stderr bytes — bounded
|
|
1006
|
+
// by the termination grace — before destroying the source streams and
|
|
1007
|
+
// snapshotting output/observer state. Streams with no buffered bytes
|
|
1008
|
+
// resolve immediately, so never-ending stdio (timer-faked stand-ins, held
|
|
1009
|
+
// pipes) never delays settle; the bound only covers genuinely in-flight
|
|
1010
|
+
// data. The timer stays referenced so a bounded drain always completes,
|
|
1011
|
+
// mirroring the already-referenced exit-grace `later()` timer.
|
|
1012
|
+
function drainChildStdio(child, timeoutMs) {
|
|
1013
|
+
const pending = [];
|
|
1014
|
+
for (const stream of [child?.stdout, child?.stderr]) {
|
|
1015
|
+
if (!stream || typeof stream.once !== 'function' || stream.readableEnded || stream.destroyed) continue;
|
|
1016
|
+
if (!(stream.readableLength > 0)) continue;
|
|
1017
|
+
try { stream.resume?.(); } catch { continue; }
|
|
1018
|
+
pending.push(new Promise(resolve => {
|
|
1019
|
+
const done = () => { clearTimeout(timer); resolve(); };
|
|
1020
|
+
const timer = setTimeout(done, Math.max(0, timeoutMs ?? 0));
|
|
1021
|
+
stream.once('end', done);
|
|
1022
|
+
stream.once('close', done);
|
|
1023
|
+
stream.once('error', done);
|
|
1024
|
+
}));
|
|
1025
|
+
}
|
|
1026
|
+
if (!pending.length) return Promise.resolve();
|
|
1027
|
+
return Promise.all(pending);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
903
1030
|
async function captureOneWorker(owner, ctx) {
|
|
904
1031
|
const { child, outcome, invocation, invocationId, supervisor, launchTime, monotonicStart, publish, paths, handles, options,
|
|
905
1032
|
canaryDeadlineMs = null, canaryNowMs = () => Date.now(), recoveryGated = false } = ctx;
|
|
@@ -968,7 +1095,16 @@ async function captureOneWorker(owner, ctx) {
|
|
|
968
1095
|
};
|
|
969
1096
|
const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
|
|
970
1097
|
const signal = value => {
|
|
971
|
-
if (settled
|
|
1098
|
+
if (settled) return false;
|
|
1099
|
+
let delivered = signalOwnedWorker(identity, value, { supervisor });
|
|
1100
|
+
// Startup can consume the whole deadline before the transport spawns
|
|
1101
|
+
// its worker or publishes an owned group. We still own this attached
|
|
1102
|
+
// ChildProcess: stop it before it can launch work after finalization.
|
|
1103
|
+
// This proves no descendant termination; degraded mode stays uncertain.
|
|
1104
|
+
if (!delivered && !identity && hasTransport && outcome.mode !== 'systemd-unit' && !sawExit) {
|
|
1105
|
+
try { delivered = signalChild(child, value); } catch { /* already exited or no child handle */ }
|
|
1106
|
+
}
|
|
1107
|
+
if (!delivered) return false;
|
|
972
1108
|
deliveredSignal = value;
|
|
973
1109
|
return true;
|
|
974
1110
|
};
|
|
@@ -1007,8 +1143,19 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1007
1143
|
}
|
|
1008
1144
|
signal('SIGTERM');
|
|
1009
1145
|
const remainingGrace = Math.min(graceMs, hardRemaining());
|
|
1010
|
-
|
|
1011
|
-
|
|
1146
|
+
const escalate = () => {
|
|
1147
|
+
signal('SIGKILL');
|
|
1148
|
+
// Startup may have no worker identity yet. Wait for our transport's
|
|
1149
|
+
// exit status before settling: TERM can have killed it even if KILL
|
|
1150
|
+
// was also delivered before Node observes exit (#3532). Use the unused
|
|
1151
|
+
// grace plus the existing 100ms settle tick, keeping the total
|
|
1152
|
+
// termination budget at graceMs + 100 on the tracked launcher timers.
|
|
1153
|
+
const exitWaitMs = !identity && hasTransport && outcome.mode !== 'systemd-unit'
|
|
1154
|
+
? graceMs - remainingGrace + 100 : 100;
|
|
1155
|
+
later(() => finish(), exitWaitMs);
|
|
1156
|
+
};
|
|
1157
|
+
if (remainingGrace === 0) escalate();
|
|
1158
|
+
else later(escalate, remainingGrace);
|
|
1012
1159
|
};
|
|
1013
1160
|
owner.currentProcess = child;
|
|
1014
1161
|
owner.workerStop = stop;
|
|
@@ -1023,9 +1170,15 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1023
1170
|
owner.emit('stdout', chunk);
|
|
1024
1171
|
});
|
|
1025
1172
|
let stderrBytes = 0;
|
|
1173
|
+
let sandboxStderrTail = '';
|
|
1026
1174
|
let recoveryReason = null;
|
|
1027
1175
|
child.stderr.on('data', chunk => {
|
|
1028
1176
|
observer.feed(chunk, { kind: 'stderr', id: 'stderr', generation: 0, offset: stderrOffset }); stderrOffset += chunk.length;
|
|
1177
|
+
if (workerNeedsNativeNamespaces(driver.qualification.adapter, ctx.inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution)) {
|
|
1178
|
+
const observedStderr = sandboxStderrTail + chunk.toString('utf8');
|
|
1179
|
+
if (nativeSandboxFailure(observedStderr)) errorCode = NATIVE_SANDBOX_UNAVAILABLE;
|
|
1180
|
+
sandboxStderrTail = observedStderr.slice(-1024);
|
|
1181
|
+
}
|
|
1029
1182
|
const hint = classifyWorkerHealthFailure(chunk.toString('utf8'), { source: { kind: 'stderr' } })?.reasonCode;
|
|
1030
1183
|
if (['RATE_LIMIT', 'NETWORK'].includes(hint)) recoveryReason = hint;
|
|
1031
1184
|
const remaining = Math.max(0, 10485760 - stderrBytes);
|
|
@@ -1055,6 +1208,10 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1055
1208
|
timers.clear();
|
|
1056
1209
|
process.off('SIGTERM', onSignal); process.off('SIGINT', onSignal);
|
|
1057
1210
|
owner.currentProcess = null; owner.workerStop = null;
|
|
1211
|
+
// #3489: drain in-flight stdio before destroying the source streams —
|
|
1212
|
+
// destroying first discards bytes never delivered as 'data', settling
|
|
1213
|
+
// health on a partial stream (exit observed before drain).
|
|
1214
|
+
await drainChildStdio(child, graceMs);
|
|
1058
1215
|
child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
|
|
1059
1216
|
for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
|
|
1060
1217
|
const native = outcome.mode === 'systemd-unit' ? readNative() : null;
|
|
@@ -1415,12 +1572,15 @@ async function captureOneWorker(owner, ctx) {
|
|
|
1415
1572
|
|
|
1416
1573
|
// ===========================================================================
|
|
1417
1574
|
// BASED ON a4085b86a (#2915 round 8, D6).
|
|
1418
|
-
//
|
|
1419
|
-
// respects the winning stop cause
|
|
1575
|
+
// Shared lifecycle fixes: finish() snapshots timeout classification before
|
|
1576
|
+
// cleanup and respects the winning stop cause; stop() can signal the owned
|
|
1577
|
+
// transport before worker identity is available, and then uses the unused
|
|
1578
|
+
// termination grace to observe its exit before falling back to the last
|
|
1579
|
+
// delivered signal (#3532).
|
|
1420
1580
|
// The test/fixtures frozen reference remains unchanged.
|
|
1421
1581
|
//
|
|
1422
1582
|
// `launchOneWorkerBaseline` and `captureOneWorkerBaseline` reproduce
|
|
1423
|
-
// `launchOneWorker`/`captureOneWorker`, apart from the
|
|
1583
|
+
// `launchOneWorker`/`captureOneWorker`, apart from the lifecycle fixes above,
|
|
1424
1584
|
// as they existed at base commit `a4085b86a` (`git show
|
|
1425
1585
|
// a4085b86a:tools/ralph-external/session-launcher.mjs`) — before any
|
|
1426
1586
|
// #2915 direct-recovery code existed in this file. `launchOneWorker`
|
|
@@ -1472,7 +1632,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1472
1632
|
if (runStat.uid !== process.getuid()) throw new Error('Worker run directory must be conductor-owned');
|
|
1473
1633
|
await withWorkerPath(runDir, async target => { const handle = await open(target, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
1474
1634
|
try { await handle.chmod(0o700); } finally { await handle.close(); } });
|
|
1475
|
-
const invocationId = randomUUID();
|
|
1635
|
+
const invocationId = options.managedAdmission ? options.managedInvocationId : randomUUID();
|
|
1476
1636
|
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
1637
|
try { await reservation.writeFile(invocationId); await reservation.sync(); } finally { await reservation.close(); }
|
|
1478
1638
|
const supervisor = sampleWorkerIdentity(process.pid);
|
|
@@ -1492,11 +1652,12 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1492
1652
|
const handles = [];
|
|
1493
1653
|
let outcome;
|
|
1494
1654
|
let artifacts;
|
|
1655
|
+
let managedSpawnAttempted = false;
|
|
1495
1656
|
try {
|
|
1496
1657
|
artifacts = await materializeWorkerArtifacts(invocation, runDir);
|
|
1497
1658
|
const displayArgv = artifacts.argv.map((token, i) => i === 0 ? token : `<argument:${bytesDigest(token)}>`);
|
|
1498
1659
|
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,
|
|
1660
|
+
previousAttemptId: verifiedPreviousAttemptId ?? null, requestId: options.managedAdmission?.requestId ?? invocationId, taskDigest: qualificationDigest(saved.request), role: saved.request.role,
|
|
1500
1661
|
selected: invocation.selected, planDigest: qualificationDigest(saved), invocationDigest: qualificationDigest(invocation),
|
|
1501
1662
|
policyDigest: invocation.policyDigest, catalogDigest: invocation.catalogDigest, profileDigest: invocation.profileDigest,
|
|
1502
1663
|
expiresAt: invocation.expiresAt, argv: displayArgv, commandDisplay: displayArgv.map(token => JSON.stringify(token)).join(' '),
|
|
@@ -1509,7 +1670,7 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1509
1670
|
prompt: { path: invocation.briefPath, digest: invocation.briefDigest }, cwd: invocation.cwd,
|
|
1510
1671
|
worktree: { path: invocation.cwd, baseRevision: null }, paths, captures, outputFraming: profile.health?.outputFraming ?? 'opaque',
|
|
1511
1672
|
health: invocation.health ?? null, transport: invocation.transport,
|
|
1512
|
-
isolation: { mode: isolation.mode, memoryMaxGb: options.memoryMaxGb ?? 4 },
|
|
1673
|
+
isolation: { mode: isolation.mode, memoryMaxGb: isolation.mode === 'native-sandbox' ? null : options.memoryMaxGb ?? 4 },
|
|
1513
1674
|
limits: invocation.limits, maxOutputBytes: Math.min(invocation.maxOutputBytes ?? 10485760, 10485760) });
|
|
1514
1675
|
await publish('process.json', { invocationId, origin: 'launcher', supervisor, worker: null,
|
|
1515
1676
|
launchTime, monotonicStart, ownership: 'unknown', isolation: isolation.mode });
|
|
@@ -1521,8 +1682,9 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1521
1682
|
// Recheck policy, prompt, installation and exact bindings after file preparation.
|
|
1522
1683
|
inputs = await loadInputs();
|
|
1523
1684
|
await validate(saved, inputs.snapshot, inputs.drivers, clock());
|
|
1524
|
-
localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config
|
|
1525
|
-
|
|
1685
|
+
localGate = (options.checkDispatch ?? checkWorkerDispatch)({ config: inputs.snapshot.config,
|
|
1686
|
+
paths: await Promise.all([invocation.cwd, runDir].map(workerDispatchPath)) });
|
|
1687
|
+
if (!localGate.ok) throw workerDispatchRefusal(localGate);
|
|
1526
1688
|
if (bytesDigest(await boundedPrivateRead(invocation.briefPath, 262144, inspect)) !== invocation.briefDigest) throw new Error('Worker prompt changed before spawn');
|
|
1527
1689
|
const environment = Object.fromEntries(invocation.envNames.filter(name => process.env[name] !== undefined)
|
|
1528
1690
|
.map(name => [name, process.env[name]]));
|
|
@@ -1539,16 +1701,24 @@ async function launchOneWorkerBaseline(owner, options, ctx) {
|
|
|
1539
1701
|
const launchResult = (options.launchIsolated ?? launchIsolatedWorker)({ name: invocationId,
|
|
1540
1702
|
command: artifacts.argv[0], args: artifacts.argv.slice(1), cwd: invocation.cwd,
|
|
1541
1703
|
memoryMaxGb: options.memoryMaxGb ?? 4, gate: localGate, config: inputs.snapshot.config, isolation,
|
|
1704
|
+
workerGroup: invocation.selected.group,
|
|
1542
1705
|
allowDegraded: !!options.allowDegraded, attached: true, ownedGroup: true,
|
|
1706
|
+
...(options.managedAdmission ? { onDispatchCommit: async () => {
|
|
1707
|
+
const admitted = await assertManagedWorkerLaunch(options, invocation, inputs);
|
|
1708
|
+
options.managedDeadlineAtMs = Math.min(options.managedDeadlineAtMs, admitted.deadlineAtMs);
|
|
1709
|
+
} } : {}),
|
|
1710
|
+
...(options.managedAdmission ? { onSpawnAttempt: () => { managedSpawnAttempted = true; } } : {}),
|
|
1543
1711
|
env: environment, envNames: invocation.envNames, stdin: invocation.stdin, runDir });
|
|
1544
1712
|
outcome = typeof launchResult?.then === 'function' ? await launchResult : launchResult;
|
|
1545
|
-
if (!outcome
|
|
1713
|
+
if (!outcome?.child) throw workerLaunchRefusal(outcome);
|
|
1546
1714
|
return await captureOneWorkerBaseline(owner, { child: outcome.child, outcome, invocation, invocationId, supervisor,
|
|
1547
1715
|
launchTime, monotonicStart, publish, publishFailures, paths, handles, inspect, inputs, profile, baselineContent, options,
|
|
1548
1716
|
brief: ctx.brief, stdinBytes: ctx.stdinBytes, runDir });
|
|
1549
1717
|
} catch (error) {
|
|
1550
1718
|
if (!outcome?.child) await publish('exit.json', { invocationId, exitCode: null, signal: null,
|
|
1551
|
-
|
|
1719
|
+
...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
|
|
1720
|
+
endedAt: new Date().toISOString(), isolation: isolation.mode, termination: managedSpawnAttempted ? 'uncertain' : 'confirmed',
|
|
1721
|
+
descendants: managedSpawnAttempted ? 'unknown' : 'not-started', errorCode: error.code ?? 'LAUNCH_REFUSED' });
|
|
1552
1722
|
throw error;
|
|
1553
1723
|
} finally {
|
|
1554
1724
|
try { await artifacts?.cleanup(); } finally {
|
|
@@ -1588,9 +1758,13 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1588
1758
|
let sawClose = false;
|
|
1589
1759
|
const driver = ctx.inputs.drivers[invocation.selected.cli];
|
|
1590
1760
|
const observationOptions = { provider: driver.qualification.adapter.provider, profile: ctx.profile,
|
|
1591
|
-
effectiveHealth:
|
|
1761
|
+
effectiveHealth: options.managedAdmission ? { ...invocation.health,
|
|
1762
|
+
hardTimeoutSeconds: Math.min(invocation.health?.hardTimeoutSeconds ?? invocation.limits.wallSeconds,
|
|
1763
|
+
(options.managedDeadlineAtMs - Date.parse(launchTime)) / 1000) } : invocation.health,
|
|
1764
|
+
invocationId, startedAt: Number(BigInt(monotonicStart)) / 1e6,
|
|
1592
1765
|
launchTime, contentDigest: ctx.baselineContent, sessionId: options.healthSessionLog?.sessionId ?? null,
|
|
1593
|
-
runId: options.healthSessionLog?.runId ?? null, maxOutputBytes: invocation.maxOutputBytes
|
|
1766
|
+
runId: options.healthSessionLog?.runId ?? null, maxOutputBytes: invocation.maxOutputBytes,
|
|
1767
|
+
...(options.managedAdmission ? { observerGeneration: options.managedAdmission.generation, requestId: options.managedAdmission.requestId } : {}) };
|
|
1594
1768
|
const observer = driver.createHealthObserver?.(observationOptions) ?? createWorkerHealthObserver(observationOptions);
|
|
1595
1769
|
const tickRate = qualifyWorkerCpuTickRate();
|
|
1596
1770
|
let previousCpu = null;
|
|
@@ -1611,7 +1785,16 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1611
1785
|
};
|
|
1612
1786
|
const graceMs = (invocation.health?.terminateGraceSeconds ?? 5) * 1000;
|
|
1613
1787
|
const signal = value => {
|
|
1614
|
-
if (settled
|
|
1788
|
+
if (settled) return false;
|
|
1789
|
+
let delivered = signalOwnedWorker(identity, value, { supervisor });
|
|
1790
|
+
// Startup can consume the whole deadline before the transport spawns
|
|
1791
|
+
// its worker or publishes an owned group. We still own this attached
|
|
1792
|
+
// ChildProcess: stop it before it can launch work after finalization.
|
|
1793
|
+
// This proves no descendant termination; degraded mode stays uncertain.
|
|
1794
|
+
if (!delivered && !identity && hasTransport && outcome.mode !== 'systemd-unit' && !sawExit) {
|
|
1795
|
+
try { delivered = signalChild(child, value); } catch { /* already exited or no child handle */ }
|
|
1796
|
+
}
|
|
1797
|
+
if (!delivered) return false;
|
|
1615
1798
|
deliveredSignal = value;
|
|
1616
1799
|
return true;
|
|
1617
1800
|
};
|
|
@@ -1640,8 +1823,19 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1640
1823
|
}
|
|
1641
1824
|
signal('SIGTERM');
|
|
1642
1825
|
const remainingGrace = Math.min(graceMs, hardRemaining());
|
|
1643
|
-
|
|
1644
|
-
|
|
1826
|
+
const escalate = () => {
|
|
1827
|
+
signal('SIGKILL');
|
|
1828
|
+
// Startup may have no worker identity yet. Wait for our transport's
|
|
1829
|
+
// exit status before settling: TERM can have killed it even if KILL
|
|
1830
|
+
// was also delivered before Node observes exit (#3532). Use the unused
|
|
1831
|
+
// grace plus the existing 100ms settle tick, keeping the total
|
|
1832
|
+
// termination budget at graceMs + 100 on the tracked launcher timers.
|
|
1833
|
+
const exitWaitMs = !identity && hasTransport && outcome.mode !== 'systemd-unit'
|
|
1834
|
+
? graceMs - remainingGrace + 100 : 100;
|
|
1835
|
+
later(() => finish(), exitWaitMs);
|
|
1836
|
+
};
|
|
1837
|
+
if (remainingGrace === 0) escalate();
|
|
1838
|
+
else later(escalate, remainingGrace);
|
|
1645
1839
|
};
|
|
1646
1840
|
owner.currentProcess = child;
|
|
1647
1841
|
owner.workerStop = stop;
|
|
@@ -1656,8 +1850,14 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1656
1850
|
owner.emit('stdout', chunk);
|
|
1657
1851
|
});
|
|
1658
1852
|
let stderrBytes = 0;
|
|
1853
|
+
let sandboxStderrTail = '';
|
|
1659
1854
|
child.stderr.on('data', chunk => {
|
|
1660
1855
|
observer.feed(chunk, { kind: 'stderr', id: 'stderr', generation: 0, offset: stderrOffset }); stderrOffset += chunk.length;
|
|
1856
|
+
if (workerNeedsNativeNamespaces(driver.qualification.adapter, ctx.inputs.snapshot.config.parallelism?.groups?.[invocation.selected.group]?.execution)) {
|
|
1857
|
+
const observedStderr = sandboxStderrTail + chunk.toString('utf8');
|
|
1858
|
+
if (nativeSandboxFailure(observedStderr)) errorCode = NATIVE_SANDBOX_UNAVAILABLE;
|
|
1859
|
+
sandboxStderrTail = observedStderr.slice(-1024);
|
|
1860
|
+
}
|
|
1661
1861
|
const remaining = Math.max(0, 10485760 - stderrBytes);
|
|
1662
1862
|
streams[1].write(chunk.subarray(0, remaining)); streams[3].write(chunk.subarray(0, remaining)); stderrBytes += chunk.length;
|
|
1663
1863
|
if (stderrBytes > 10485760) { errorCode = 'OUTPUT_LIMIT'; stop(); }
|
|
@@ -1685,6 +1885,11 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1685
1885
|
timers.clear();
|
|
1686
1886
|
process.off('SIGTERM', onSignal); process.off('SIGINT', onSignal);
|
|
1687
1887
|
owner.currentProcess = null; owner.workerStop = null;
|
|
1888
|
+
// #3489: drain in-flight stdio before destroying the source streams —
|
|
1889
|
+
// destroying first discards bytes never delivered as 'data', settling
|
|
1890
|
+
// health on a partial stream (exit observed before drain). Ordering-only;
|
|
1891
|
+
// recovery semantics in this frozen baseline are unchanged.
|
|
1892
|
+
await drainChildStdio(child, graceMs);
|
|
1688
1893
|
child.stdout.destroy(); child.stderr.destroy(); child.stdin?.destroy(); child.unref();
|
|
1689
1894
|
for (const stream of streams) await new Promise(resolve => { if (stream.destroyed) resolve(); else stream.end(resolve); });
|
|
1690
1895
|
const native = outcome.mode === 'systemd-unit' ? readNative() : null;
|
|
@@ -1707,6 +1912,7 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1707
1912
|
const exitCode = resultSignal ? null
|
|
1708
1913
|
: outcome.mode === 'systemd-unit' && native?.execMainCode === 1 ? native.execMainStatus : waitCode;
|
|
1709
1914
|
const result = { invocationId, exitCode, signal: resultSignal,
|
|
1915
|
+
...(options.managedAdmission ? { managedAdmission: options.managedAdmission } : {}),
|
|
1710
1916
|
endedAt: new Date().toISOString(), termination: confirmed ? 'confirmed' : 'uncertain',
|
|
1711
1917
|
descendants: confirmed ? 'stopped' : 'unknown', timedOut: resultTimedOut,
|
|
1712
1918
|
cancelled: stopReason === 'CANCELLED', stopReason, errorCode: errorCode ?? null, capturedBytes: Math.min(total, output.length),
|
|
@@ -1772,8 +1978,11 @@ async function captureOneWorkerBaseline(owner, ctx) {
|
|
|
1772
1978
|
sawExit = true; waitCode = code; waitSignal = sig; later(() => finish(), graceMs);
|
|
1773
1979
|
});
|
|
1774
1980
|
child.once('close', () => { sawClose = true; finish(); });
|
|
1775
|
-
const hardRemaining = () => Math.max(0, Math.min(
|
|
1776
|
-
invocation.limits.wallSeconds
|
|
1981
|
+
const hardRemaining = () => Math.max(0, Math.min(
|
|
1982
|
+
Math.min(invocation.health?.hardTimeoutSeconds ?? invocation.limits.wallSeconds, invocation.limits.wallSeconds,
|
|
1983
|
+
options.managedAdmission ? (options.managedDeadlineAtMs - Date.parse(launchTime)) / 1000 : Infinity) * 1000
|
|
1984
|
+
- (Number(process.hrtime.bigint() - BigInt(monotonicStart)) / 1e6),
|
|
1985
|
+
options.managedAdmission ? options.managedDeadlineAtMs - Date.now() : Infinity));
|
|
1777
1986
|
later(() => {
|
|
1778
1987
|
observer.check();
|
|
1779
1988
|
reportHealth(); stop('HARD_DEADLINE');
|
|
@@ -5055,3 +5264,42 @@ export function planDirectWorkerLaunch({
|
|
|
5055
5264
|
}
|
|
5056
5265
|
|
|
5057
5266
|
export default SessionLauncher;
|
|
5267
|
+
|
|
5268
|
+
/** The packaged session owner accepts only the parent-bound managed one-shot contract. */
|
|
5269
|
+
export function parseManagedWorkerLaunchArgs(args) {
|
|
5270
|
+
const result = {};
|
|
5271
|
+
const seen = new Set();
|
|
5272
|
+
for (let index = 0; index < args.length; index++) {
|
|
5273
|
+
const flag = args[index];
|
|
5274
|
+
if (seen.has(flag)) throw new Error(`Duplicate managed worker option: ${flag}`);
|
|
5275
|
+
seen.add(flag);
|
|
5276
|
+
if (flag === '--launch-worker') continue;
|
|
5277
|
+
const key = { '--invocation': 'invocationPath', '--run-dir': 'runDir',
|
|
5278
|
+
'--managed-request': 'requestId', '--managed-generation': 'generation' }[flag];
|
|
5279
|
+
if (!key) throw new Error(`Unsupported managed worker option: ${flag}`);
|
|
5280
|
+
const value = args[++index];
|
|
5281
|
+
if (!value || value.startsWith('--')) throw new Error(`${flag} requires a value`);
|
|
5282
|
+
result[key] = value;
|
|
5283
|
+
}
|
|
5284
|
+
if (!seen.has('--launch-worker') || !result.invocationPath || !result.runDir || !result.requestId || !result.generation) {
|
|
5285
|
+
throw new Error('Managed worker launch requires invocation, run directory and fenced admission');
|
|
5286
|
+
}
|
|
5287
|
+
const generation = Number(result.generation);
|
|
5288
|
+
if (!Number.isSafeInteger(generation) || generation < 1) throw new Error('Invalid managed admission generation');
|
|
5289
|
+
return { invocationPath: result.invocationPath, runDir: result.runDir,
|
|
5290
|
+
managedAdmission: { requestId: result.requestId, generation } };
|
|
5291
|
+
}
|
|
5292
|
+
|
|
5293
|
+
async function runManagedWorkerEntry() {
|
|
5294
|
+
const result = await new SessionLauncher().launchWorker(parseManagedWorkerLaunchArgs(process.argv.slice(2)));
|
|
5295
|
+
console.log(JSON.stringify(result));
|
|
5296
|
+
process.exitCode = result.termination !== 'confirmed' ? 2
|
|
5297
|
+
: result.errorCode || result.timedOut || result.exitCode !== 0 ? 1 : 0;
|
|
5298
|
+
}
|
|
5299
|
+
|
|
5300
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
5301
|
+
runManagedWorkerEntry().catch(error => {
|
|
5302
|
+
console.error(error.message);
|
|
5303
|
+
process.exitCode = 1;
|
|
5304
|
+
});
|
|
5305
|
+
}
|
|
@@ -36,10 +36,10 @@ const _require = createRequire(import.meta.url);
|
|
|
36
36
|
let fs;
|
|
37
37
|
try { const gfs = _require('graceful-fs'); gfs.gracefulify(realFs); fs = realFs; } catch { fs = realFs; }
|
|
38
38
|
import path from 'path';
|
|
39
|
+
import { fileURLToPath } from 'node:url';
|
|
39
40
|
import os from 'os';
|
|
40
41
|
import { contentHash, writeSidecarManifest, getFrameworksForMode, normalizeDeploymentMode, skillMatchesProvider, isKernelSkill } from '../agents/providers/base.mjs';
|
|
41
42
|
|
|
42
|
-
const CODEX_SKILLS_DIR = path.join(os.homedir(), '.codex', 'skills');
|
|
43
43
|
const MAX_NAME_LENGTH = 100;
|
|
44
44
|
const MAX_DESCRIPTION_LENGTH = 500;
|
|
45
45
|
const LEGACY_RENAMED_SKILLS = new Set(['aiwg-mcp']);
|
|
@@ -104,11 +104,10 @@ export function planCodexListingBudget(planned, kernelDir, cap) {
|
|
|
104
104
|
return { demoted, before, after: projected, cap };
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
function parseArgs() {
|
|
108
|
-
const args = process.argv.slice(2);
|
|
107
|
+
function parseArgs(args, homeDir) {
|
|
109
108
|
const cfg = {
|
|
110
109
|
source: null,
|
|
111
|
-
target:
|
|
110
|
+
target: path.join(homeDir, '.codex', 'skills'),
|
|
112
111
|
mode: 'all',
|
|
113
112
|
dryRun: false,
|
|
114
113
|
force: false,
|
|
@@ -483,8 +482,8 @@ function isFullAiwgSourceRoot(srcRoot) {
|
|
|
483
482
|
fs.existsSync(path.join(srcRoot, 'agentic', 'code', 'extensions'));
|
|
484
483
|
}
|
|
485
484
|
|
|
486
|
-
|
|
487
|
-
const cfg = parseArgs();
|
|
485
|
+
export async function main(args = process.argv.slice(2), internal = {}) {
|
|
486
|
+
const cfg = parseArgs(args, internal.homeDir || os.homedir());
|
|
488
487
|
const { source, target, mode, dryRun, force } = cfg;
|
|
489
488
|
|
|
490
489
|
// Resolve source directory
|
|
@@ -699,4 +698,9 @@ function isFullAiwgSourceRoot(srcRoot) {
|
|
|
699
698
|
if (!dryRun && totalDeployed > 0) {
|
|
700
699
|
console.log(`\nRestart Codex to load new skills.`);
|
|
701
700
|
}
|
|
702
|
-
}
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
704
|
+
try { await main(); }
|
|
705
|
+
catch (error) { console.error(error.message); process.exitCode = 1; }
|
|
706
|
+
}
|
|
@@ -21,7 +21,12 @@
|
|
|
21
21
|
"type": "object", "additionalProperties": false,
|
|
22
22
|
"properties": {
|
|
23
23
|
"llm_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000, "default": 15, "description": "Planning USD per million frontier tokens, blended input/output rate." },
|
|
24
|
-
"jev_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000, "default": 0.2, "description": "Planning USD per million Jev tokens
|
|
24
|
+
"jev_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000, "default": 0.2, "description": "Planning USD per million Jev tokens. Complete split rates with known usage take precedence." },
|
|
25
|
+
"jev_input_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000 },
|
|
26
|
+
"jev_output_per_mtok": { "type": "number", "minimum": 0, "maximum": 1000000 },
|
|
27
|
+
"source": { "type": "string", "enum": ["typesafe-docs", "manual", "typesafe-snapshot"] },
|
|
28
|
+
"checked_at": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "ISO calendar date YYYY-MM-DD; calendar validity checked by the loader." },
|
|
29
|
+
"jev_model": { "type": "string", "pattern": "^jev-\\d+\\.\\d+\\.\\d+$" }
|
|
25
30
|
}
|
|
26
31
|
},
|
|
27
32
|
"savings": {
|
|
@@ -1497,6 +1502,11 @@
|
|
|
1497
1502
|
"type": "object",
|
|
1498
1503
|
"additionalProperties": false,
|
|
1499
1504
|
"properties": {
|
|
1505
|
+
"launchMode": {
|
|
1506
|
+
"enum": ["systemd-unit", "native-sandbox"],
|
|
1507
|
+
"default": "systemd-unit",
|
|
1508
|
+
"description": "Policy-selected worker launch boundary. native-sandbox requires a verified enforcing CLI sandbox and has no cgroup resource limit."
|
|
1509
|
+
},
|
|
1500
1510
|
"isolation": {
|
|
1501
1511
|
"enum": [
|
|
1502
1512
|
"worktree",
|