@ai-dossier/sched 0.33.0 → 0.34.0

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/README.md CHANGED
@@ -89,8 +89,8 @@ where every mechanical supervision decision is code, not remembered prose:
89
89
 
90
90
  (`strong` sets only `model`, so its command falls back to the default claude template.)
91
91
  What an engine is ACTUALLY running is always visible in two places, added in #680: the
92
- `Dispatch: …` line in `sched status` (and its `--json` `dispatch` field), and the
93
- `▶ sched dispatch: …` banner `sched start` prints once at startup — both show
92
+ `Dispatch (default): …` line in `sched status` (and its `--json` `dispatch` field), and the
93
+ `▶ sched dispatch (default): …` banner `sched start` prints once at startup — both show
94
94
  `tier=agent/model` resolved exactly as spawns resolve. Note the inverse trap: running
95
95
  the batch FROM opencode/GLM does not make it run ON opencode/GLM — without a
96
96
  `dispatch.tiers` override every tier dispatches the default claude template regardless
@@ -283,6 +283,30 @@ Config schema moves to 1.8.0 (#565): a new top-level `default_batch_priority` ke
283
283
  invalid value degrades the whole config file to built-in defaults, same contract as every
284
284
  other field.
285
285
 
286
+ Config schema moves to 1.9.0 (#707): `dispatch.dispatch_profiles` names complete
287
+ dispatch configurations for batch runs. A profile has the same `command`, `tier_models`,
288
+ `prompt`, and optional per-tier `tiers` overrides as `dispatch`; the batch records the
289
+ profile name at enqueue time, and every member, tail, report, and recovery agent resolves
290
+ through that profile. For example:
291
+
292
+ ```json
293
+ {
294
+ "dispatch": {
295
+ "dispatch_profiles": {
296
+ "claude": { "command": ["claude", "-p", "--model", "{model}"], "tier_models": { "mechanical": "haiku", "mid": "sonnet", "strong": "opus" } },
297
+ "glm": { "command": ["opencode", "run", "-m", "{model}", "--"], "tier_models": { "mechanical": "glm-flash", "mid": "glm", "strong": "glm-strong" } }
298
+ }
299
+ }
300
+ }
301
+ ```
302
+
303
+ Use `sched enqueue --mode slot --batch <id> --dispatch glm` to select a profile
304
+ explicitly. Without `--dispatch`, a new batch detects Claude Code from `CLAUDECODE` or a
305
+ configured agent binary in its parent process chain. Detection is convenience only: if
306
+ profiles exist and detection is inconclusive, enqueue fails and names the available
307
+ profiles rather than silently choosing the default. `--dispatch` is intentionally rejected
308
+ for full-cycle entries; no profiles means legacy dispatch behavior remains unchanged.
309
+
286
310
  Two engine-safety policies were explicit product decisions on #464:
287
311
 
288
312
  - **Pid identity is hybrid-verified (decision 1, option C).** Every spawn records the
@@ -1 +1 @@
1
- {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"batch-dispatch.d.ts","sourceRoot":"","sources":["../src/batch-dispatch.ts"],"names":[],"mappings":"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"}
@@ -1374,7 +1374,10 @@ function runValidate(deps, config, dispatch, batchId, now, result) {
1374
1374
  return;
1375
1375
  }
1376
1376
  const offender = outcome.offenders[0];
1377
- const { state: fixing, dispatch: fixDispatch } = (0, recovery_1.beginFixAttempt)(attributed, batchId, offender, rDeps, { config, tests: outcome.attributed.get(offender) ?? [] });
1377
+ const { state: fixing, dispatch: fixDispatch } = (0, recovery_1.beginFixAttempt)(attributed, batchId, offender, rDeps,
1378
+ // #707: `dispatch` is the batch's own (profile-resolved) dispatch — the
1379
+ // fix agent rides the same family the members ran on.
1380
+ { config, tests: outcome.attributed.get(offender) ?? [], dispatch });
1378
1381
  deps.store.withLock((s) => ({
1379
1382
  state: applyBatchAndIssues(s, fixing, batchId, []),
1380
1383
  result: undefined,
@@ -2791,6 +2794,67 @@ function teardownBatch(deps, batchId) {
2791
2794
  function runBatchTick(deps, config, dispatch) {
2792
2795
  const result = emptyResult();
2793
2796
  const now = deps.now();
2797
+ // #707: each batch dispatches through ITS OWN recorded profile, not the
2798
+ // engine's default. Resolution is cached per tick (per runBatchTick call):
2799
+ // within one pass a profile resolves at most once, and a failed resolution
2800
+ // reacts at most once. A profile that no longer resolves (config edited
2801
+ // mid-run) never silently falls back — the silent fallback is precisely
2802
+ // the #680 incident shape (a run that looked like one agent family,
2803
+ // executed as another). The reaction depends on how far the batch got:
2804
+ // - pre-merge (`dissolving` is a legal edge): DISSOLVE with reason
2805
+ // `dispatch-profile-missing:<name>` — loud, deterministic, one-shot, and
2806
+ // the members requeue as full-cycle units on the config default, which is
2807
+ // a coherent recovery rather than a mislabelled continuation.
2808
+ // - post-merge (PR parked/merged — the product already shipped): dissolving
2809
+ // would discard a landed PR's bookkeeping, so the remaining dispatch (the
2810
+ // report agent) runs on the default and journals
2811
+ // `dispatch-profile-missing` every tick it holds the decision open — a
2812
+ // bounded phase (deployed → reported), so the event cannot flood.
2813
+ const profileCache = new Map();
2814
+ const dispatchFor = (batch) => {
2815
+ if (batch.dispatch_profile === null)
2816
+ return dispatch;
2817
+ const cached = profileCache.get(batch.dispatch_profile);
2818
+ if (cached !== undefined)
2819
+ return cached;
2820
+ let resolved;
2821
+ try {
2822
+ resolved = (0, dispatch_1.resolveProfiledDispatch)(config, batch.dispatch_profile);
2823
+ }
2824
+ catch (err) {
2825
+ if (!(err instanceof dispatch_1.DispatchProfileError))
2826
+ throw err;
2827
+ const current = deps.store.load();
2828
+ const fresh = (0, state_1.findBatch)(current, batch.id);
2829
+ const dissolvable = fresh !== undefined && (0, state_1.allowedBatchTransitions)(fresh.status).includes('dissolving');
2830
+ if (dissolvable) {
2831
+ const liveSlot = slotFor(current, batch.id);
2832
+ if (liveSlot !== undefined &&
2833
+ liveSlot.pid !== null &&
2834
+ deps.spawnDeps.isAlive(liveSlot.pid, liveSlot.pid_start ?? undefined)) {
2835
+ deps.spawnDeps.kill(liveSlot.pid, liveSlot.pid_start ?? undefined);
2836
+ }
2837
+ const outcome = (0, recovery_1.dissolveBatch)(current, batch.id, { strategy: 'full', reason: `dispatch-profile-missing:${batch.dispatch_profile}` }, recoveryDeps(deps, config, fresh, now));
2838
+ deps.store.withLock((s) => ({
2839
+ state: applyBatchAndIssues(s, outcome.state, batch.id, outcome.requeued),
2840
+ result: undefined,
2841
+ }));
2842
+ result.failed.push(unit(batch.id));
2843
+ // Resolution failed: skip this batch for the rest of the pass with the
2844
+ // base dispatch as a harmless placeholder — the batch is dissolved, so
2845
+ // no further arm can claim a slot for it this tick.
2846
+ resolved = dispatch;
2847
+ }
2848
+ else {
2849
+ journalEvent(deps, 'dispatch-profile-missing', unit(batch.id), {
2850
+ detail: `profile '${batch.dispatch_profile}' is no longer configured; the post-merge tail runs on the default dispatch`,
2851
+ });
2852
+ resolved = dispatch;
2853
+ }
2854
+ }
2855
+ profileCache.set(batch.dispatch_profile, resolved);
2856
+ return resolved;
2857
+ };
2794
2858
  // #565: when more ready batches exist than free capacity, claim them in
2795
2859
  // the same priority order `runnableUnits` would (desc priority → asc
2796
2860
  // readiness age → anchor) — this loop never goes through
@@ -2822,10 +2886,15 @@ function runBatchTick(deps, config, dispatch) {
2822
2886
  const state = deps.store.load();
2823
2887
  const batch = (0, state_1.findBatch)(state, batchId);
2824
2888
  if (batch && batch.status === 'ready' && slotFor(state, batchId) === undefined) {
2825
- claimAndSetup(deps, config, dispatch, batchId, now, result);
2889
+ // #707: batch-setup dispatches through the batch's own profile too — it
2890
+ // is the batch's first agent, not an engine-internal step.
2891
+ claimAndSetup(deps, config, dispatchFor(batch), batchId, now, result);
2826
2892
  }
2827
2893
  }
2828
2894
  for (const batch of deps.store.load().batches) {
2895
+ // #707: resolve this batch's profile once per pass — or dissolve/fall
2896
+ // back loudly when it no longer resolves (see `dispatchFor` above).
2897
+ const batchDispatch = dispatchFor(batch);
2829
2898
  const slot = slotFor(deps.store.load(), batch.id);
2830
2899
  // #609: a TERMINAL batch (`done`/`dissolved`) still holding a slot matches
2831
2900
  // none of the status arms below and would fall through their `continue`,
@@ -2850,7 +2919,7 @@ function runBatchTick(deps, config, dispatch) {
2850
2919
  }
2851
2920
  if (slot && (slot.status === 'running' || slot.status === 'assigned')) {
2852
2921
  if (batch.status === 'executing') {
2853
- reconcileMemberSlot(deps, config, dispatch, batch.id, slot, now, result);
2922
+ reconcileMemberSlot(deps, config, batchDispatch, batch.id, slot, now, result);
2854
2923
  }
2855
2924
  else if (batch.status === 'fixing') {
2856
2925
  reconcileFixSlot(deps, config, batch.id, slot, now, result);
@@ -2867,7 +2936,7 @@ function runBatchTick(deps, config, dispatch) {
2867
2936
  continue; // live but neither running/assigned (e.g. mid-verify) — next tick
2868
2937
  if (batch.status === 'validating') {
2869
2938
  // A local suite run, not a provider dispatch — unaffected by `paused`.
2870
- runValidate(deps, config, dispatch, batch.id, now, result);
2939
+ runValidate(deps, config, batchDispatch, batch.id, now, result);
2871
2940
  continue;
2872
2941
  }
2873
2942
  // #629: every branch below spawns a provider agent — hold the wedge
@@ -2876,7 +2945,7 @@ function runBatchTick(deps, config, dispatch) {
2876
2945
  if (paused)
2877
2946
  continue;
2878
2947
  if (batch.status === 'deployed') {
2879
- spawnReportAgent(deps, config, dispatch, batch.id, now, result);
2948
+ spawnReportAgent(deps, config, batchDispatch, batch.id, now, result);
2880
2949
  }
2881
2950
  else if (batch.status === 'executing') {
2882
2951
  // A prior spawn threw, or `claimAndSpawn` found zero free capacity —
@@ -2887,11 +2956,11 @@ function runBatchTick(deps, config, dispatch) {
2887
2956
  // pre-checks BEFORE any pool/git work, so a full scheduler (or stale
2888
2957
  // member context) exits here without exec'ing, and `claimAndSpawn`
2889
2958
  // remains the authoritative capacity gate.
2890
- spawnMemberContinuation(deps, config, dispatch, batch.id, now, result);
2959
+ spawnMemberContinuation(deps, config, batchDispatch, batch.id, now, result);
2891
2960
  }
2892
2961
  else if (batch.status === 'reviewing' || batch.status === 'shipping') {
2893
2962
  // Same wedge, for a dead-or-never-claimed tail agent.
2894
- spawnTailAgent(deps, config, dispatch, batch.id, now, result);
2963
+ spawnTailAgent(deps, config, batchDispatch, batch.id, now, result);
2895
2964
  }
2896
2965
  else if (batch.status === 'fixing') {
2897
2966
  // `beginFixAttempt` already recorded this member's ONE attempt as