@aiwg/cli 2026.10.9 → 2026.10.10

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.
Files changed (91) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +197 -3
  2. package/bin/aiwg.mjs +8 -0
  3. package/dist/src/cli/handlers/mc-admission.js +39 -0
  4. package/dist/src/cli/handlers/mc.js +2 -31
  5. package/dist/src/cli/handlers/models-help.js +52 -0
  6. package/dist/src/cli/handlers/models.js +11 -81
  7. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  8. package/dist/src/cli/handlers/use.js +191 -230
  9. package/dist/src/cli/ui.js +1 -0
  10. package/dist/src/config/aiwg-config.js +38 -184
  11. package/dist/src/config/cli-help.js +54 -0
  12. package/dist/src/config/cli.js +5 -69
  13. package/dist/src/config/config-normalization.js +10 -0
  14. package/dist/src/config/worker-activation-gate.js +26 -0
  15. package/dist/src/config/worker-adoption-cli.js +102 -0
  16. package/dist/src/config/worker-adoption.js +368 -0
  17. package/dist/src/config/worker-config-digest.js +14 -0
  18. package/dist/src/config/worker-group-templates.js +150 -0
  19. package/dist/src/config/worker-guidance-warning.js +28 -0
  20. package/dist/src/config/worker-owner-lock.js +152 -0
  21. package/dist/src/config/worker-parallelism.js +154 -0
  22. package/dist/src/config/worker-write.js +75 -0
  23. package/dist/src/decision/telemetry/exporter.js +2 -1
  24. package/dist/src/decision/telemetry/index.js +1 -0
  25. package/dist/src/decision/telemetry/mapping.js +43 -0
  26. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  27. package/dist/src/decision/telemetry/redaction.js +6 -1
  28. package/dist/src/decision/telemetry/types.js +3 -0
  29. package/dist/src/decision/telemetry/worker.js +69 -0
  30. package/dist/src/lint/rule-activation.js +15 -7
  31. package/dist/src/lint/runner.js +2 -2
  32. package/dist/src/mcp/server.mjs +2 -11
  33. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  34. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  35. package/dist/src/mcp/tools/worker-invoke.mjs +6 -6
  36. package/dist/src/mission-protocol/codecs.js +8 -38
  37. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  38. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  39. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  40. package/dist/src/models/conductor-data-policy.mjs +190 -0
  41. package/dist/src/models/config-loader.js +19 -3
  42. package/dist/src/models/config-writer.js +27 -0
  43. package/dist/src/models/model-capabilities.v1.json +197 -3
  44. package/dist/src/models/provider-policy.js +7 -155
  45. package/dist/src/models/resolver.js +2 -293
  46. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  47. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  48. package/dist/src/models/worker-direct-readiness.js +9 -218
  49. package/dist/src/models/worker-health-readiness.js +105 -26
  50. package/dist/src/models/worker-installation.mjs +12 -14
  51. package/dist/src/models/worker-member-references.js +64 -0
  52. package/dist/src/models/worker-plan.js +317 -0
  53. package/dist/src/models/worker-selection-policy.js +217 -0
  54. package/dist/src/models/wrapper-deployment.js +1 -1
  55. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  56. package/dist/src/serve/shared-host-scheduler.js +25 -107
  57. package/dist/src/serve/shared-host-starvation.js +73 -0
  58. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  59. package/dist/src/serve/worker-artifacts.mjs +56 -0
  60. package/dist/src/serve/worker-command-request.js +68 -0
  61. package/dist/src/serve/worker-invocation-paths.js +144 -0
  62. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  63. package/dist/src/serve/worker-invocation.js +24 -291
  64. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  65. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  66. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  67. package/package.json +9 -1
  68. package/schemas/mission-v1.schema.json +20 -19
  69. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  70. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  71. package/tools/agents/providers/claude.mjs +3 -30
  72. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  73. package/tools/agents/providers/codex.mjs +2 -8
  74. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  75. package/tools/agents/providers/rule-delivery.mjs +3 -16
  76. package/tools/agents/providers/rule-presentation.mjs +38 -0
  77. package/tools/plugin/package-plugins.mjs +1 -1
  78. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  79. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  80. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  81. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  82. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  83. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  84. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  85. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  86. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  87. package/tools/ralph-external/lib/worker-isolation.mjs +2 -44
  88. package/tools/ralph-external/lib/worker-launch-cli.mjs +47 -0
  89. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  90. package/tools/ralph-external/session-launcher.mjs +73 -217
  91. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -0,0 +1,116 @@
