@aiwg/cli 2026.10.5 → 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 +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/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-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/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 +1 -0
- 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 +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 +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
|
@@ -12,14 +12,244 @@
|
|
|
12
12
|
* @issue #483
|
|
13
13
|
*/
|
|
14
14
|
import * as ui from '../ui.js';
|
|
15
|
-
import { promises as fs } from 'node:fs';
|
|
16
|
-
import { join } from 'node:path';
|
|
15
|
+
import { constants as fsConstants, promises as fs } from 'node:fs';
|
|
16
|
+
import { isAbsolute, join, relative } from 'node:path';
|
|
17
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
17
18
|
import { createHash, randomBytes } from 'node:crypto';
|
|
18
|
-
import { assertMissionWorkerHandoff } from '../../mission-protocol/codecs.js';
|
|
19
|
+
import { assertMissionWorkerHandoff, assertValidWorkerPayload, validateWorkerPayload, workerConsumerSupport } from '../../mission-protocol/codecs.js';
|
|
20
|
+
import { WORKER_BASELINE_FEATURES } from '../../mission-protocol/types.js';
|
|
19
21
|
// ── Constants ────────────────────────────────────────────────
|
|
20
22
|
const MC_ROOT = '.aiwg/ralph-external/mc';
|
|
21
23
|
const SESSIONS_DIR = join(MC_ROOT, 'sessions');
|
|
22
24
|
const CONTROL_ID_RE = /^[a-zA-Z0-9._-]+$/;
|
|
25
|
+
/** Managed callers fail closed on invalid configuration; legacy reads keep their
|
|
26
|
+
* historical best-effort behavior. No activation is installed by a caller. */
|
|
27
|
+
async function readAdmissionConfig(projectRoot) {
|
|
28
|
+
const { readAiwgConfig, readAiwgConfigCandidate } = await import('../../config/aiwg-config.js');
|
|
29
|
+
try {
|
|
30
|
+
return await readAiwgConfig(projectRoot);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const candidate = await readAiwgConfigCandidate(projectRoot);
|
|
34
|
+
if (candidate?.parallelism?.worker_mode === 'managed')
|
|
35
|
+
throw error;
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Shared binding used by MC and the managed Mission/Flow/Ralph caller paths. */
|
|
40
|
+
export async function createMcAdmissionScheduler(projectRoot, cfg, hostRegistry) {
|
|
41
|
+
const { resolveParallelism, workerActivationDigests, workerConfigDigest } = await import('../../config/aiwg-config.js');
|
|
42
|
+
const { FileAdmissionStore, SharedHostScheduler } = await import('../../serve/shared-host-scheduler.js');
|
|
43
|
+
const provider = cfg?.providers[0] ?? 'unknown';
|
|
44
|
+
const parallelism = resolveParallelism(cfg?.parallelism, provider);
|
|
45
|
+
if (parallelism.worker_mode === 'managed') {
|
|
46
|
+
const { managedProjectAdmissionPolicy, projectAdmissionAuthority } = await import('../../serve/shared-host-scheduler.js');
|
|
47
|
+
const activation = parallelism.activation;
|
|
48
|
+
if (!cfg || !activation || activation.configDigest !== workerActivationDigests(cfg).configDigest) {
|
|
49
|
+
throw new Error('managed admission requires a current activated policy');
|
|
50
|
+
}
|
|
51
|
+
const authority = projectAdmissionAuthority(projectRoot);
|
|
52
|
+
const authorityRef = parallelism.admission?.authorityRef ?? 'project';
|
|
53
|
+
if (authorityRef.startsWith('host:') && process.env['AIWG_EXECUTOR_IDENTITY_STORE']) {
|
|
54
|
+
ui.warn('AIWG_EXECUTOR_IDENTITY_STORE is ignored for host admission authority lookup.');
|
|
55
|
+
}
|
|
56
|
+
const policy = managedProjectAdmissionPolicy(projectRoot, { ...parallelism,
|
|
57
|
+
admission: { ...parallelism.admission, authorityRef: 'project' },
|
|
58
|
+
}, workerConfigDigest(activation), parallelism.max_parallel_subagents);
|
|
59
|
+
const binding = authorityRef === 'project' ? { store: new FileAdmissionStore(authority.path), policy }
|
|
60
|
+
: (hostRegistry ?? (await import('../../serve/executor-registry.js')).executorRegistry).resolveHostAdmissionAuthority(authorityRef, policy);
|
|
61
|
+
const scheduler = new SharedHostScheduler(binding.store, binding.policy);
|
|
62
|
+
scheduler.reconcileNow();
|
|
63
|
+
return scheduler;
|
|
64
|
+
}
|
|
65
|
+
const maxConcurrent = parallelism.max_parallel_mc_missions;
|
|
66
|
+
return new SharedHostScheduler(new FileAdmissionStore(join(projectRoot, MC_ROOT, 'admission.json')), {
|
|
67
|
+
maxConcurrent, leaseTtlMs: 5 * 60_000, agingIntervalMs: 30_000, allowPreemption: false, defaultHostQuota: maxConcurrent,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const mcWorkerSupport = () => workerConsumerSupport([...WORKER_BASELINE_FEATURES, 'worker-health.v1']);
|
|
71
|
+
function workerDispatch(mission) {
|
|
72
|
+
const bindings = new Set();
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
function visit(value) {
|
|
75
|
+
if (!value || typeof value !== 'object' || seen.has(value))
|
|
76
|
+
return;
|
|
77
|
+
seen.add(value);
|
|
78
|
+
const record = value;
|
|
79
|
+
if (record['aiwg.worker-dispatch'])
|
|
80
|
+
bindings.add(record['aiwg.worker-dispatch']);
|
|
81
|
+
visit(record.extensions);
|
|
82
|
+
visit(record.metadata);
|
|
83
|
+
for (const [key, child] of Object.entries(record))
|
|
84
|
+
if (key.startsWith('aiwg.source.'))
|
|
85
|
+
visit(child);
|
|
86
|
+
}
|
|
87
|
+
visit(mission);
|
|
88
|
+
if (bindings.size > 1)
|
|
89
|
+
throw new Error('Managed task contains multiple dispatch bindings');
|
|
90
|
+
return [...bindings][0];
|
|
91
|
+
}
|
|
92
|
+
async function readWorkerArtifact(artifact, projectRoot) {
|
|
93
|
+
const filename = artifact.ref.startsWith('file:') ? fileURLToPath(artifact.ref) : artifact.ref;
|
|
94
|
+
const inside = relative(projectRoot, filename);
|
|
95
|
+
if (!isAbsolute(filename) || inside.startsWith('..') || isAbsolute(inside))
|
|
96
|
+
throw new Error('Managed worker artifacts must be inside the project');
|
|
97
|
+
const bytes = await readWorkerBytes(filename);
|
|
98
|
+
if (`sha256:${createHash('sha256').update(bytes).digest('hex')}` !== artifact.digest)
|
|
99
|
+
throw new Error('Managed worker artifact digest changed');
|
|
100
|
+
return JSON.parse(bytes.toString('utf8'));
|
|
101
|
+
}
|
|
102
|
+
async function readWorkerBytes(filename) {
|
|
103
|
+
const { inspectWorkerPath } = await import('../../serve/worker-invocation.js');
|
|
104
|
+
if (!await inspectWorkerPath(filename, 'file'))
|
|
105
|
+
throw Object.assign(new Error('Managed worker artifact is missing'), { code: 'ENOENT' });
|
|
106
|
+
const handle = await fs.open(filename, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
107
|
+
try {
|
|
108
|
+
const stat = await handle.stat();
|
|
109
|
+
if (!stat.isFile() || stat.nlink !== 1 || stat.size > 1048576)
|
|
110
|
+
throw new Error('Invalid managed worker artifact');
|
|
111
|
+
const bytes = Buffer.alloc(1048577);
|
|
112
|
+
const { bytesRead } = await handle.read(bytes, 0, bytes.length, 0);
|
|
113
|
+
if (bytesRead > 1048576)
|
|
114
|
+
throw new Error('Managed worker artifact exceeds its size bound');
|
|
115
|
+
return bytes.subarray(0, bytesRead);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
await handle.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async function readWorkerJson(filename) {
|
|
122
|
+
return JSON.parse((await readWorkerBytes(filename)).toString('utf8'));
|
|
123
|
+
}
|
|
124
|
+
async function resolveMcWorker(ctx, session, mission, parentKind = 'mission') {
|
|
125
|
+
const dispatch = workerDispatch(mission);
|
|
126
|
+
if (!dispatch)
|
|
127
|
+
return undefined;
|
|
128
|
+
const projectRoot = ctx.cwd || process.cwd();
|
|
129
|
+
const intent = await readWorkerArtifact(dispatch.intent, projectRoot);
|
|
130
|
+
assertValidWorkerPayload('workerSelectionIntent', intent);
|
|
131
|
+
if (!dispatch.brief)
|
|
132
|
+
throw new Error('Managed worker requires a prepared brief');
|
|
133
|
+
const brief = await readWorkerArtifact(dispatch.brief, projectRoot);
|
|
134
|
+
assertValidWorkerPayload('workerBrief', brief);
|
|
135
|
+
const { workerConfigDigest } = await import('../../config/aiwg-config.js');
|
|
136
|
+
if (intent.parent.kind !== parentKind || intent.parent.id !== session.id || intent.objective !== mission.objective
|
|
137
|
+
|| intent.taskId !== dispatch.taskId || intent.requestId !== dispatch.requestId
|
|
138
|
+
|| brief.invocationId !== dispatch.invocationId || workerConfigDigest(brief.intent) !== workerConfigDigest(intent)
|
|
139
|
+
|| brief.intentDigest !== workerConfigDigest(intent))
|
|
140
|
+
throw new Error('Managed worker task authority differs from its parent');
|
|
141
|
+
const { loadWorkerInputs } = await import('./models.js');
|
|
142
|
+
const { resolveWorkerInvocation } = await import('../../serve/worker-invocation.js');
|
|
143
|
+
const inputs = await loadWorkerInputs(ctx);
|
|
144
|
+
const plan = await resolveWorkerInvocation(inputs.snapshot, { role: intent.role, task: intent.taskClass, group: intent.group,
|
|
145
|
+
brief: dispatch.brief.ref.startsWith('file:') ? fileURLToPath(dispatch.brief.ref) : dispatch.brief.ref,
|
|
146
|
+
workdir: brief.workspace.path, final_output: brief.output.resultPath, managedIntent: intent }, inputs.drivers);
|
|
147
|
+
if (!plan.invocation || plan.status !== 'eligible' || plan.invocation.readiness !== 'launchable') {
|
|
148
|
+
throw new Error(`Managed worker is ineligible: ${plan.diagnostics.map(item => item.message).join('; ')}`);
|
|
149
|
+
}
|
|
150
|
+
if (parentKind === 'mission' && plan.invocation.transport !== 'shell')
|
|
151
|
+
throw new Error('Managed Ralph capture requires a qualified shell transport');
|
|
152
|
+
return { dispatch, intent, brief, plan };
|
|
153
|
+
}
|
|
154
|
+
/** Flow adapters use the same eligibility, project authority and allowance owner. */
|
|
155
|
+
export async function createMcFlowAdapter(ctx, bindings) {
|
|
156
|
+
const cfg = await readAdmissionConfig(ctx.cwd);
|
|
157
|
+
if (!cfg?.parallelism?.worker_dispatch || cfg.parallelism.worker_mode !== 'managed') {
|
|
158
|
+
throw new Error('OG_CONTRACT_UNSUPPORTED: Flow shared dispatch requires managed mode');
|
|
159
|
+
}
|
|
160
|
+
const scheduler = await createMcAdmissionScheduler(ctx.cwd, cfg);
|
|
161
|
+
const { managedAllowanceFromOwner } = await import('../../serve/shared-host-scheduler.js');
|
|
162
|
+
const { createManagedCompositionAdapter } = await import(join(ctx.frameworkRoot, 'agentic/code/addons/composition-engine/lib/runtime.mjs'));
|
|
163
|
+
return createManagedCompositionAdapter({ scheduler, allowanceFromOwner: managedAllowanceFromOwner,
|
|
164
|
+
invokeNode: bindings.invokeNode,
|
|
165
|
+
prepareNode: async (request) => {
|
|
166
|
+
const binding = await bindings.prepareNode(request);
|
|
167
|
+
const worker = await resolveMcWorker(ctx, { id: request.runId }, {
|
|
168
|
+
id: request.node.id, objective: binding.objective, status: 'queued', loop: 0,
|
|
169
|
+
maxIterations: 1, priority: 'normal', mode: 'direct', extensions: { 'aiwg.worker-dispatch': binding.dispatch },
|
|
170
|
+
}, 'flow');
|
|
171
|
+
if (!worker || worker.intent.taskId !== request.node.id)
|
|
172
|
+
throw new Error('Flow task authority differs from node identity');
|
|
173
|
+
const selected = worker.plan.selected;
|
|
174
|
+
return { invocation: worker.plan.invocation, plan: worker.plan, brief: worker.brief,
|
|
175
|
+
admission: { requestId: worker.dispatch.requestId, orchestratorId: request.runId,
|
|
176
|
+
environment: process.env.AIWG_ENVIRONMENT ?? 'default', provider: selected.inferenceProvider, runtimeKind: 'host',
|
|
177
|
+
priority: 10, submittedAt: new Date().toISOString(), queueTimeoutMs: (cfg.parallelism.admission?.queueTimeoutSeconds ?? 300) * 1000,
|
|
178
|
+
preemptible: false, metadata: { nodeId: request.node.id, invocationId: worker.dispatch.invocationId },
|
|
179
|
+
managed: { kind: 'external', group: selected.group, member: selected.member, cli: selected.cli,
|
|
180
|
+
model: selected.modelKey, quotaScope: cfg.parallelism.clis[selected.cli].quotaScope },
|
|
181
|
+
allowance: { runId: request.runId, taskId: request.node.id,
|
|
182
|
+
taskBudgets: { maxAttempts: worker.intent.limits.maxAttempts, maxTokens: worker.intent.limits.tokens,
|
|
183
|
+
maxCostUsd: worker.intent.limits.costUsd ?? undefined, timeoutSeconds: worker.intent.limits.wallSeconds },
|
|
184
|
+
reservation: { tokens: worker.plan.invocation.limits.tokens, costUsd: worker.plan.invocation.limits.costUsd } },
|
|
185
|
+
} };
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
/** Capture completion is evidence for the parent, never an independent gate. */
|
|
190
|
+
async function readManagedWorkerState(mission, scheduler) {
|
|
191
|
+
const binding = mission.managedWorker;
|
|
192
|
+
const observation = await readWorkerJson(join(binding.runDir, 'health.json'));
|
|
193
|
+
assertValidWorkerPayload('workerHealthSummary', observation.health);
|
|
194
|
+
const dispatch = workerDispatch(mission);
|
|
195
|
+
if (observation.invocationId !== dispatch.invocationId || observation.health.observerGeneration !== mission.admissionGeneration) {
|
|
196
|
+
throw new Error('Stale managed worker observation');
|
|
197
|
+
}
|
|
198
|
+
let exit;
|
|
199
|
+
try {
|
|
200
|
+
exit = await readWorkerJson(join(binding.runDir, 'exit.json'));
|
|
201
|
+
}
|
|
202
|
+
catch (error) {
|
|
203
|
+
if (error.code !== 'ENOENT')
|
|
204
|
+
throw error;
|
|
205
|
+
}
|
|
206
|
+
if (!exit)
|
|
207
|
+
return { status: 'running', health: observation.health };
|
|
208
|
+
if (exit.invocationId !== dispatch.invocationId || exit.managedAdmission?.requestId !== mission.admissionRequestId
|
|
209
|
+
|| exit.managedAdmission?.generation !== mission.admissionGeneration)
|
|
210
|
+
throw new Error('Stale managed worker exit evidence');
|
|
211
|
+
const record = scheduler.snapshot().records[mission.admissionRequestId];
|
|
212
|
+
const usage = { inputTokens: null, outputTokens: null, cacheTokens: null, reasoningTokens: null,
|
|
213
|
+
costUsd: null, quality: 'unknown', reservedTokens: record.accounting.reservation.tokens,
|
|
214
|
+
reservedUsd: record.accounting.reservation.costUsd };
|
|
215
|
+
const plan = await readWorkerJson(binding.invocationPath);
|
|
216
|
+
const succeeded = exit.exitCode === 0 && !exit.errorCode && !exit.timedOut && !exit.cancelled;
|
|
217
|
+
const result = { version: 1, taskId: dispatch.taskId, invocationId: dispatch.invocationId,
|
|
218
|
+
briefDigest: plan.invocation.briefDigest, worker: null,
|
|
219
|
+
execution: { status: succeeded ? 'completed' : 'failed', exitCode: exit.exitCode,
|
|
220
|
+
requested: plan.invocation.selected, observedModel: null, observedFamily: null, observedEffort: null,
|
|
221
|
+
identityEvidence: [], identityQuality: 'unavailable', startedAt: mission.startedAt, endedAt: exit.endedAt,
|
|
222
|
+
termination: exit.termination, usage, failure: null, health: observation.health },
|
|
223
|
+
verification: { status: 'pending', verifierId: null, runnerRef: null, artifactDigest: null, evidence: [], findings: [] } };
|
|
224
|
+
try {
|
|
225
|
+
const claims = await readWorkerJson(plan.invocation.finalPath);
|
|
226
|
+
assertValidWorkerPayload('workerClaims', claims);
|
|
227
|
+
result.worker = claims;
|
|
228
|
+
}
|
|
229
|
+
catch { /* Invalid or absent claims remain separate from execution evidence. */ }
|
|
230
|
+
assertValidWorkerPayload('workerResult', result);
|
|
231
|
+
// Use the checkpoint's own directory; request IDs need not contain a session ID.
|
|
232
|
+
const destination = `${binding.invocationPath}.result.json`;
|
|
233
|
+
const bytes = JSON.stringify(result);
|
|
234
|
+
await fs.writeFile(destination, bytes, { mode: 0o600 });
|
|
235
|
+
dispatch.result = { ref: pathToFileURL(destination).href, digest: `sha256:${createHash('sha256').update(bytes).digest('hex')}` };
|
|
236
|
+
scheduler.acceptFinal(mission.admissionRequestId, mission.admissionGeneration, observation.health, usage);
|
|
237
|
+
// A surviving or unobservable outer supervisor cannot prove its own stop.
|
|
238
|
+
let supervisorGone = false;
|
|
239
|
+
try {
|
|
240
|
+
process.kill(mission.admissionExecution.pid, 0);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
supervisorGone = error.code === 'ESRCH';
|
|
244
|
+
}
|
|
245
|
+
if (supervisorGone && exit.termination === 'confirmed' && exit.descendants === 'stopped') {
|
|
246
|
+
const settled = scheduler.settle(mission.admissionRequestId, mission.admissionGeneration, observation.health, usage, undefined, mission.admissionExecution);
|
|
247
|
+
mission.admissionState = settled.state;
|
|
248
|
+
}
|
|
249
|
+
dispatch.state = mission.admissionState === 'settled' ? succeeded ? 'completed' : 'failed' : 'uncertain';
|
|
250
|
+
// Verification and original completion criteria remain with the parent.
|
|
251
|
+
return { status: succeeded && result.worker?.status === 'completed' ? 'paused' : 'failed', health: observation.health };
|
|
252
|
+
}
|
|
23
253
|
// ── Helpers ──────────────────────────────────────────────────
|
|
24
254
|
function genId(prefix) {
|
|
25
255
|
return `${prefix}-${Date.now().toString(36)}-${randomBytes(3).toString('hex')}`;
|
|
@@ -221,6 +451,7 @@ Queue a mission onto a session. Does NOT execute — use 'aiwg mc run' to launch
|
|
|
221
451
|
--budget-stop-policy P completion-wins (default) | budget-wins
|
|
222
452
|
--mode pty-orchestrator PTY-orchestrator mode (requires --target-agent)
|
|
223
453
|
--target-agent <id> Required for --mode pty-orchestrator
|
|
454
|
+
--worker-dispatch <path> Prepared managed worker dispatch JSON (queues only)
|
|
224
455
|
--expected-updated-at <time> Reject stale state with exit 3
|
|
225
456
|
--request-id <id> Idempotent dispatch key`,
|
|
226
457
|
run: `Usage: aiwg mc run [<session-id>] [--accept-cost]
|
|
@@ -382,9 +613,11 @@ async function mcDispatch(ctx) {
|
|
|
382
613
|
// mission-dispatch accepts only the frozen objective envelope. Worker
|
|
383
614
|
// bindings can arrive through its existing session, so inspect that payload.
|
|
384
615
|
try {
|
|
385
|
-
|
|
616
|
+
const cfg = await readAdmissionConfig(ctx.cwd || process.cwd());
|
|
617
|
+
const support = cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed' ? mcWorkerSupport() : undefined;
|
|
618
|
+
assertMissionWorkerHandoff(selected, 'mc-cli', support);
|
|
386
619
|
for (const mission of selected.missions)
|
|
387
|
-
assertMissionWorkerHandoff(mission, 'mc-cli');
|
|
620
|
+
assertMissionWorkerHandoff(mission, 'mc-cli', support);
|
|
388
621
|
}
|
|
389
622
|
catch (error) {
|
|
390
623
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -430,6 +663,24 @@ async function mcDispatch(ctx) {
|
|
|
430
663
|
mode,
|
|
431
664
|
targetAgent: targetAgent || undefined,
|
|
432
665
|
};
|
|
666
|
+
const dispatchPath = parseFlag(ctx.args, '--worker-dispatch');
|
|
667
|
+
if (dispatchPath) {
|
|
668
|
+
try {
|
|
669
|
+
const cfg = await readAdmissionConfig(ctx.cwd || process.cwd());
|
|
670
|
+
if (!cfg?.parallelism?.worker_dispatch || cfg.parallelism.worker_mode !== 'managed' || mode !== 'direct') {
|
|
671
|
+
throw new Error('OG_CONTRACT_UNSUPPORTED: worker dispatch requires the managed MC caller');
|
|
672
|
+
}
|
|
673
|
+
if (!isAbsolute(dispatchPath) || relative(ctx.cwd, dispatchPath).startsWith('..'))
|
|
674
|
+
throw new Error('Worker dispatch path must be inside the project');
|
|
675
|
+
const dispatch = await readWorkerJson(dispatchPath);
|
|
676
|
+
mission.extensions = { 'aiwg.worker-dispatch': dispatch };
|
|
677
|
+
assertMissionWorkerHandoff(mission, 'mc-cli', mcWorkerSupport());
|
|
678
|
+
await resolveMcWorker(ctx, selected, mission);
|
|
679
|
+
}
|
|
680
|
+
catch (error) {
|
|
681
|
+
return managedControlFailure(error);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
433
684
|
const result = await mutateSession(selected.id, 'dispatch', mission.id, flags.expectedUpdatedAt, flags.requestId, session => {
|
|
434
685
|
if (session.state !== 'active')
|
|
435
686
|
return 'invalid_state';
|
|
@@ -573,10 +824,13 @@ async function mcRun(ctx) {
|
|
|
573
824
|
return { exitCode: 1 };
|
|
574
825
|
}
|
|
575
826
|
const queued = session.missions.filter((m) => m.status === 'queued');
|
|
827
|
+
let cfg;
|
|
576
828
|
try {
|
|
577
|
-
|
|
829
|
+
cfg = await readAdmissionConfig(ctx.cwd || process.cwd());
|
|
830
|
+
const support = cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed' ? mcWorkerSupport() : undefined;
|
|
831
|
+
assertMissionWorkerHandoff(session, 'mc-cli', support);
|
|
578
832
|
for (const mission of queued)
|
|
579
|
-
assertMissionWorkerHandoff(mission, 'mc-cli');
|
|
833
|
+
assertMissionWorkerHandoff(mission, 'mc-cli', support);
|
|
580
834
|
}
|
|
581
835
|
catch (error) {
|
|
582
836
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -596,8 +850,15 @@ async function mcRun(ctx) {
|
|
|
596
850
|
const eligible = queued.filter(m => m.mode !== 'pty-orchestrator' && !!m.completion);
|
|
597
851
|
const projectRoot = ctx.cwd || process.cwd();
|
|
598
852
|
const frameworkRoot = ctx.frameworkRoot;
|
|
599
|
-
|
|
600
|
-
|
|
853
|
+
let scheduler;
|
|
854
|
+
try {
|
|
855
|
+
scheduler = await createMcAdmissionScheduler(projectRoot, cfg);
|
|
856
|
+
}
|
|
857
|
+
catch (error) {
|
|
858
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
859
|
+
ui.error(message);
|
|
860
|
+
return { exitCode: 1, message };
|
|
861
|
+
}
|
|
601
862
|
const provider = cfg?.providers[0] ?? 'unknown';
|
|
602
863
|
const estimate = estimateMissionRunCost(eligible, provider);
|
|
603
864
|
if (estimate.estimateUsd >= COST_WARNING_THRESHOLD_USD && !acceptCost) {
|
|
@@ -629,16 +890,20 @@ async function mcRun(ctx) {
|
|
|
629
890
|
let launched = 0;
|
|
630
891
|
let skipped = 0;
|
|
631
892
|
let failed = 0;
|
|
632
|
-
const
|
|
633
|
-
const
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
}
|
|
893
|
+
const managed = cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed';
|
|
894
|
+
const allowanceOwner = managed ? await import('../../serve/shared-host-scheduler.js') : null;
|
|
895
|
+
// Qualify every worker before admission; one parent retains completion gates.
|
|
896
|
+
const workers = new Map();
|
|
897
|
+
try {
|
|
898
|
+
for (const mission of queued)
|
|
899
|
+
if (workerDispatch(mission))
|
|
900
|
+
workers.set(mission.id, await resolveMcWorker(ctx, session, mission));
|
|
901
|
+
}
|
|
902
|
+
catch (error) {
|
|
903
|
+
return managedControlFailure(error);
|
|
904
|
+
}
|
|
641
905
|
for (const mission of queued) {
|
|
906
|
+
const worker = workers.get(mission.id);
|
|
642
907
|
if (mission.mode === 'pty-orchestrator') {
|
|
643
908
|
ui.warn(`Mission ${mission.id} mode=pty-orchestrator is not yet wired to mc run; skipping. Use 'aiwg ralph' directly for PTY-orchestrator workflows.`);
|
|
644
909
|
skipped += 1;
|
|
@@ -651,7 +916,7 @@ async function mcRun(ctx) {
|
|
|
651
916
|
}
|
|
652
917
|
const admissionRequestId = mission.admissionState === 'released'
|
|
653
918
|
? `${session.id}.${mission.id}.${Date.now().toString(36)}`
|
|
654
|
-
: mission.admissionRequestId ?? `${session.id}.${mission.id}`;
|
|
919
|
+
: mission.admissionRequestId ?? worker?.dispatch.requestId ?? `${session.id}.${mission.id}`;
|
|
655
920
|
const admissionSubmittedAt = mission.admissionState === 'released'
|
|
656
921
|
? new Date().toISOString()
|
|
657
922
|
: mission.admissionSubmittedAt ?? new Date().toISOString();
|
|
@@ -659,18 +924,35 @@ async function mcRun(ctx) {
|
|
|
659
924
|
requestId: admissionRequestId,
|
|
660
925
|
orchestratorId: session.id,
|
|
661
926
|
environment: process.env.AIWG_ENVIRONMENT ?? 'default',
|
|
662
|
-
provider,
|
|
927
|
+
provider: worker?.plan.invocation?.selected.inferenceProvider ?? provider,
|
|
663
928
|
runtimeKind: 'host',
|
|
664
929
|
priority: mission.priority === 'critical' ? 100 : mission.priority === 'high' ? 50 : mission.priority === 'low' ? 0 : 10,
|
|
665
930
|
submittedAt: admissionSubmittedAt,
|
|
666
|
-
queueTimeoutMs: 24 * 60 * 60_000,
|
|
931
|
+
queueTimeoutMs: managed ? (cfg?.parallelism?.admission?.queueTimeoutSeconds ?? 300) * 1000 : 24 * 60 * 60_000,
|
|
667
932
|
preemptible: false,
|
|
668
|
-
metadata: { missionId: mission.id },
|
|
933
|
+
metadata: { missionId: mission.id, ...(worker ? { invocationId: worker.dispatch.invocationId } : {}) },
|
|
934
|
+
...(managed ? { managed: worker ? { kind: 'external', group: worker.plan.selected.group,
|
|
935
|
+
member: worker.plan.selected.member, cli: worker.plan.selected.cli, model: worker.plan.selected.modelKey,
|
|
936
|
+
quotaScope: cfg.parallelism.clis[worker.plan.selected.cli].quotaScope } : { kind: 'native' },
|
|
937
|
+
allowance: worker ? { runId: session.id, taskId: worker.intent.taskId,
|
|
938
|
+
taskBudgets: { maxAttempts: Math.min(worker.intent.limits.maxAttempts, mission.maxIterations),
|
|
939
|
+
maxTokens: Math.min(worker.intent.limits.tokens, mission.maxTotalTokens ?? Infinity),
|
|
940
|
+
maxCostUsd: worker.intent.limits.costUsd === null ? mission.maxTotalCost
|
|
941
|
+
: Math.min(worker.intent.limits.costUsd, mission.maxTotalCost ?? Infinity),
|
|
942
|
+
timeoutSeconds: Math.min(worker.intent.limits.wallSeconds, (mission.maxWallClockMinutes ?? Infinity) * 60) },
|
|
943
|
+
reservation: { tokens: worker.plan.invocation.limits.tokens, costUsd: worker.plan.invocation.limits.costUsd },
|
|
944
|
+
} : allowanceOwner.managedAllowanceFromOwner(session.id, mission.id, {
|
|
945
|
+
kind: 'ralph', maxIterations: mission.maxIterations, maxTotalTokens: mission.maxTotalTokens,
|
|
946
|
+
maxTotalCost: mission.maxTotalCost, maxWallClockMinutes: mission.maxWallClockMinutes,
|
|
947
|
+
}),
|
|
948
|
+
} : {}),
|
|
669
949
|
});
|
|
670
950
|
mission.admissionRequestId = admissionRequestId;
|
|
671
951
|
mission.admissionSubmittedAt = admissionSubmittedAt;
|
|
672
952
|
mission.admissionState = admission.state;
|
|
673
953
|
mission.admissionLeaseExpiresAt = admission.leaseExpiresAt;
|
|
954
|
+
if (managed)
|
|
955
|
+
mission.admissionGeneration = admission.leaseGeneration;
|
|
674
956
|
await writeSession(session);
|
|
675
957
|
if (admission.state !== 'admitted') {
|
|
676
958
|
await appendLog(session.id, {
|
|
@@ -684,21 +966,62 @@ async function mcRun(ctx) {
|
|
|
684
966
|
continue;
|
|
685
967
|
}
|
|
686
968
|
try {
|
|
969
|
+
let maxWallClockMinutes = mission.maxWallClockMinutes;
|
|
970
|
+
if (managed) {
|
|
971
|
+
const scopes = scheduler.snapshot().allowances;
|
|
972
|
+
const deadlines = [scopes[admission.accounting.runKey].deadlineAt, scopes[admission.accounting.taskKey].deadlineAt]
|
|
973
|
+
.filter((value) => value !== undefined).map(Date.parse);
|
|
974
|
+
const remainingMs = Math.min(...deadlines) - Date.now();
|
|
975
|
+
if (remainingMs <= 0)
|
|
976
|
+
throw new Error('cumulative allowance deadline elapsed before launch');
|
|
977
|
+
maxWallClockMinutes = deadlines.length ? remainingMs / 60_000 : undefined;
|
|
978
|
+
}
|
|
979
|
+
if (worker) {
|
|
980
|
+
if (worker.brief.lease.generation !== mission.admissionGeneration || worker.brief.lease.reservationId !== admissionRequestId
|
|
981
|
+
|| worker.brief.lease.authorityRef !== 'project')
|
|
982
|
+
throw new Error('Managed brief lease differs from current admission');
|
|
983
|
+
const invocationPath = join(projectRoot, SESSIONS_DIR, session.id, `${mission.id}.invocation.json`);
|
|
984
|
+
const runDir = join(projectRoot, SESSIONS_DIR, session.id, `${mission.id}.run`);
|
|
985
|
+
await fs.writeFile(invocationPath, JSON.stringify(worker.plan), { mode: 0o600 });
|
|
986
|
+
mission.managedWorker = { invocationPath, runDir };
|
|
987
|
+
}
|
|
988
|
+
if (managed) {
|
|
989
|
+
scheduler.claimLaunch(admissionRequestId, mission.admissionGeneration);
|
|
990
|
+
mission.admissionLaunchClaimed = true;
|
|
991
|
+
await writeSession(session);
|
|
992
|
+
}
|
|
687
993
|
const result = await launchExternalRalph(frameworkRoot, projectRoot, {
|
|
688
994
|
objective: mission.objective,
|
|
689
995
|
completionCriteria: mission.completion,
|
|
690
|
-
maxIterations: mission.maxIterations,
|
|
691
|
-
maxTotalTokens: mission.maxTotalTokens,
|
|
996
|
+
maxIterations: managed ? admission.accounting.attemptCount : mission.maxIterations,
|
|
997
|
+
maxTotalTokens: managed ? admission.accounting.reservation.tokens ?? undefined : mission.maxTotalTokens,
|
|
692
998
|
maxOutputTokens: mission.maxOutputTokens,
|
|
693
999
|
maxToolCalls: mission.maxToolCalls,
|
|
694
|
-
maxTotalCost: mission.maxTotalCost,
|
|
695
|
-
maxWallClockMinutes
|
|
1000
|
+
maxTotalCost: managed ? admission.accounting.reservation.costUsd ?? undefined : mission.maxTotalCost,
|
|
1001
|
+
maxWallClockMinutes,
|
|
696
1002
|
explorationQuota: mission.explorationQuota,
|
|
697
1003
|
budgetStopPolicy: mission.budgetStopPolicy,
|
|
698
1004
|
// Defaults for cycle 1; advanced options can be added per-mission later.
|
|
699
1005
|
verbose: false,
|
|
1006
|
+
...(managed ? { managedAdmission: { requestId: admissionRequestId, generation: mission.admissionGeneration } } : {}),
|
|
1007
|
+
...(worker ? { managedWorker: mission.managedWorker } : {}),
|
|
700
1008
|
});
|
|
1009
|
+
if (managed) {
|
|
1010
|
+
if (!result.execution || result.execution.pid !== result.pid)
|
|
1011
|
+
throw new Error('managed launch requires its OS process birth identity');
|
|
1012
|
+
scheduler.bindExecution(admissionRequestId, mission.admissionGeneration, result.execution);
|
|
1013
|
+
mission.admissionExecution = result.execution;
|
|
1014
|
+
mission.ralphLoopId = result.loopId;
|
|
1015
|
+
mission.ralphPid = result.pid;
|
|
1016
|
+
const current = scheduler.renew(admissionRequestId, mission.admissionGeneration);
|
|
1017
|
+
if (current.state !== 'admitted') {
|
|
1018
|
+
await cancelManagedMission(ctx, scheduler, mission);
|
|
1019
|
+
throw new Error('managed launch lost admission during execution checkpoint; reconcile uncertain work');
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
701
1022
|
mission.status = 'running';
|
|
1023
|
+
if (worker)
|
|
1024
|
+
worker.dispatch.state = 'running';
|
|
702
1025
|
mission.startedAt = new Date().toISOString();
|
|
703
1026
|
mission.ralphLoopId = result.loopId;
|
|
704
1027
|
mission.ralphPid = result.pid;
|
|
@@ -717,8 +1040,12 @@ async function mcRun(ctx) {
|
|
|
717
1040
|
mission.status = 'failed';
|
|
718
1041
|
mission.error = msg;
|
|
719
1042
|
mission.completedAt = new Date().toISOString();
|
|
720
|
-
|
|
721
|
-
|
|
1043
|
+
if (managed)
|
|
1044
|
+
mission.admissionState = scheduler.stop(admissionRequestId, mission.admissionGeneration, 'observer-loss').state;
|
|
1045
|
+
else {
|
|
1046
|
+
scheduler.release(admissionRequestId);
|
|
1047
|
+
mission.admissionState = 'released';
|
|
1048
|
+
}
|
|
722
1049
|
mission.admissionLeaseExpiresAt = undefined;
|
|
723
1050
|
await writeSession(session);
|
|
724
1051
|
await appendLog(session.id, {
|
|
@@ -752,22 +1079,33 @@ async function mcRun(ctx) {
|
|
|
752
1079
|
*/
|
|
753
1080
|
async function syncMissionsFromRalph(session, projectRoot) {
|
|
754
1081
|
let mutated = false;
|
|
755
|
-
const
|
|
756
|
-
const
|
|
757
|
-
const
|
|
758
|
-
const provider = cfg?.providers[0] ?? 'unknown';
|
|
759
|
-
const maxConcurrent = resolveParallelism(cfg?.parallelism, provider).max_parallel_mc_missions;
|
|
760
|
-
const scheduler = new SharedHostScheduler(new FileAdmissionStore(join(projectRoot, MC_ROOT, 'admission.json')), { maxConcurrent, leaseTtlMs: 5 * 60_000, agingIntervalMs: 30_000, allowPreemption: false, defaultHostQuota: maxConcurrent });
|
|
1082
|
+
const cfg = await readAdmissionConfig(projectRoot);
|
|
1083
|
+
const scheduler = await createMcAdmissionScheduler(projectRoot, cfg);
|
|
1084
|
+
const managed = cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed';
|
|
761
1085
|
for (const mission of session.missions) {
|
|
762
|
-
|
|
1086
|
+
const wasRunning = mission.status === 'running';
|
|
1087
|
+
if (!wasRunning && !(managed && mission.managedWorker && mission.admissionState === 'uncertain'))
|
|
763
1088
|
continue;
|
|
764
1089
|
if (!mission.ralphLoopId)
|
|
765
1090
|
continue;
|
|
766
1091
|
const ralphStatePath = join(projectRoot, '.aiwg', 'ralph-external', 'loops', mission.ralphLoopId, 'session-state.json');
|
|
1092
|
+
if (managed) {
|
|
1093
|
+
const current = mission.admissionRequestId ? scheduler.snapshot().records[mission.admissionRequestId] : undefined;
|
|
1094
|
+
if (!current || !mission.admissionGeneration || current.leaseGeneration !== mission.admissionGeneration)
|
|
1095
|
+
continue;
|
|
1096
|
+
}
|
|
767
1097
|
try {
|
|
768
|
-
const
|
|
769
|
-
|
|
1098
|
+
const state = managed && mission.managedWorker
|
|
1099
|
+
? await readManagedWorkerState(mission, scheduler)
|
|
1100
|
+
: JSON.parse(await fs.readFile(ralphStatePath, 'utf-8'));
|
|
770
1101
|
const ralphStatus = String(state.status || '').toLowerCase();
|
|
1102
|
+
if (managed && mission.admissionRequestId && state.health !== undefined) {
|
|
1103
|
+
if (!validateWorkerPayload('workerHealthSummary', state.health).valid)
|
|
1104
|
+
throw new Error('invalid Ralph worker health');
|
|
1105
|
+
const observed = scheduler.observeHealth(mission.admissionRequestId, mission.admissionGeneration, state.health);
|
|
1106
|
+
mission.admissionState = observed.state;
|
|
1107
|
+
mutated = true;
|
|
1108
|
+
}
|
|
771
1109
|
// Map ralph status → mc mission status
|
|
772
1110
|
let nextStatus = null;
|
|
773
1111
|
if (ralphStatus === 'completed')
|
|
@@ -778,6 +1116,8 @@ async function syncMissionsFromRalph(session, projectRoot) {
|
|
|
778
1116
|
nextStatus = 'aborted';
|
|
779
1117
|
else if (ralphStatus === 'paused')
|
|
780
1118
|
nextStatus = 'paused';
|
|
1119
|
+
if (!wasRunning)
|
|
1120
|
+
nextStatus = null;
|
|
781
1121
|
// 'running' stays running
|
|
782
1122
|
// Reflect iteration count if available
|
|
783
1123
|
const iter = typeof state.iteration === 'number' ? state.iteration : (typeof state.currentIteration === 'number' ? state.currentIteration : null);
|
|
@@ -797,17 +1137,29 @@ async function syncMissionsFromRalph(session, projectRoot) {
|
|
|
797
1137
|
}
|
|
798
1138
|
mutated = true;
|
|
799
1139
|
if (mission.admissionRequestId) {
|
|
800
|
-
|
|
801
|
-
|
|
1140
|
+
if (managed) {
|
|
1141
|
+
if (!mission.managedWorker) {
|
|
1142
|
+
if (state.health)
|
|
1143
|
+
scheduler.acceptFinal(mission.admissionRequestId, mission.admissionGeneration, state.health);
|
|
1144
|
+
}
|
|
1145
|
+
if (mission.admissionState !== 'settled')
|
|
1146
|
+
mission.admissionState = scheduler.stop(mission.admissionRequestId, mission.admissionGeneration, 'observer-loss').state;
|
|
1147
|
+
}
|
|
1148
|
+
else {
|
|
1149
|
+
try {
|
|
1150
|
+
scheduler.release(mission.admissionRequestId);
|
|
1151
|
+
}
|
|
1152
|
+
catch { /* already expired or released */ }
|
|
1153
|
+
mission.admissionState = 'released';
|
|
802
1154
|
}
|
|
803
|
-
catch { /* already expired or released */ }
|
|
804
|
-
mission.admissionState = 'released';
|
|
805
1155
|
mission.admissionLeaseExpiresAt = undefined;
|
|
806
1156
|
}
|
|
807
1157
|
}
|
|
808
1158
|
else if (mission.admissionRequestId) {
|
|
809
1159
|
try {
|
|
810
|
-
const renewed =
|
|
1160
|
+
const renewed = managed
|
|
1161
|
+
? scheduler.renew(mission.admissionRequestId, mission.admissionGeneration)
|
|
1162
|
+
: scheduler.renew(mission.admissionRequestId);
|
|
811
1163
|
mission.admissionState = renewed.state;
|
|
812
1164
|
mission.admissionLeaseExpiresAt = renewed.leaseExpiresAt;
|
|
813
1165
|
mutated = true;
|
|
@@ -818,7 +1170,12 @@ async function syncMissionsFromRalph(session, projectRoot) {
|
|
|
818
1170
|
}
|
|
819
1171
|
}
|
|
820
1172
|
catch {
|
|
821
|
-
|
|
1173
|
+
if (managed && mission.admissionRequestId) {
|
|
1174
|
+
mission.admissionState = scheduler.disconnect(mission.admissionRequestId, mission.admissionGeneration).state;
|
|
1175
|
+
mission.admissionLeaseExpiresAt = undefined;
|
|
1176
|
+
mutated = true;
|
|
1177
|
+
}
|
|
1178
|
+
// Missing supervision preserves the last task status and all artifacts.
|
|
822
1179
|
}
|
|
823
1180
|
}
|
|
824
1181
|
if (mutated) {
|
|
@@ -909,6 +1266,46 @@ async function mcStatus(ctx) {
|
|
|
909
1266
|
ui.blank();
|
|
910
1267
|
return { exitCode: 0 };
|
|
911
1268
|
}
|
|
1269
|
+
async function managedControlScheduler(ctx, missions) {
|
|
1270
|
+
const projectRoot = ctx.cwd || process.cwd();
|
|
1271
|
+
const cfg = await readAdmissionConfig(projectRoot);
|
|
1272
|
+
if (!cfg?.parallelism?.worker_dispatch || cfg.parallelism.worker_mode !== 'managed')
|
|
1273
|
+
return undefined;
|
|
1274
|
+
const scheduler = await createMcAdmissionScheduler(projectRoot, cfg);
|
|
1275
|
+
const snapshot = scheduler.snapshot();
|
|
1276
|
+
for (const mission of missions) {
|
|
1277
|
+
if (!mission.admissionRequestId)
|
|
1278
|
+
continue;
|
|
1279
|
+
const record = snapshot.records[mission.admissionRequestId];
|
|
1280
|
+
if (!record || !mission.admissionGeneration || record.leaseGeneration !== mission.admissionGeneration) {
|
|
1281
|
+
throw new Error('stale managed Mission checkpoint generation');
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
return scheduler;
|
|
1285
|
+
}
|
|
1286
|
+
function managedControlFailure(error) {
|
|
1287
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1288
|
+
ui.error(message);
|
|
1289
|
+
return { exitCode: 1, message };
|
|
1290
|
+
}
|
|
1291
|
+
async function cancelManagedMission(ctx, scheduler, mission) {
|
|
1292
|
+
if (!mission.admissionRequestId)
|
|
1293
|
+
return;
|
|
1294
|
+
const cancelled = scheduler.cancel(mission.admissionRequestId, mission.admissionGeneration);
|
|
1295
|
+
mission.admissionState = cancelled.state;
|
|
1296
|
+
mission.admissionLeaseExpiresAt = undefined;
|
|
1297
|
+
if (cancelled.state !== 'uncertain' || !mission.ralphLoopId || !mission.admissionExecution)
|
|
1298
|
+
return;
|
|
1299
|
+
try {
|
|
1300
|
+
const { cancelManagedRalph } = await import('./ralph-launcher.js');
|
|
1301
|
+
await cancelManagedRalph(ctx.frameworkRoot, ctx.cwd || process.cwd(), mission.ralphLoopId, {
|
|
1302
|
+
requestId: mission.admissionRequestId, generation: mission.admissionGeneration, execution: mission.admissionExecution,
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
catch {
|
|
1306
|
+
// Reservation is already uncertain; unavailable signalling cannot release it.
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
912
1309
|
async function mcAbort(ctx) {
|
|
913
1310
|
const positional = getPositionalArgs(ctx.args);
|
|
914
1311
|
const sessionId = positional[0];
|
|
@@ -917,6 +1314,14 @@ async function mcAbort(ctx) {
|
|
|
917
1314
|
ui.error('Usage: aiwg mc abort <session-id> <mission-id>');
|
|
918
1315
|
return { exitCode: 1 };
|
|
919
1316
|
}
|
|
1317
|
+
let managedScheduler;
|
|
1318
|
+
try {
|
|
1319
|
+
const selected = await readSession(sessionId);
|
|
1320
|
+
managedScheduler = await managedControlScheduler(ctx, selected?.missions.filter(mission => mission.id === missionId) ?? []);
|
|
1321
|
+
}
|
|
1322
|
+
catch (error) {
|
|
1323
|
+
return managedControlFailure(error);
|
|
1324
|
+
}
|
|
920
1325
|
const flags = mutationFlags(ctx.args);
|
|
921
1326
|
const result = await mutateSession(sessionId, 'cancel', missionId, flags.expectedUpdatedAt, flags.requestId, session => {
|
|
922
1327
|
const mission = session.missions.find(m => m.id === missionId);
|
|
@@ -934,17 +1339,21 @@ async function mcAbort(ctx) {
|
|
|
934
1339
|
const mission = result.session.missions.find(candidate => candidate.id === missionId);
|
|
935
1340
|
if (mission?.admissionRequestId) {
|
|
936
1341
|
try {
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1342
|
+
if (managedScheduler) {
|
|
1343
|
+
await cancelManagedMission(ctx, managedScheduler, mission);
|
|
1344
|
+
await writeSession(result.session);
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
const projectRoot = ctx.cwd || process.cwd();
|
|
1348
|
+
const cfg = await readAdmissionConfig(projectRoot);
|
|
1349
|
+
const scheduler = await createMcAdmissionScheduler(projectRoot, cfg);
|
|
1350
|
+
scheduler.cancel(mission.admissionRequestId);
|
|
1351
|
+
}
|
|
945
1352
|
}
|
|
946
|
-
catch {
|
|
947
|
-
|
|
1353
|
+
catch (error) {
|
|
1354
|
+
if (managedScheduler)
|
|
1355
|
+
return managedControlFailure(error);
|
|
1356
|
+
// Legacy leases keep their existing best-effort cancellation behavior.
|
|
948
1357
|
}
|
|
949
1358
|
}
|
|
950
1359
|
}
|
|
@@ -959,6 +1368,13 @@ async function mcPause(ctx) {
|
|
|
959
1368
|
ui.error('No active session to pause.');
|
|
960
1369
|
return { exitCode: 1 };
|
|
961
1370
|
}
|
|
1371
|
+
let managedScheduler;
|
|
1372
|
+
try {
|
|
1373
|
+
managedScheduler = await managedControlScheduler(ctx, selected.missions.filter(mission => mission.status === 'running'));
|
|
1374
|
+
}
|
|
1375
|
+
catch (error) {
|
|
1376
|
+
return managedControlFailure(error);
|
|
1377
|
+
}
|
|
962
1378
|
const flags = mutationFlags(ctx.args);
|
|
963
1379
|
const result = await mutateSession(selected.id, 'pause', selected.id, flags.expectedUpdatedAt, flags.requestId, session => {
|
|
964
1380
|
if (session.state !== 'active')
|
|
@@ -971,6 +1387,13 @@ async function mcPause(ctx) {
|
|
|
971
1387
|
});
|
|
972
1388
|
if (!result.ok)
|
|
973
1389
|
return mutationFailure(result);
|
|
1390
|
+
if (!result.replayed && managedScheduler) {
|
|
1391
|
+
for (const mission of result.session.missions) {
|
|
1392
|
+
if (mission.status === 'paused')
|
|
1393
|
+
await cancelManagedMission(ctx, managedScheduler, mission);
|
|
1394
|
+
}
|
|
1395
|
+
await writeSession(result.session);
|
|
1396
|
+
}
|
|
974
1397
|
ui.success(`${result.replayed ? 'Replayed pause for' : 'Paused'} session: ${selected.id}`);
|
|
975
1398
|
return { exitCode: 0, message: JSON.stringify({ ok: true, replayed: result.replayed, updated_at: result.session.updatedAt }) };
|
|
976
1399
|
}
|
|
@@ -982,6 +1405,26 @@ async function mcResume(ctx) {
|
|
|
982
1405
|
ui.error('No paused session to resume.');
|
|
983
1406
|
return { exitCode: 1 };
|
|
984
1407
|
}
|
|
1408
|
+
try {
|
|
1409
|
+
const cfg = await readAdmissionConfig(ctx.cwd || process.cwd());
|
|
1410
|
+
if (cfg?.parallelism?.worker_dispatch && cfg.parallelism.worker_mode === 'managed') {
|
|
1411
|
+
const scheduler = await createMcAdmissionScheduler(ctx.cwd || process.cwd(), cfg);
|
|
1412
|
+
const snapshot = scheduler.snapshot();
|
|
1413
|
+
for (const mission of selected.missions.filter(candidate => candidate.status === 'paused')) {
|
|
1414
|
+
if (!mission.admissionRequestId)
|
|
1415
|
+
continue;
|
|
1416
|
+
const record = snapshot.records[mission.admissionRequestId];
|
|
1417
|
+
if (!record || record.leaseGeneration !== mission.admissionGeneration || record.state !== 'admitted') {
|
|
1418
|
+
throw new Error('managed resume requires current admission and reconciliation of uncertain work');
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
catch (error) {
|
|
1424
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1425
|
+
ui.error(message);
|
|
1426
|
+
return { exitCode: 1, message };
|
|
1427
|
+
}
|
|
985
1428
|
const flags = mutationFlags(ctx.args);
|
|
986
1429
|
const result = await mutateSession(selected.id, 'resume', selected.id, flags.expectedUpdatedAt, flags.requestId, session => {
|
|
987
1430
|
if (session.state !== 'paused')
|
|
@@ -1006,6 +1449,13 @@ async function mcStop(ctx) {
|
|
|
1006
1449
|
ui.error('No active session to stop.');
|
|
1007
1450
|
return { exitCode: 1 };
|
|
1008
1451
|
}
|
|
1452
|
+
let managedScheduler;
|
|
1453
|
+
try {
|
|
1454
|
+
managedScheduler = await managedControlScheduler(ctx, session.missions.filter(mission => mission.status === 'queued' || (!drain && (mission.status === 'running' || mission.status === 'paused'))));
|
|
1455
|
+
}
|
|
1456
|
+
catch (error) {
|
|
1457
|
+
return managedControlFailure(error);
|
|
1458
|
+
}
|
|
1009
1459
|
if (drain) {
|
|
1010
1460
|
// Mark queued missions as aborted, let running finish
|
|
1011
1461
|
for (const m of session.missions) {
|
|
@@ -1025,6 +1475,12 @@ async function mcStop(ctx) {
|
|
|
1025
1475
|
}
|
|
1026
1476
|
}
|
|
1027
1477
|
}
|
|
1478
|
+
if (managedScheduler) {
|
|
1479
|
+
for (const mission of session.missions) {
|
|
1480
|
+
if (mission.status === 'aborted')
|
|
1481
|
+
await cancelManagedMission(ctx, managedScheduler, mission);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1028
1484
|
session.state = 'stopped';
|
|
1029
1485
|
await writeSession(session);
|
|
1030
1486
|
await appendLog(session.id, { event: 'session_stopped', drain });
|