1
+ export function attemptBlock(snapshot, candidate, policy, now) {
2
+ const accounting = candidate.accounting;
3
+ const deny = (cause, kind = 'policy-exclusion') => ({ kind, cause, terminal: true });
4
+ const attempts = Object.values(snapshot.records).filter(record => record.accounting?.attemptNumber !== undefined);
5
+ const taskAttempts = attempts.filter(record => record.accounting.taskKey === accounting.taskKey);
6
+ if (taskAttempts.length) {
7
+ const previous = taskAttempts.sort((a, b) => a.accounting.attemptNumber - b.accounting.attemptNumber).at(-1);
8
+ if (candidate.allowance?.previousRequestId !== previous.requestId)
9
+ return deny('new attempt must reference the latest task attempt');
10
+ if (previous.state !== 'settled')
11
+ return deny('previous attempt requires proof-checked reconciliation before replacement');
12
+ const failure = previous.accounting.failure;
13
+ const purpose = candidate.allowance?.purpose ?? 'retry';
14
+ if (purpose === 'retry' || purpose === 'fallback' || failure?.class === 'rate-limit') {
15
+ if (!failure || !failure.retryable || !['rate-limit', 'unavailable', 'timeout'].includes(failure.class))
16
+ return deny('previous failure does not permit automatic retry');
17
+ if (!(policy.managed.retry?.on ?? ['rate-limit', 'unavailable', 'timeout']).includes(failure.class))
18
+ return deny(`${failure.class} retries are disabled`);
19
+ }
20
+ }
21
+ else if (candidate.allowance?.previousRequestId)
22
+ return deny('unknown previous task attempt');
23
+ for (const key of [accounting.runKey, accounting.taskKey]) {
24
+ const scope = snapshot.allowances[key];
25
+ if (Date.parse(scope.deadlineAt ?? '') <= now)
26
+ return deny('cumulative allowance deadline elapsed');
27
+ const prior = attempts.filter(record => record.accounting.runKey === key || record.accounting.taskKey === key);
28
+ if (scope.limits.maxAttempts !== undefined && prior.reduce((sum, record) => sum + record.accounting.attemptCount, 0) + accounting.attemptCount > scope.limits.maxAttempts)
29
+ return deny('cumulative attempt allowance exhausted', 'budget-denial');
30
+ for (const [dimension, limit] of [['tokens', scope.limits.maxTokens], ['costUsd', scope.limits.maxCostUsd]]) {
31
+ if (limit === undefined)
32
+ continue;
33
+ let used = 0;
34
+ for (const record of prior) {
35
+ const value = chargedUsage(record, dimension);
36
+ if (value === null)
37
+ return deny(`unknown ${dimension} charge has no finite reservation`, 'budget-denial');
38
+ used += value;
39
+ }
40
+ const reserved = accounting.reservation[dimension];
41
+ if (reserved === null || reserved === 0 || used + reserved > limit)
42
+ return deny(`cumulative ${dimension} reservation exceeds allowance`, 'budget-denial');
43
+ }
44
+ }
45
+ if (taskAttempts.some(record => Date.parse(record.accounting.notBefore ?? '') > now))
46
+ return { kind: 'policy-exclusion', cause: 'retry-backoff', terminal: false };
47
+ if (policy.managed.budgets?.unknownCost === 'deny' && accounting.reservation.costUsd === null)
48
+ return deny('unknown cost is denied by policy', 'budget-denial');
49
+ const quotaScope = candidate.managed?.cli && snapshot.hostBinding
50
+ ? policy.managed.upstream[candidate.managed.cli].quotaScope : candidate.managed?.quotaScope;
51
+ const quotas = quotaScope ? [`quota/${encodeURIComponent(quotaScope)}`]
52
+ : Object.keys(snapshot.cooldowns ?? {});
53
+ if (quotas.some(key => Date.parse(snapshot.cooldowns?.[key]?.until ?? '') > now))
54
+ return { kind: 'policy-exclusion', cause: 'quota-cooldown', terminal: false };
55
+ return undefined;
56
+ }
57
+ function chargedUsage(record, dimension) {
58
+ const accounting = record.accounting;
59
+ const usage = accounting.usage;
60
+ const measured = !usage || usage.quality === 'unknown' ? null : dimension === 'costUsd' ? usage.costUsd
61
+ : usage.inputTokens === null || usage.outputTokens === null || usage.cacheTokens === null || usage.reasoningTokens === null ? null
62
+ : usage.inputTokens + usage.outputTokens + usage.cacheTokens + usage.reasoningTokens;
63
+ const reservation = accounting.reservation[dimension];
64
+ // Uncertain execution can still accrue spend; estimates never refund a reserve.
65
+ if (record.state !== 'settled' || usage?.quality !== 'reported') {
66
+ return reservation === null || measured === null ? reservation : Math.max(reservation, measured);
67
+ }
68
+ return measured ?? reservation;
69
+ }
70
+ export function capacityBlock(snapshot, candidate, policy, now = -Infinity) {
71
+ const slots = policy.managed ? candidate.reservedSlots ?? 1 : 1;
72
+ const active = Object.values(snapshot.records).filter(record => record.state === 'admitted' || (policy.managed && record.state === 'uncertain'));
73
+ const limits = [
74
+ { dimension: 'global', cap: policy.maxConcurrent, matches: () => true },
75
+ ];
76
+ if (policy.managed)
77
+ for (const key of candidate.dimensions ?? []) {
78
+ const cap = policy.managed.dimensionCaps[key];
79
+ if (cap !== undefined)
80
+ limits.push({ dimension: key, cap, matches: record => !record.dimensions || record.dimensions.includes(key) });
81
+ }
82
+ for (const [field, quotas] of [
83
+ ['environment', policy.environmentQuotas], ['provider', policy.providerQuotas],
84
+ ['runtimeKind', { host: policy.defaultHostQuota ?? 1, ...policy.runtimeQuotas }],
85
+ ]) {
86
+ const cap = quotas?.[candidate[field]];
87
+ if (cap !== undefined)
88
+ limits.push({ dimension: `${field}/${candidate[field]}`, cap: policy.managed ? cap : Math.ceil(cap), matches: record => record[field] === candidate[field] });
89
+ }
90
+ const impossible = limits.find(limit => slots > limit.cap);
91
+ if (impossible)
92
+ return { kind: 'policy-exclusion', cause: 'request-exceeds-cap', dimension: impossible.dimension, terminal: false };
93
+ const used = (records, limit) => records.filter(limit.matches)
94
+ .reduce((sum, record) => sum + (policy.managed ? record.reservedSlots ?? 1 : 1), 0);
95
+ const blocked = limits.filter(limit => used(active, limit) + slots > limit.cap);
96
+ if (!blocked.length)
97
+ return undefined;
98
+ const dimensions = blocked.map(limit => limit.dimension);
99
+ const certain = active.filter(record => record.state === 'admitted' && !(Date.parse(record.leaseExpiresAt ?? '') <= now));
100
+ if (policy.managed && blocked.some(limit => used(certain, limit) + slots <= limit.cap)) {
101
+ return { kind: 'safety-blocked', cause: 'uncertain-lease', dimension: dimensions[0], dimensions, terminal: false };
102
+ }
103
+ return { kind: 'capacity-wait', cause: 'capacity-cap', dimension: dimensions[0], dimensions, terminal: false };
104
+ }
105
+ /** Past retry/cooldown exclusion does not count toward an eligible service gap. */
106
+ export function attemptReadyAt(snapshot, candidate, policy) {
107
+ const retry = Object.values(snapshot.records).filter(record => record.accounting?.attemptNumber !== undefined
108
+ && record.accounting.taskKey === candidate.accounting?.taskKey)
109
+ .reduce((latest, record) => Math.max(latest, Date.parse(record.accounting.notBefore ?? '') || 0), 0);
110
+ const quota = candidate.managed?.cli && snapshot.hostBinding
111
+ ? policy.managed.upstream[candidate.managed.cli].quotaScope : candidate.managed?.quotaScope;
112
+ const cooldown = Object.entries(snapshot.cooldowns ?? {}).filter(([key]) => !quota || key === `quota/${encodeURIComponent(quota)}`)
113
+ .reduce((latest, [, value]) => Math.max(latest, Date.parse(value.until) || 0), 0);
114
+ return Math.max(retry, cooldown);
115
+ }
116
+ //# sourceMappingURL=shared-host-admission-checks.js.map
@@ -14,6 +14,9 @@ import { hostname } from 'node:os';
14
14
  import { createHash } from 'node:crypto';
15
15
  import { advanceProgressWatermark, assertMissionWorkerHandoff, assertValidWorkerPayload, decideLeaseMapping, projectHealthTrace } from '../mission-protocol/codecs.js';
16
16
  import { isCanonicalWorkerModelKey } from '../models/provider-policy.js';
17
+ import { attemptBlock, capacityBlock } from './shared-host-admission-checks.js';
18
+ import { projectAdmissionDiagnostics } from './shared-host-starvation.js';
19
+ import { resolveParallelism } from '../config/aiwg-config.js';
17
20
  /** Translate existing owner ceilings without creating another budget contract. */
18
21
  export function managedAllowanceFromOwner(runId, taskId, owner) {
19
22
  const budgets = owner.kind === 'mission' ? owner.budgets : owner.kind === 'ralph' ? {
@@ -57,8 +60,8 @@ export function managedDimensionKeys(projectId, worker) {
57
60
  ...(worker.quotaScope ? [`quota/${encodeURIComponent(worker.quotaScope)}`] : []),
58
61
  ];
59
62
  }
60
- /** Build the project policy for the existing config/activation owner. */
61
63
  export function managedProjectAdmissionPolicy(projectRoot, parallelism, policyDigest, maxConcurrent) {
64
+ parallelism = resolveParallelism(parallelism);
62
65
  const { projectId } = projectAdmissionAuthority(projectRoot);
63
66
  const admission = parallelism.admission;
64
67
  if ((admission?.authorityRef ?? 'project') !== 'project')
@@ -90,6 +93,7 @@ export function managedProjectAdmissionPolicy(projectRoot, parallelism, policyDi
90
93
  add({ kind: 'native', quotaScope }, cap);
91
94
  return {
92
95
  maxConcurrent, leaseTtlMs: (admission?.leaseSeconds ?? 60) * 1000,
96
+ starvationThresholdMs: (admission?.starvationSeconds ?? 120) * 1000,
93
97
  agingIntervalMs: 30_000, allowPreemption: false, defaultHostQuota: maxConcurrent,
94
98
  managed: {
95
99
  authorityRef: 'project', projectId, policyDigest, maxQueued: admission?.maxQueued ?? 128,
@@ -97,7 +101,7 @@ export function managedProjectAdmissionPolicy(projectRoot, parallelism, policyDi
97
101
  budgets: parallelism.budgets, retry: parallelism.retry,
98
102
  installations: Object.fromEntries(Object.entries(parallelism.clis ?? {}).filter(([, cli]) => cli.enabled !== false)
99
103
  .map(([id, cli]) => [id, { accountScope: cli.accountScope, quotaScope: cli.quotaScope }])),
100
- members: Object.fromEntries(Object.entries(parallelism.groups ?? {}).flatMap(([group, definition]) => definition.members.filter(member => member.enabled !== false && parallelism.clis?.[member.cli]?.enabled !== false)
104
+ members: Object.fromEntries(Object.entries(parallelism.groups ?? {}).flatMap(([group, definition]) => (definition.members ?? []).filter(member => member.enabled !== false && parallelism.clis?.[member.cli]?.enabled !== false)
101
105
  .map(member => [`${group}/${member.id}`, { cli: member.cli }]))),
102
106
  },
103
107
  };
@@ -256,16 +260,21 @@ export class SharedHostScheduler {
256
260
  store;
257
261
  policy;
258
262
  clock;
259
- constructor(store, policy, clock = Date.now) {
263
+ checkActivation;
264
+ constructor(store, policy, clock = Date.now, checkActivation) {
260
265
  this.store = store;
261
266
  this.policy = policy;
262
267
  this.clock = clock;
268
+ this.checkActivation = checkActivation;
263
269
  if (!Number.isInteger(policy.maxConcurrent) || policy.maxConcurrent < 1) {
264
270
  throw new Error('maxConcurrent must be a positive integer');
265
271
  }
266
272
  if (policy.leaseTtlMs < 1 || policy.agingIntervalMs < 1) {
267
273
  throw new Error('leaseTtlMs and agingIntervalMs must be positive');
268
274
  }
275
+ if (!Number.isSafeInteger(policy.starvationThresholdMs ?? 120_000) || (policy.starvationThresholdMs ?? 120_000) < 1) {
276
+ throw new Error('starvationThresholdMs must be a positive safe integer');
277
+ }
269
278
  if (policy.managed)
270
279
  this.policy = structuredClone(policy);
271
280
  const managed = this.policy.managed;
@@ -347,6 +356,7 @@ export class SharedHostScheduler {
347
356
  const record = snapshot.records[requestKey];
348
357
  record.state = 'denied';
349
358
  record.reason = 'managed admission queue is full';
359
+ record.denial = { kind: 'policy-exclusion', cause: record.reason, terminal: true };
350
360
  record.finishedAt = new Date(this.clock()).toISOString();
351
361
  record.revision += 1;
352
362
  }
@@ -609,6 +619,7 @@ export class SharedHostScheduler {
609
619
  snapshot.allowances = Object.fromEntries(Object.entries(snapshot.allowances ?? {})
610
620
  .filter(([key]) => JSON.parse(key)[0] === projectId));
611
621
  }
622
+ snapshot.diagnostics = projectAdmissionDiagnostics(value, record => this.policyFor(value, record), this.clock(), this.policy.managed?.authorityRef.startsWith('host:') ? this.policy.managed.projectId : undefined);
612
623
  return snapshot;
613
624
  }
614
625
  reconcile(snapshot) {
@@ -717,66 +728,17 @@ export class SharedHostScheduler {
717
728
  };
718
729
  }
719
730
  allowAttempt(snapshot, candidate, now) {
720
- const accounting = candidate.accounting;
721
- const deny = (reason) => {
731
+ const block = attemptBlock(snapshot, candidate, this.policyFor(snapshot, candidate), now);
732
+ if (!block)
733
+ return true;
734
+ if (block.terminal) {
722
735
  candidate.state = 'denied';
723
- candidate.reason = reason;
736
+ candidate.reason = block.cause;
737
+ candidate.denial = block;
724
738
  candidate.finishedAt = new Date(now).toISOString();
725
739
  candidate.revision += 1;
726
- return false;
727
- };
728
- const attempts = Object.values(snapshot.records).filter(record => record.accounting?.attemptNumber !== undefined);
729
- const taskAttempts = attempts.filter(record => record.accounting.taskKey === accounting.taskKey);
730
- if (taskAttempts.length) {
731
- const previous = taskAttempts.sort((a, b) => a.accounting.attemptNumber - b.accounting.attemptNumber).at(-1);
732
- if (candidate.allowance?.previousRequestId !== previous.requestId)
733
- return deny('new attempt must reference the latest task attempt');
734
- if (previous.state !== 'settled')
735
- return deny('previous attempt requires proof-checked reconciliation before replacement');
736
- const failure = previous.accounting.failure;
737
- const purpose = candidate.allowance?.purpose ?? 'retry';
738
- if (purpose === 'retry' || purpose === 'fallback' || failure?.class === 'rate-limit') {
739
- if (!failure || !failure.retryable || !['rate-limit', 'unavailable', 'timeout'].includes(failure.class))
740
- return deny('previous failure does not permit automatic retry');
741
- if (!(this.policyFor(snapshot, candidate).managed.retry?.on ?? ['rate-limit', 'unavailable', 'timeout']).includes(failure.class))
742
- return deny(`${failure.class} retries are disabled`);
743
- }
744
- }
745
- else if (candidate.allowance?.previousRequestId)
746
- return deny('unknown previous task attempt');
747
- for (const key of [accounting.runKey, accounting.taskKey]) {
748
- const scope = snapshot.allowances[key];
749
- if (Date.parse(scope.deadlineAt ?? '') <= now)
750
- return deny('cumulative allowance deadline elapsed');
751
- const prior = attempts.filter(record => record.accounting.runKey === key || record.accounting.taskKey === key);
752
- if (scope.limits.maxAttempts !== undefined && prior.reduce((sum, record) => sum + record.accounting.attemptCount, 0) + accounting.attemptCount > scope.limits.maxAttempts)
753
- return deny('cumulative attempt allowance exhausted');
754
- for (const [dimension, limit] of [['tokens', scope.limits.maxTokens], ['costUsd', scope.limits.maxCostUsd]]) {
755
- if (limit === undefined)
756
- continue;
757
- let used = 0;
758
- for (const record of prior) {
759
- const value = chargedUsage(record, dimension);
760
- if (value === null)
761
- return deny(`unknown ${dimension} charge has no finite reservation`);
762
- used += value;
763
- }
764
- const reserved = accounting.reservation[dimension];
765
- if (reserved === null || reserved === 0 || used + reserved > limit)
766
- return deny(`cumulative ${dimension} reservation exceeds allowance`);
767
- }
768
740
  }
769
- if (taskAttempts.some(record => Date.parse(record.accounting.notBefore ?? '') > now))
770
- return false;
771
- if (this.policyFor(snapshot, candidate).managed.budgets?.unknownCost === 'deny' && accounting.reservation.costUsd === null)
772
- return deny('unknown cost is denied by policy');
773
- const quotaScope = candidate.managed?.cli && snapshot.hostBinding
774
- ? this.policyFor(snapshot, candidate).managed.upstream[candidate.managed.cli].quotaScope : candidate.managed?.quotaScope;
775
- const quotas = quotaScope ? [`quota/${encodeURIComponent(quotaScope)}`]
776
- : Object.keys(snapshot.cooldowns ?? {});
777
- if (quotas.some(key => Date.parse(snapshot.cooldowns?.[key]?.until ?? '') > now))
778
- return false;
779
- return true;
741
+ return false;
780
742
  }
781
743
  tryPreempt(snapshot, candidate, now) {
782
744
  const victims = Object.values(snapshot.records)
@@ -800,31 +762,7 @@ export class SharedHostScheduler {
800
762
  victim.revision += 1;
801
763
  }
802
764
  hasCapacity(snapshot, candidate) {
803
- const policy = this.policyFor(snapshot, candidate);
804
- const active = Object.values(snapshot.records).filter(record => record.state === 'admitted' || (policy.managed && record.state === 'uncertain'));
805
- const slots = policy.managed ? candidate.reservedSlots ?? 1 : 1;
806
- if (active.reduce((sum, record) => sum + (policy.managed ? record.reservedSlots ?? 1 : 1), 0) + slots > policy.maxConcurrent)
807
- return false;
808
- if (policy.managed) {
809
- for (const key of candidate.dimensions ?? []) {
810
- const cap = policy.managed.dimensionCaps[key];
811
- if (cap === undefined)
812
- continue;
813
- const used = active.filter(record => !record.dimensions || record.dimensions.includes(key))
814
- .reduce((sum, record) => sum + (record.reservedSlots ?? 1), 0);
815
- if (used + slots > cap)
816
- return false;
817
- }
818
- }
819
- if (!belowQuota(active, 'environment', candidate.environment, policy.environmentQuotas, slots, !!policy.managed))
820
- return false;
821
- if (!belowQuota(active, 'provider', candidate.provider, policy.providerQuotas, slots, !!policy.managed))
822
- return false;
823
- const runtimeQuotas = {
824
- host: policy.defaultHostQuota ?? 1,
825
- ...policy.runtimeQuotas,
826
- };
827
- return belowQuota(active, 'runtimeKind', candidate.runtimeKind, runtimeQuotas, slots, !!policy.managed);
765
+ return !capacityBlock(snapshot, candidate, this.policyFor(snapshot, candidate));
828
766
  }
829
767
  effectivePriority(record, now) {
830
768
  const waited = Math.max(0, now - Date.parse(record.submittedAt));
@@ -927,6 +865,7 @@ export class SharedHostScheduler {
927
865
  }
928
866
  }
929
867
  assertSubmissionsOpen(snapshot) {
868
+ this.checkActivation?.();
930
869
  if (snapshot.migration || snapshot.authorityRedirect)
931
870
  throw new Error('admission authority is frozen for migration');
932
871
  }
@@ -1028,6 +967,7 @@ export function hostProjectAdmissionPolicy(snapshot, projectId) {
1028
967
  return {
1029
968
  ...structuredClone(host.policy),
1030
969
  leaseTtlMs: Math.min(host.policy.leaseTtlMs, project.policy.leaseTtlMs),
970
+ starvationThresholdMs: Math.min(host.policy.starvationThresholdMs ?? 120_000, project.policy.starvationThresholdMs ?? 120_000),
1031
971
  managed: {
1032
972
  ...structuredClone(managed), authorityRef: host.authorityRef, hostPolicyRevision: host.revision,
1033
973
  authorityGeneration: generation, maxQueued: Math.min(host.maxQueued, managed.maxQueued),
@@ -1271,18 +1211,9 @@ function isCanonicalManagedDimension(projectId, key) {
1271
1211
  return false;
1272
1212
  }
1273
1213
  function sameManagedRequest(record, request) {
1274
- const { state: _state, revision: _revision, reason: _reason, admittedAt: _admittedAt, leaseExpiresAt: _leaseExpiresAt, finishedAt: _finishedAt, preemptedBy: _preemptedBy, projectId: _project, attemptGeneration: _attempt, leaseGeneration: _lease, dimensions: _dimensions, reservedSlots: _slots, accounting: _accounting, launchClaimedAt: _launch, execution: _execution, health: _health, lastProgressAt: _progress, finalEvidence: _final, ...original } = record;
1214
+ const { state: _state, revision: _revision, reason: _reason, denial: _denial, admittedAt: _admittedAt, leaseExpiresAt: _leaseExpiresAt, finishedAt: _finishedAt, preemptedBy: _preemptedBy, projectId: _project, attemptGeneration: _attempt, leaseGeneration: _lease, dimensions: _dimensions, reservedSlots: _slots, accounting: _accounting, launchClaimedAt: _launch, execution: _execution, health: _health, lastProgressAt: _progress, finalEvidence: _final, ...original } = record;
1275
1215
  return canonical(original) === canonical(request);
1276
1216
  }
1277
- function belowQuota(active, field, value, quotas, slots = 1, managed = false) {
1278
- const quota = quotas?.[String(value)];
1279
- if (quota === undefined)
1280
- return true;
1281
- if (!managed)
1282
- return active.filter(record => record[field] === value).length < quota;
1283
- return active.filter(record => record[field] === value)
1284
- .reduce((sum, record) => sum + (record.reservedSlots ?? 1), 0) + slots <= quota;
1285
- }
1286
1217
  function sameRequest(record, request) {
1287
1218
  return record.orchestratorId === request.orchestratorId
1288
1219
  && record.environment === request.environment
@@ -1311,19 +1242,6 @@ function intersectBudgets(...budgets) {
1311
1242
  }
1312
1243
  return result;
1313
1244
  }
1314
- function chargedUsage(record, dimension) {
1315
- const accounting = record.accounting;
1316
- const usage = accounting.usage;
1317
- const measured = !usage || usage.quality === 'unknown' ? null : dimension === 'costUsd' ? usage.costUsd
1318
- : usage.inputTokens === null || usage.outputTokens === null || usage.cacheTokens === null || usage.reasoningTokens === null ? null
1319
- : usage.inputTokens + usage.outputTokens + usage.cacheTokens + usage.reasoningTokens;
1320
- const reservation = accounting.reservation[dimension];
1321
- // Uncertain execution can still accrue spend; estimates never refund a reserve.
1322
- if (record.state !== 'settled' || usage?.quality !== 'reported') {
1323
- return reservation === null || measured === null ? reservation : Math.max(reservation, measured);
1324
- }
1325
- return measured ?? reservation;
1326
- }
1327
1245
  function canRefineUsage(previous, next) {
1328
1246
  const rank = { unknown: 0, estimated: 1, reported: 2 };
1329
1247
  if (rank[next.quality] < rank[previous.quality])
@@ -0,0 +1,73 @@
1
+ // Project bounded group/run service diagnostics without changing admission decisions.
2
+ import { createHash } from 'node:crypto';
3
+ import { attemptBlock, attemptReadyAt, capacityBlock } from './shared-host-admission-checks.js';
4
+ const maximumFindings = 64;
5
+ const label = (value) => Buffer.byteLength(value, 'utf8') <= 256 ? value
6
+ : `${Buffer.from(value).subarray(0, 180).toString('utf8')}#${createHash('sha256').update(value).digest('hex')}`;
7
+ const run = (record) => record.allowance?.runId ?? record.orchestratorId;
8
+ const elapsed = (now, since) => Math.max(0, now - since);
9
+ export function projectAdmissionDiagnostics(snapshot, policyFor, now, projectId) {
10
+ const records = Object.values(snapshot.records);
11
+ const findings = new Map();
12
+ // Current waits take precedence over historical denials. Stable ties make
13
+ // truncation repeatable across independent readers of the durable snapshot.
14
+ const candidates = records.filter(record => record.managed && (!projectId || record.projectId === projectId)
15
+ && (record.state === 'queued' || (record.state === 'denied' && record.denial)))
16
+ .sort((a, b) => Number(a.state === 'denied') - Number(b.state === 'denied')
17
+ || (a.state === 'denied' ? Date.parse(b.finishedAt) - Date.parse(a.finishedAt) : Date.parse(a.submittedAt) - Date.parse(b.submittedAt))
18
+ || a.requestId.localeCompare(b.requestId));
19
+ for (const candidate of candidates) {
20
+ const submitted = Date.parse(candidate.submittedAt);
21
+ if (candidate.state === 'queued' && submitted + candidate.queueTimeoutMs <= now)
22
+ continue;
23
+ const policy = policyFor(candidate);
24
+ const thresholdMs = policy.starvationThresholdMs ?? 120_000;
25
+ const waitMs = elapsed(candidate.state === 'denied' ? Date.parse(candidate.finishedAt) : now, submitted);
26
+ if (candidate.state === 'queued' && waitMs < thresholdMs)
27
+ continue;
28
+ let block = candidate.denial;
29
+ if (candidate.state === 'queued') {
30
+ block = snapshot.migration || snapshot.authorityRedirect
31
+ ? { kind: 'policy-exclusion', cause: 'authority-frozen', terminal: false }
32
+ : !candidate.accounting
33
+ ? { kind: 'safety-blocked', cause: 'allowance-reconciliation-required', terminal: false }
34
+ : attemptBlock(snapshot, candidate, policy, now) ?? capacityBlock(snapshot, candidate, policy, now);
35
+ }
36
+ if (!block)
37
+ continue;
38
+ const scopes = [
39
+ ...(candidate.managed?.group ? [['group', candidate.managed.group]] : []), ['run', run(candidate)],
40
+ ];
41
+ for (const [scope, scopeId] of scopes) {
42
+ const sameScope = (record) => record.projectId === candidate.projectId
43
+ && (scope === 'group' ? record.managed?.group === scopeId : run(record) === scopeId);
44
+ const lastService = records.filter(sameScope).reduce((latest, record) => Math.max(latest, Date.parse(record.admittedAt ?? '') || 0), Math.max(submitted, attemptReadyAt(snapshot, candidate, policy)));
45
+ const serviceWaitMs = elapsed(now, lastService);
46
+ // Admissions from other projects also compete at host-wide dimensions;
47
+ // only counts, never other projects' identities, enter the public view.
48
+ const consumes = (record, dimension) => dimension === 'global'
49
+ || (dimension.startsWith('environment/') ? record.environment === candidate.environment
50
+ : dimension.startsWith('provider/') ? record.provider === candidate.provider
51
+ : dimension.startsWith('runtimeKind/') ? record.runtimeKind === candidate.runtimeKind
52
+ : !record.dimensions || record.dimensions.includes(dimension));
53
+ // Every independently binding cap must see competing service. A full
54
+ // global queue cannot mask a local group cap that never frees capacity.
55
+ const competingAdmissions = records.filter(record => record.admittedAt && !sameScope(record)
56
+ && Date.parse(record.admittedAt) > lastService && Date.parse(record.admittedAt) <= now
57
+ && (block.dimensions ?? (block.dimension ? [block.dimension] : [])).every(dimension => consumes(record, dimension))).length;
58
+ const kind = block.kind === 'capacity-wait' && serviceWaitMs >= thresholdMs && competingAdmissions >= 2
59
+ ? 'service-starvation' : block.kind;
60
+ const finding = {
61
+ kind, scope, scopeId: label(scopeId), projectId: candidate.projectId ?? policy.managed?.projectId,
62
+ requestId: label(candidate.requestId), group: candidate.managed?.group, runId: label(run(candidate)),
63
+ waitMs, serviceWaitMs, thresholdMs, blockingCause: block.cause,
64
+ ...(block.dimension ? { dimension: label(block.dimension) } : {}), competingAdmissions,
65
+ };
66
+ const key = JSON.stringify([finding.projectId, scope, scopeId, kind, block.cause, block.dimension]);
67
+ if (!findings.has(key))
68
+ findings.set(key, finding);
69
+ }
70
+ }
71
+ return { findings: [...findings.values()].slice(0, maximumFindings), truncated: Math.max(0, findings.size - maximumFindings) };
72
+ }
73
+ //# sourceMappingURL=shared-host-starvation.js.map
@@ -0,0 +1,4 @@
1
+ /** Pure validation shared by the typed renderer and installed execution owner. */
2
+ import type { WorkerResolvedInvocation } from '../mission-protocol/types.js';
3
+ export function validateWorkerArtifacts(invocation: Pick<WorkerResolvedInvocation,
4
+ 'artifacts' | 'invocationId' | 'cwd' | 'briefPath' | 'stdoutPath' | 'stderrPath' | 'finalPath'>): NonNullable<WorkerResolvedInvocation['artifacts']>;
@@ -0,0 +1,56 @@
1
+ /** Validate bounded artifact declarations without touching the worker filesystem. */
2
+ import path from 'node:path';
3
+
4
+ const safeId = /^[a-z0-9][a-z0-9-]{0,51}$/;
5
+ const safeName = /^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/;
6
+ const allowed = new Set(['name', 'content', 'location', 'prefix', 'relativePath']);
7
+
8
+ function normalizedRelative(value) {
9
+ return typeof value === 'string' && value.length > 0 && value.length <= 4096
10
+ && !/[\\\x00-\x1f\x7f]/.test(value) && !path.posix.isAbsolute(value)
11
+ && !value.split('/').some(part => !part || part === '.' || part === '..')
12
+ && path.posix.normalize(value) === value;
13
+ }
14
+
15
+ /** The installed module owns the prefix; its loader rechecks the whole declaration. */
16
+ export function validateWorkerArtifacts(invocation) {
17
+ const artifacts = invocation.artifacts ?? [];
18
+ if (!Array.isArray(artifacts) || artifacts.length > 16) throw new Error('Worker artifact count exceeds bound');
19
+ const names = new Set();
20
+ let total = 0, workspaceCount = 0;
21
+ for (const artifact of artifacts) {
22
+ if (!artifact || typeof artifact.name !== 'string' || !safeName.test(artifact.name)
23
+ || /[^A-Za-z0-9_-]/.test(artifact.name) || names.has(artifact.name)
24
+ || typeof artifact.content !== 'string' || Object.keys(artifact).some(key => !allowed.has(key))) {
25
+ throw new Error('Invalid worker artifact declaration');
26
+ }
27
+ names.add(artifact.name);
28
+ const size = Buffer.byteLength(artifact.content);
29
+ total += size;
30
+ if (size > 1048576 || total > 4194304) throw new Error('Worker artifact bytes exceed bound');
31
+ if (artifact.location === 'workspace-relative') {
32
+ if (++workspaceCount > 1) throw new Error('Only one workspace-relative worker artifact is allowed');
33
+ const { prefix, relativePath } = artifact;
34
+ const id = invocation.invocationId;
35
+ if (typeof id !== 'string' || !safeId.test(id) || /[^a-z0-9-]/.test(id)) throw new Error('Invalid worker artifact invocation id');
36
+ if (!normalizedRelative(prefix) || prefix.length > 256 || !prefix.endsWith('-')
37
+ || !normalizedRelative(relativePath) || !/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(path.posix.basename(relativePath))
38
+ || path.posix.dirname(relativePath) !== `${prefix}${id}`) {
39
+ throw new Error('Worker artifact path must be normalized and bound to its prefix and invocation id');
40
+ }
41
+ if (typeof invocation.cwd !== 'string' || !path.posix.isAbsolute(invocation.cwd)
42
+ || /[\\\x00-\x1f\x7f]/.test(invocation.cwd) || path.posix.normalize(invocation.cwd) !== invocation.cwd) {
43
+ throw new Error('Worker artifact requires a normalized absolute cwd');
44
+ }
45
+ const target = path.posix.resolve(invocation.cwd, relativePath);
46
+ if (!target.startsWith(`${invocation.cwd === '/' ? '' : invocation.cwd}/`)) throw new Error('Worker artifact escapes cwd');
47
+ if (['briefPath', 'stdoutPath', 'stderrPath', 'finalPath'].some(key => invocation[key] && path.resolve(invocation[key]) === target)) {
48
+ throw new Error('Worker artifact and input/output paths must be distinct');
49
+ }
50
+ } else if (artifact.location !== undefined && artifact.location !== 'run-directory'
51
+ || artifact.prefix !== undefined || artifact.relativePath !== undefined) {
52
+ throw new Error('Invalid worker artifact declaration');
53
+ }
54
+ }
55
+ return artifacts;
56
+ }
@@ -0,0 +1,68 @@
1
+ // Parses direct worker requests; configured role authority is validated by the resolver.
2
+ import { DEFAULT_WORKER_SELECTION, WorkerPolicyError } from '../models/provider-policy.js';
3
+ /** Only fixed argument guidance is safe to expose in public request diagnostics. */
4
+ export class WorkerRequestArgumentError extends WorkerPolicyError {
5
+ pointer;
6
+ constructor(key) {
7
+ super('OG_CONFIG_REFERENCE', key === 'task'
8
+ ? `Missing required --task. Valid task classes: ${Object.keys(DEFAULT_WORKER_SELECTION.taskClasses).join(', ')}, plus classes configured in defaults.workerSelection.taskClasses.`
9
+ : `--${key.replace('_', '-')} must be an absolute path; relative paths are not resolved against cwd or --workdir.`);
10
+ this.pointer = `/request/${key}`;
11
+ }
12
+ }
13
+ /** Reject unsupported syntax before discovery or any path reads. */
14
+ export function parseWorkerCommandArgs(args) {
15
+ const names = { role: 'role', task: 'task', group: 'group', brief: 'brief',
16
+ workdir: 'workdir', 'final-output': 'final_output', transport: 'transport', format: 'format' };
17
+ const result = {};
18
+ let json = false;
19
+ let premiumAuthorized = false;
20
+ for (let i = 0; i < args.length; i++) {
21
+ const token = args[i];
22
+ if (token === '--authorize-premium') {
23
+ if (premiumAuthorized)
24
+ throw new Error('Duplicate --authorize-premium');
25
+ premiumAuthorized = true;
26
+ continue;
27
+ }
28
+ if (token === '--json') {
29
+ if (json)
30
+ throw new Error('Duplicate --json');
31
+ json = true;
32
+ continue;
33
+ }
34
+ const match = /^--([^=]+)(?:=([\s\S]*))?$/.exec(token);
35
+ if (!match || !Object.hasOwn(names, match[1]))
36
+ throw new Error(`Unsupported models resolve argument: ${token}`);
37
+ const key = names[match[1]];
38
+ if (key in result)
39
+ throw new Error(`Duplicate --${match[1]}`);
40
+ const value = match[2] ?? args[++i];
41
+ if (!value || value.startsWith('--') && match[2] === undefined || value.includes('\0')) {
42
+ if (key === 'task' && !value?.includes('\0'))
43
+ throw new WorkerRequestArgumentError('task');
44
+ throw new Error(`--${match[1]} requires a value`);
45
+ }
46
+ result[key] = value;
47
+ }
48
+ if (json && result.format && result.format !== 'json')
49
+ throw new Error('--json conflicts with --format');
50
+ if (json)
51
+ result.format = 'json';
52
+ for (const key of ['role', 'task', 'group', 'brief', 'workdir', 'final_output']) {
53
+ if (key === 'task' && !result[key])
54
+ throw new WorkerRequestArgumentError('task');
55
+ if (!result[key])
56
+ throw new Error(`Missing ${key}`);
57
+ }
58
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(result.role))
59
+ throw new Error('Unsupported role');
60
+ if (!/^[a-z][a-z0-9-]{0,62}$/.test(result.group))
61
+ throw new Error('Invalid group');
62
+ if (result.transport && !['shell', 'mcp'].includes(result.transport))
63
+ throw new Error('Unsupported transport');
64
+ if (result.format && !['shell', 'json'].includes(result.format))
65
+ throw new Error('Unsupported renderer; use POSIX shell or JSON');
66
+ return { ...result, ...(premiumAuthorized ? { authority: { premiumAuthorized: true, maxAutoTier: 3 } } : {}) };
67
+ }
68
+ //# sourceMappingURL=worker-command-request.js.map