@coool/pi-adaptive 1.0.0 → 1.0.1

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/dist/index.js CHANGED
@@ -693,8 +693,13 @@ function updatePreferences(file, patch) {
693
693
  });
694
694
  }
695
695
  // ../core/src/state/receipts/bound.ts
696
+ /**
697
+ * Agent-written evidence can carry emoji; cutting between the halves of a
698
+ * surrogate pair would persist a lone surrogate that renders as garbage and
699
+ * throws in URI encoders, so the cut backs off to the pair boundary.
700
+ */
696
701
  function boundText(raw) {
697
- return raw.length > 128 ? raw.slice(0, 128) : raw;
702
+ return truncateBounded(raw, 128);
698
703
  }
699
704
  function boundRef(ref) {
700
705
  return {
@@ -804,8 +809,7 @@ function isRecord$2(value) {
804
809
  return typeof value === "object" && value !== null && !Array.isArray(value);
805
810
  }
806
811
  function boundString(raw) {
807
- if (typeof raw !== "string") return void 0;
808
- return raw.length > 128 ? raw.slice(0, 128) : raw;
812
+ return typeof raw === "string" ? boundText(raw) : void 0;
809
813
  }
810
814
  function isFiniteNonNegative(raw) {
811
815
  return typeof raw === "number" && Number.isFinite(raw) && raw >= 0;
@@ -1650,6 +1654,15 @@ function classifyFailure(evidence) {
1650
1654
  * refuses. Termination rests on the budget, which the caller must carry
1651
1655
  * across every unit of one task (each escalation continues on a new unit).
1652
1656
  */
1657
+ /**
1658
+ * Only a well-formed whole-number budget can bound the loop: NaN, infinite,
1659
+ * fractional, or negative counters would make `used >= max` unreachable or
1660
+ * leave a negative remainder, so they read as exhausted (finite is the
1661
+ * invariant, not the caller's good behavior).
1662
+ */
1663
+ function hasBudgetLeft({ used, max }) {
1664
+ return Number.isSafeInteger(used) && Number.isSafeInteger(max) && used >= 0 && used < max;
1665
+ }
1653
1666
  function escalate(input, target, step, tier) {
1654
1667
  return {
1655
1668
  kind: "escalate",
@@ -1669,7 +1682,7 @@ function planEscalation(input) {
1669
1682
  kind: "defer",
1670
1683
  reason: "escalation-not-justified"
1671
1684
  };
1672
- if (input.budget.max <= 0 || input.budget.used >= input.budget.max) return {
1685
+ if (!hasBudgetLeft(input.budget)) return {
1673
1686
  kind: "defer",
1674
1687
  reason: "escalation-budget-exhausted"
1675
1688
  };
@@ -2811,6 +2824,7 @@ var ResourceHealthRegistry = class {
2811
2824
  const records = resourceChain(key).map((chainKey) => this.#sweptRecord(chainKey, now));
2812
2825
  const hard = hardest(records, now);
2813
2826
  if (hard !== void 0) return blocked(hard.reason, hard.key, hard.retryAtMs);
2827
+ const due = [];
2814
2828
  for (const record of records) {
2815
2829
  if (record === void 0) continue;
2816
2830
  const expired = expireProbe(record.breaker, now, this.#config.breaker);
@@ -2821,14 +2835,20 @@ var ResourceHealthRegistry = class {
2821
2835
  };
2822
2836
  if (current !== record) this.#records.set(resourceKeyID(record.key), current);
2823
2837
  const admission = admitProbe(current.breaker, now);
2824
- if (admission.kind === "closed") continue;
2825
2838
  if (admission.kind === "waiting") return blocked("breaker-open", record.key, admission.retryAtMs);
2826
2839
  if (admission.kind === "in-flight") return blocked("probe-in-flight", record.key, void 0);
2840
+ if (admission.kind === "admitted") due.push({
2841
+ record,
2842
+ breaker: admission.record
2843
+ });
2844
+ }
2845
+ const first = due[0];
2846
+ if (first !== void 0) {
2827
2847
  const probeId = this.#newProbeId();
2828
- this.#records.set(resourceKeyID(record.key), {
2829
- ...record,
2848
+ this.#records.set(resourceKeyID(first.record.key), {
2849
+ ...first.record,
2830
2850
  breaker: {
2831
- ...admission.record,
2851
+ ...first.breaker,
2832
2852
  probeId,
2833
2853
  probeAt: now
2834
2854
  },
@@ -2838,7 +2858,7 @@ var ResourceHealthRegistry = class {
2838
2858
  kind: "probe",
2839
2859
  ticket: {
2840
2860
  id: probeId,
2841
- key: record.key
2861
+ key: first.record.key
2842
2862
  }
2843
2863
  };
2844
2864
  }
@@ -3611,7 +3631,7 @@ function planWake(targets, now, fruitless) {
3611
3631
  * waiting for a stated reset is free, only resumed work that failed again
3612
3632
  * counts.
3613
3633
  */
3614
- const MAX_WAKES = 5;
3634
+ const MAX_WAKES = 96;
3615
3635
  const PARK_ENTRY = "adaptive.parked";
3616
3636
  var WaitCancelled = class extends Error {
3617
3637
  constructor() {
@@ -3687,14 +3707,18 @@ function claimPark(file, record, now, manual = false) {
3687
3707
  };
3688
3708
  });
3689
3709
  }
3710
+ /** Drops the session's records; true when one was still waiting (not yet claimed). */
3690
3711
  function cancelPark(file, session) {
3691
- return file.update((value) => ({
3692
- next: {
3693
- version: 1,
3694
- records: records(value).filter((row) => row.session !== session)
3695
- },
3696
- result: void 0
3697
- }));
3712
+ return file.update((value) => {
3713
+ const rows = records(value);
3714
+ return {
3715
+ next: {
3716
+ version: 1,
3717
+ records: rows.filter((row) => row.session !== session)
3718
+ },
3719
+ result: rows.some((row) => row.session === session && !row.claimed)
3720
+ };
3721
+ });
3698
3722
  }
3699
3723
  /** Abortable ticks, shared by countdowns and long owned headless waits. */
3700
3724
  async function waitUntil(sources, due, signal, tick) {
@@ -4018,7 +4042,10 @@ function virtualModels(sources, activeTools, parking, dispatched, feedback) {
4018
4042
  }
4019
4043
  selectedRef = fallback;
4020
4044
  } else {
4021
- if (decision.kind !== "apply") throw new Error(`Adaptive unavailable: ${decision.reason}. Check your tier settings.`);
4045
+ if (decision.kind !== "apply") {
4046
+ const undeclared = decision.reason === "no-compatible-resource" && decision.detail?.includes("tool-use-unsupported") === true;
4047
+ throw new Error(undeclared ? `Adaptive unavailable: ${decision.reason}. Run /adaptive tools to declare which of your models call tools.` : `Adaptive unavailable: ${decision.reason}. Check your tier settings.`);
4048
+ }
4022
4049
  selectedRef = decision.model;
4023
4050
  }
4024
4051
  const selected = prepared.models.find((model) => model.provider === selectedRef.providerID && model.id === selectedRef.id);
@@ -4298,11 +4325,12 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
4298
4325
  disarm?.();
4299
4326
  disarm = void 0;
4300
4327
  };
4328
+ /** True when a still-waiting record was dropped. */
4301
4329
  const cancel = async (ctx) => {
4302
4330
  stop();
4303
4331
  clearParked(ctx);
4304
4332
  const session = ctx.sessionManager.getSessionId?.();
4305
- if (session !== void 0 && ctx.sessionManager.getBranch().some((entry) => entry.customType === "adaptive.parked")) await cancelPark(sources.parking.file, session);
4333
+ return session !== void 0 && ctx.sessionManager.getBranch().some((entry) => entry.customType === "adaptive.parked") && cancelPark(sources.parking.file, session);
4306
4334
  };
4307
4335
  const noteResume = async (owned, tier) => {
4308
4336
  try {
@@ -4320,9 +4348,9 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
4320
4348
  const owned = generation;
4321
4349
  if (!await claimPark(sources.parking.file, record, now, manual)) {
4322
4350
  clearParked(ctx);
4323
- return;
4351
+ return false;
4324
4352
  }
4325
- if (owned !== generation) return;
4353
+ if (owned !== generation) return false;
4326
4354
  clearParked(ctx);
4327
4355
  if (saved.input !== void 0) {
4328
4356
  resumedInput = {
@@ -4351,6 +4379,7 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
4351
4379
  }
4352
4380
  }, { triggerTurn: true });
4353
4381
  await noteResume(owned, record.tier);
4382
+ return true;
4354
4383
  };
4355
4384
  const arm = (ctx, record, saved) => {
4356
4385
  showParked(ctx, record);
@@ -4517,12 +4546,9 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
4517
4546
  const session = ctx.sessionManager.getSessionId?.();
4518
4547
  const record = session === void 0 ? void 0 : await findPark(sources.parking.file, session);
4519
4548
  const saved = record === void 0 ? void 0 : parkedPayload(ctx, record.token);
4520
- if (record === void 0 || saved === void 0) {
4521
- ctx.ui?.notify("Nothing is parked in this session.", "info");
4522
- return;
4523
- }
4549
+ if (record === void 0 || saved === void 0) return "not-parked";
4524
4550
  stop();
4525
- await wake(ctx, record, saved, record.due, true);
4551
+ return await wake(ctx, record, saved, record.due, true) ? "resumed" : "not-parked";
4526
4552
  };
4527
4553
  const clearServed = async (task, ctx) => {
4528
4554
  try {
@@ -4544,7 +4570,7 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
4544
4570
  sessionStart,
4545
4571
  resume,
4546
4572
  clearServed,
4547
- cancel,
4573
+ cancel: async (ctx) => await cancel(ctx) ? "cancelled" : "not-parked",
4548
4574
  shutdown: stop,
4549
4575
  async recover(recovery, ctx) {
4550
4576
  const id = selectedVirtual(ctx.model);
@@ -4638,31 +4664,6 @@ function parkingHandlers(pi, sources, tracker = { last: void 0 }) {
4638
4664
  }
4639
4665
  };
4640
4666
  }
4641
- // ../core/src/flow/waiting.ts
4642
- const DAY_MS = 864e5;
4643
- const WEEK_DAYS = 6;
4644
- function startOfDay(at) {
4645
- const date = new Date(at);
4646
- return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
4647
- }
4648
- function clock(date) {
4649
- return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
4650
- }
4651
- /**
4652
- * Short human text for a parked session (ADP-024): "waiting · resumes 18:42". Weekly limits reset days away, so
4653
- * a later day gains its weekday, and beyond a week its date. Local fields, not the locale formatter, keep the
4654
- * text identical across hosts.
4655
- */
4656
- function waitingLabel(resumeAt, now) {
4657
- if (resumeAt === null) return "waiting";
4658
- if (resumeAt <= now) return "waiting · resuming";
4659
- const date = new Date(resumeAt);
4660
- const days = Math.round((startOfDay(resumeAt) - startOfDay(now)) / DAY_MS);
4661
- if (days === 0) return `waiting · resumes ${clock(date)}`;
4662
- const day = date.toDateString();
4663
- if (days <= WEEK_DAYS) return `waiting · resumes ${day.slice(0, 3)} ${clock(date)}`;
4664
- return `waiting · resumes ${day.slice(4, 7)} ${String(date.getDate())} ${clock(date)}`;
4665
- }
4666
4667
  // src/health-actions.ts
4667
4668
  function parseSharedHealth(raw) {
4668
4669
  if (raw === void 0) return [];
@@ -4712,76 +4713,86 @@ const PI_COPY = {
4712
4713
  receiptExplanation: (reason) => defer[reason],
4713
4714
  parkExplanation: (reason) => park[reason]
4714
4715
  };
4715
- // src/ui/doctor.ts
4716
- function parts(version) {
4717
- if (!/^\d+\.\d+\.\d+$/.test(version)) return void 0;
4718
- const [major, minor, patch] = version.split(".");
4719
- return [
4720
- Number(major),
4721
- Number(minor),
4722
- Number(patch)
4723
- ];
4716
+ // ../core/src/flow/status.ts
4717
+ /**
4718
+ * Status-view helpers (ADP-033): flatten the status snapshot into one page
4719
+ * showing every configured model with its state, grouped by tier in priority
4720
+ * order. Pure and deterministic; local wall-clock rendering is derived from
4721
+ * the snapshot's own timestamps.
4722
+ */
4723
+ /** Configured resources in tier order (quick → standard → advanced). */
4724
+ function healthEntries(snapshot) {
4725
+ const entries = [];
4726
+ for (const tier of TIERS) for (const status of snapshot.tiers[tier]) entries.push({
4727
+ tier,
4728
+ status
4729
+ });
4730
+ return entries;
4724
4731
  }
4725
- function compare(a, b) {
4726
- for (const index of [
4727
- 0,
4728
- 1,
4729
- 2
4730
- ]) {
4731
- const difference = a[index] - b[index];
4732
- if (difference !== 0) return difference;
4732
+ /** Local date and time: quota windows may reset days or weeks from now. */
4733
+ function clockTime(until) {
4734
+ const at = new Date(until);
4735
+ const hours = String(at.getHours()).padStart(2, "0");
4736
+ const minutes = String(at.getMinutes()).padStart(2, "0");
4737
+ const month = String(at.getMonth() + 1).padStart(2, "0");
4738
+ const day = String(at.getDate()).padStart(2, "0");
4739
+ return `${String(at.getFullYear())}-${month}-${day} ${hours}:${minutes}`;
4740
+ }
4741
+ function pinnedTo(ref, pin) {
4742
+ if (pin === null) return false;
4743
+ return pin.providerID === ref.providerID && pin.id === ref.id && pin.variant === ref.variant;
4744
+ }
4745
+ /** Everything after the health state: capacity usage, window, breaker, pin. */
4746
+ function stateSuffix(status, pin) {
4747
+ let suffix = "";
4748
+ if (status.windowed) suffix += status.windowUntil === null ? ", windowed (deadline unknown)" : `, windowed until ${clockTime(status.windowUntil)}`;
4749
+ if (status.usedPercent !== null) {
4750
+ suffix += `, used ${String(status.usedPercent)}%`;
4751
+ if (status.resetAt !== null) suffix += `, resets ${clockTime(status.resetAt)}`;
4733
4752
  }
4734
- return 0;
4753
+ if (status.breaker === "open") suffix += ", breaker open";
4754
+ else if (status.breaker === "half_open") suffix += ", probing";
4755
+ if (pinnedTo(status.ref, pin)) suffix += " (pinned)";
4756
+ return suffix;
4735
4757
  }
4736
- function versionChecks(version, minimum, testedUpTo) {
4737
- const actual = parts(version);
4738
- const floor = parts(minimum);
4739
- const tested = testedUpTo === void 0 ? void 0 : parts(testedUpTo);
4740
- if (actual === void 0 || floor === void 0) return [{
4741
- state: "warning",
4742
- message: `Pi ${version}: version support could not be checked`,
4743
- action: "Pick a normal model and report this version."
4744
- }];
4745
- if (actual[0] !== 1 || compare(actual, floor) < 0) return [{
4746
- state: "error",
4747
- message: `Pi ${version} is unsupported (requires ≥${minimum} <2)`,
4748
- action: "Install a supported Pi version or pick a normal model."
4749
- }];
4750
- const checks = [{
4751
- state: "ok",
4752
- message: `Pi ${version} (supported: ≥${minimum} <2)`
4753
- }];
4754
- if (tested === void 0) checks.push({
4755
- state: "warning",
4756
- message: "Packed-host tested-up-to evidence is not recorded",
4757
- action: "Run the complete Pi host matrix before release."
4758
- });
4759
- else if (compare(actual, tested) > 0) checks.push({
4760
- state: "warning",
4761
- message: `Pi ${version} is newer than last tested ${String(testedUpTo)}`,
4762
- action: "If routing misbehaves, pick a normal model and report it."
4763
- });
4764
- else checks.push({
4765
- state: "ok",
4766
- message: `Packed hosts tested up to ${String(testedUpTo)}`
4767
- });
4768
- return checks;
4758
+ /** The STATE column of the status table: window, breaker, pin markers. */
4759
+ function stateCell(status, pin) {
4760
+ return stateSuffix(status, pin).replace(/^[, ]+/, "") || "-";
4769
4761
  }
4770
- const LABELS = {
4771
- ok: "OK",
4772
- warning: "CHECK",
4773
- error: "FIX"
4774
- };
4775
- const SEVERITY = [
4776
- "error",
4777
- "warning",
4778
- "ok"
4779
- ];
4780
- /** Problems first: the first screen answers "is anything wrong, and what do I do". */
4781
- function doctorLines(checks) {
4782
- const count = (state) => checks.filter((check) => check.state === state).length;
4783
- return [count("error") + count("warning") === 0 ? `All ${String(checks.length)} checks OK` : `${String(count("error"))} to fix · ${String(count("warning"))} to check · ${String(count("ok"))} OK`, ...SEVERITY.flatMap((state) => checks.filter((check) => check.state === state)).flatMap((check) => [`${LABELS[check.state]} ${check.message}`, ...check.action === void 0 ? [] : [` ${check.action}`]])];
4762
+ /**
4763
+ * The whole inventory as a column-aligned table (header row first): models
4764
+ * grouped in tier order (quick → standard → advanced), one row per model,
4765
+ * columns padded to the widest cell so the grid reads at a glance. There is
4766
+ * no per-resource selection step: the table IS the view.
4767
+ */
4768
+ function statusTable(snapshot) {
4769
+ const header = [
4770
+ "MODEL",
4771
+ "TIER",
4772
+ "HEALTH",
4773
+ "STATE"
4774
+ ];
4775
+ const rows = healthEntries(snapshot).map(({ tier, status }) => [
4776
+ resourceLabel(status.ref),
4777
+ tier,
4778
+ status.health,
4779
+ stateCell(status, snapshot.pin)
4780
+ ]);
4781
+ const widths = header.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column].length)));
4782
+ const render = (cells) => cells.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd();
4783
+ return [render(header), ...rows.map(render)];
4784
4784
  }
4785
+ /** Narrow hosts keep health and capacity visible instead of clipping wide model columns. */
4786
+ function compactStatus(snapshot) {
4787
+ return healthEntries(snapshot).flatMap(({ tier, status }) => [
4788
+ resourceLabel(status.ref),
4789
+ `${tier} · ${status.health}`,
4790
+ ...stateCell(status, snapshot.pin).split(", ").map((part) => ` ${part}`),
4791
+ ""
4792
+ ]);
4793
+ }
4794
+ /** Empty-state copy for the status view when nothing is configured. */
4795
+ const STATUS_EMPTY_MESSAGE = "No models configured yet — open Models to add some.";
4785
4796
  // src/capabilities.ts
4786
4797
  function object(value) {
4787
4798
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -4829,6 +4840,22 @@ function parseToolCapabilities(raw) {
4829
4840
  };
4830
4841
  });
4831
4842
  }
4843
+ function updateToolCapability(file, ref, tools) {
4844
+ return file.update((current) => {
4845
+ const entries = current === void 0 ? [] : parseToolCapabilities(current);
4846
+ const entry = {
4847
+ providerID: ref.providerID,
4848
+ id: ref.id,
4849
+ tools
4850
+ };
4851
+ const models = [...entries.filter((model) => model.providerID !== ref.providerID || model.id !== ref.id), entry];
4852
+ parseToolCapabilities({ models });
4853
+ return {
4854
+ next: { models },
4855
+ result: void 0
4856
+ };
4857
+ });
4858
+ }
4832
4859
  // src/host/actions.ts
4833
4860
  const DEFAULT_MODEL_HELP = "In Pi settings.json, set:\n{\n \"defaultProvider\": \"adaptive\",\n \"defaultModel\": \"auto\"\n}\nPi exposes no extension API to save this default. Your current model is unchanged.";
4834
4861
  async function preferencesAction(view, file) {
@@ -4862,103 +4889,266 @@ async function preferencesAction(view, file) {
4862
4889
  } else return;
4863
4890
  view.toast("Preferences saved", "success");
4864
4891
  }
4865
- async function healthAction(view, models, reset) {
4892
+ function declaredLabel(declared, ref) {
4893
+ const entry = declared.find((model) => model.providerID === ref.providerID && model.id === ref.id);
4894
+ if (entry === void 0) return "not declared · skipped on tool turns";
4895
+ return entry.tools ? "calls tools" : "no tool calls";
4896
+ }
4897
+ /**
4898
+ * Pi's catalog does not say which models call tools, and Adaptive never
4899
+ * guesses from a name: an undeclared model is skipped on every tool turn.
4900
+ */
4901
+ async function toolSupportAction(view, file, models, declared) {
4902
+ const unique = [...new Map(models.map((model) => [`${model.providerID}/${model.id}`, {
4903
+ providerID: model.providerID,
4904
+ id: model.id
4905
+ }]))];
4906
+ if (unique.length === 0) {
4907
+ await view.alert({
4908
+ title: "Tool support",
4909
+ message: STATUS_EMPTY_MESSAGE
4910
+ });
4911
+ return;
4912
+ }
4866
4913
  const ref = await view.select({
4867
- title: "Health · select a resource to reset",
4868
- options: models.map((model) => ({
4869
- title: resourceLabel(model),
4914
+ title: "Tool support · select a model",
4915
+ options: unique.map(([label, model]) => ({
4916
+ title: label,
4870
4917
  value: model,
4871
- description: "Mark healthy (also clears inherited provider limits)"
4918
+ description: declaredLabel(declared, model)
4872
4919
  }))
4873
4920
  });
4874
4921
  if (ref === void 0) return;
4922
+ const tools = await view.select({
4923
+ title: `${ref.providerID}/${ref.id}`,
4924
+ options: [{
4925
+ title: "Calls tools",
4926
+ value: true,
4927
+ description: "eligible for tool turns"
4928
+ }, {
4929
+ title: "No tool calls",
4930
+ value: false,
4931
+ description: "never routed to on tool turns"
4932
+ }]
4933
+ });
4934
+ if (tools === void 0) return;
4875
4935
  if (!await view.confirm({
4876
- title: "Reset resource health?",
4877
- message: `Reset ${resourceLabel(ref)} and its inherited provider limits? Other models on this provider may be affected. This does not restore actual provider quota.`
4936
+ title: "Confirm tool capability",
4937
+ message: `${ref.providerID}/${ref.id}: record tool support as ${tools ? "supported" : "unsupported"}? Only confirm this from provider documentation or a verified request.`
4878
4938
  })) return;
4879
- await reset(ref);
4880
- view.toast("Health reset", "success");
4939
+ await updateToolCapability(file, ref, tools);
4940
+ view.toast("Tool capability saved", "success");
4881
4941
  }
4882
- // ../core/src/flow/status.ts
4942
+ // ../core/src/flow/doctor.ts
4883
4943
  /**
4884
- * Status-view helpers (ADP-033): flatten the status snapshot into one page
4885
- * showing every configured model with its state, grouped by tier in priority
4886
- * order. Pure and deterministic; local wall-clock rendering is derived from
4887
- * the snapshot's own timestamps.
4944
+ * Doctor report (ADP-062): host checks plus the checks every host can derive
4945
+ * from the status snapshot, rendered problems-first so the first screen
4946
+ * answers "is anything wrong, and what do I do".
4888
4947
  */
4889
- /** Configured resources in tier order (quick → standard → advanced). */
4890
- function healthEntries(snapshot) {
4891
- const entries = [];
4892
- for (const tier of TIERS) for (const status of snapshot.tiers[tier]) entries.push({
4893
- tier,
4894
- status
4895
- });
4896
- return entries;
4948
+ function plural(count, noun) {
4949
+ return `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
4897
4950
  }
4898
- /** Local date and time: quota windows may reset days or weeks from now. */
4899
- function clockTime(until) {
4900
- const at = new Date(until);
4901
- const hours = String(at.getHours()).padStart(2, "0");
4902
- const minutes = String(at.getMinutes()).padStart(2, "0");
4903
- const month = String(at.getMonth() + 1).padStart(2, "0");
4904
- const day = String(at.getDate()).padStart(2, "0");
4905
- return `${String(at.getFullYear())}-${month}-${day} ${hours}:${minutes}`;
4951
+ /** Empty tiers, limited models and parked work, from one status snapshot. */
4952
+ function statusChecks(snapshot) {
4953
+ const empty = TIERS.filter((tier) => snapshot.tiers[tier].length === 0).map((tier) => ({
4954
+ state: "warning",
4955
+ message: `${tier}: no configured models`,
4956
+ action: "Open /adaptive tiers to add a model."
4957
+ }));
4958
+ const limited = new Set(healthEntries(snapshot).filter(({ status }) => status.health !== "healthy").map(({ status }) => resourceLabel(status.ref))).size;
4959
+ return [
4960
+ ...empty,
4961
+ limited === 0 ? {
4962
+ state: "ok",
4963
+ message: "Health: no models limited"
4964
+ } : {
4965
+ state: "ok",
4966
+ message: `Health: ${plural(limited, "model")} limited`,
4967
+ action: "Inspect /adaptive health; reset only after checking the provider."
4968
+ },
4969
+ snapshot.parked === 0 ? {
4970
+ state: "ok",
4971
+ message: "No parked work"
4972
+ } : {
4973
+ state: "ok",
4974
+ message: plural(snapshot.parked, "parked task"),
4975
+ action: "Use /adaptive resume or /adaptive cancel in the waiting session."
4976
+ }
4977
+ ];
4906
4978
  }
4907
- function pinnedTo(ref, pin) {
4908
- if (pin === null) return false;
4909
- return pin.providerID === ref.providerID && pin.id === ref.id && pin.variant === ref.variant;
4979
+ const LABELS = {
4980
+ ok: "OK",
4981
+ warning: "CHECK",
4982
+ error: "FIX"
4983
+ };
4984
+ const SEVERITY = [
4985
+ "error",
4986
+ "warning",
4987
+ "ok"
4988
+ ];
4989
+ /** Problems first: the first screen answers "is anything wrong, and what do I do". */
4990
+ function doctorLines(checks) {
4991
+ const count = (state) => checks.filter((check) => check.state === state).length;
4992
+ return [count("error") + count("warning") === 0 ? `All ${String(checks.length)} checks OK` : `${String(count("error"))} to fix · ${String(count("warning"))} to check · ${String(count("ok"))} OK`, ...SEVERITY.flatMap((state) => checks.filter((check) => check.state === state)).flatMap((check) => [`${LABELS[check.state]} ${check.message}`, ...check.action === void 0 ? [] : [` ${check.action}`]])];
4910
4993
  }
4911
- /** Everything after the health state: capacity usage, window, breaker, pin. */
4912
- function stateSuffix(status, pin) {
4913
- let suffix = "";
4914
- if (status.windowed) suffix += status.windowUntil === null ? ", windowed (deadline unknown)" : `, windowed until ${clockTime(status.windowUntil)}`;
4915
- if (status.usedPercent !== null) {
4916
- suffix += `, used ${String(status.usedPercent)}%`;
4917
- if (status.resetAt !== null) suffix += `, resets ${clockTime(status.resetAt)}`;
4994
+ // ../core/src/flow/waiting.ts
4995
+ const DAY_MS = 864e5;
4996
+ const WEEK_DAYS = 6;
4997
+ function startOfDay(at) {
4998
+ const date = new Date(at);
4999
+ return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
5000
+ }
5001
+ function clock(date) {
5002
+ return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
5003
+ }
5004
+ /**
5005
+ * Short human text for a parked session (ADP-024): "waiting · resumes 18:42". Weekly limits reset days away, so
5006
+ * a later day gains its weekday, and beyond a week its date. Local fields, not the locale formatter, keep the
5007
+ * text identical across hosts.
5008
+ */
5009
+ function waitingLabel(resumeAt, now) {
5010
+ if (resumeAt === null) return "waiting";
5011
+ if (resumeAt <= now) return "waiting · resuming";
5012
+ const date = new Date(resumeAt);
5013
+ const days = Math.round((startOfDay(resumeAt) - startOfDay(now)) / DAY_MS);
5014
+ if (days === 0) return `waiting · resumes ${clock(date)}`;
5015
+ const day = date.toDateString();
5016
+ if (days <= WEEK_DAYS) return `waiting · resumes ${day.slice(0, 3)} ${clock(date)}`;
5017
+ return `waiting · resumes ${day.slice(4, 7)} ${String(date.getDate())} ${clock(date)}`;
5018
+ }
5019
+ // ../core/src/flow/actions.ts
5020
+ const PARKED_TITLE = "Parked work";
5021
+ const NOTHING_PARKED = "Nothing is parked in this session.";
5022
+ const OPEN_SESSION = "Open the waiting session and run /adaptive there to resume or cancel it.";
5023
+ /** Pick a configured model, confirm, reset. Lists each model once, with its state. */
5024
+ async function runHealthReset(env, reset) {
5025
+ const snapshot = await env.api.status();
5026
+ const models = /* @__PURE__ */ new Map();
5027
+ for (const { status } of healthEntries(snapshot)) models.set(resourceLabel(status.ref), status);
5028
+ if (models.size === 0) {
5029
+ await env.view.alert({
5030
+ title: "Health",
5031
+ message: STATUS_EMPTY_MESSAGE
5032
+ });
5033
+ return;
5034
+ }
5035
+ const model = await env.view.select({
5036
+ title: "Health · select a model to reset",
5037
+ options: [...models].map(([label, status]) => {
5038
+ const state = stateCell(status, snapshot.pin);
5039
+ return {
5040
+ title: label,
5041
+ value: status.ref,
5042
+ description: state === "-" ? status.health : `${status.health} · ${state}`
5043
+ };
5044
+ })
5045
+ });
5046
+ if (model === void 0) return;
5047
+ const label = resourceLabel(model);
5048
+ if (await env.view.confirm({
5049
+ title: "Reset model health?",
5050
+ message: `Reset ${label} and its inherited provider limits? Other models on this provider may be affected. This does not restore actual provider quota.`
5051
+ }) !== true) return;
5052
+ await reset(model);
5053
+ env.view.toast("Health reset", "success");
5054
+ }
5055
+ /** Resume this session's parked work. True when the run continues and the menu should close. */
5056
+ async function resumeHere(env, resume) {
5057
+ if (env.sessionID === void 0) {
5058
+ await env.view.alert({
5059
+ title: PARKED_TITLE,
5060
+ message: OPEN_SESSION
5061
+ });
5062
+ return false;
5063
+ }
5064
+ switch (await resume(env.sessionID)) {
5065
+ case "resumed":
5066
+ env.view.toast("Resuming parked work", "success");
5067
+ return true;
5068
+ case "waiting":
5069
+ await env.view.alert({
5070
+ title: PARKED_TITLE,
5071
+ message: "No model in this tier is available yet. Adaptive keeps waiting and retries."
5072
+ });
5073
+ return false;
5074
+ case "not-parked":
5075
+ await env.view.alert({
5076
+ title: PARKED_TITLE,
5077
+ message: NOTHING_PARKED
5078
+ });
5079
+ return false;
4918
5080
  }
4919
- if (status.breaker === "open") suffix += ", breaker open";
4920
- else if (status.breaker === "half_open") suffix += ", probing";
4921
- if (pinnedTo(status.ref, pin)) suffix += " (pinned)";
4922
- return suffix;
4923
5081
  }
4924
- /** The STATE column of the status table: window, breaker, pin markers. */
4925
- function stateCell(status, pin) {
4926
- return stateSuffix(status, pin).replace(/^[, ]+/, "") || "-";
5082
+ /** Drop this session's parked work. */
5083
+ async function cancelHere(env, cancel) {
5084
+ if (env.sessionID === void 0) {
5085
+ await env.view.alert({
5086
+ title: PARKED_TITLE,
5087
+ message: OPEN_SESSION
5088
+ });
5089
+ return;
5090
+ }
5091
+ if (await cancel(env.sessionID) === "cancelled") env.view.toast("Parked work cancelled", "success");
5092
+ else await env.view.alert({
5093
+ title: PARKED_TITLE,
5094
+ message: NOTHING_PARKED
5095
+ });
4927
5096
  }
4928
5097
  /**
4929
- * The whole inventory as a column-aligned table (header row first): models
4930
- * grouped in tier order (quick → standard → advanced), one row per model,
4931
- * columns padded to the widest cell so the grid reads at a glance. There is
4932
- * no per-resource selection step: the table IS the view.
5098
+ * Waiting sessions, then what to do with this session's wait. Other sessions
5099
+ * are listed but act only from inside: a host can only continue the session
5100
+ * it runs in. True when a resumed run continues and the menu should close.
4933
5101
  */
4934
- function statusTable(snapshot) {
4935
- const header = [
4936
- "MODEL",
4937
- "TIER",
4938
- "HEALTH",
4939
- "STATE"
4940
- ];
4941
- const rows = healthEntries(snapshot).map(({ tier, status }) => [
4942
- resourceLabel(status.ref),
4943
- tier,
4944
- status.health,
4945
- stateCell(status, snapshot.pin)
4946
- ]);
4947
- const widths = header.map((cell, column) => Math.max(cell.length, ...rows.map((row) => row[column].length)));
4948
- const render = (cells) => cells.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd();
4949
- return [render(header), ...rows.map(render)];
5102
+ async function runParkedWork(env, actions) {
5103
+ const { waiting } = await env.api.status();
5104
+ if (waiting.length === 0) {
5105
+ await env.view.alert({
5106
+ title: PARKED_TITLE,
5107
+ message: "No parked work."
5108
+ });
5109
+ return false;
5110
+ }
5111
+ const now = (env.clock ?? Date.now)();
5112
+ const picked = await env.view.select({
5113
+ title: PARKED_TITLE,
5114
+ options: waiting.map((entry) => ({
5115
+ title: entry.sessionID === env.sessionID ? "This session" : entry.sessionID,
5116
+ value: entry.sessionID,
5117
+ description: `${entry.tier} · ${waitingLabel(entry.resumeAt, now)}`
5118
+ }))
5119
+ });
5120
+ if (picked === void 0) return false;
5121
+ if (picked !== env.sessionID) {
5122
+ await env.view.alert({
5123
+ title: PARKED_TITLE,
5124
+ message: OPEN_SESSION
5125
+ });
5126
+ return false;
5127
+ }
5128
+ const { resume, cancel } = actions;
5129
+ const run = await env.view.select({
5130
+ title: "This session's parked work",
5131
+ options: [...resume === void 0 ? [] : [{
5132
+ title: "Resume now",
5133
+ value: () => resumeHere(env, resume),
5134
+ description: "try the tier again"
5135
+ }], ...cancel === void 0 ? [] : [{
5136
+ title: "Cancel",
5137
+ value: async () => {
5138
+ await cancelHere(env, cancel);
5139
+ return false;
5140
+ },
5141
+ description: "drop the wait; your next prompt starts fresh"
5142
+ }]]
5143
+ });
5144
+ return run === void 0 ? false : run();
4950
5145
  }
4951
- /** Narrow hosts keep health and capacity visible instead of clipping wide model columns. */
4952
- function compactStatus(snapshot) {
4953
- return healthEntries(snapshot).flatMap(({ tier, status }) => [
4954
- resourceLabel(status.ref),
4955
- `${tier} · ${status.health}`,
4956
- ...stateCell(status, snapshot.pin).split(", ").map((part) => ` ${part}`),
4957
- ""
4958
- ]);
5146
+ async function runDoctor(env, doctor) {
5147
+ await env.view.table({
5148
+ title: "Adaptive doctor",
5149
+ lines: doctorLines(await doctor())
5150
+ });
4959
5151
  }
4960
- /** Empty-state copy for the status view when nothing is configured. */
4961
- const STATUS_EMPTY_MESSAGE = "No models configured yet — open Models to add some.";
4962
5152
  // ../core/src/flow/menu.ts
4963
5153
  /**
4964
5154
  * Main-menu and mode-menu view models for `/adaptive` (ADP-033): pure data
@@ -5293,6 +5483,46 @@ async function applyTierRequest(env, sessionID, tier, feedback) {
5293
5483
  }
5294
5484
  return result;
5295
5485
  }
5486
+ function actionOptions(env) {
5487
+ const actions = env.actions ?? {};
5488
+ const { resetHealth, resume, cancel, tools, doctor, preferences, defaults } = actions;
5489
+ const done = (run) => async () => {
5490
+ await run();
5491
+ return false;
5492
+ };
5493
+ return [
5494
+ resetHealth !== void 0 && {
5495
+ title: "Health",
5496
+ value: done(() => runHealthReset(env, resetHealth)),
5497
+ description: "reset a model marked limited"
5498
+ },
5499
+ (resume !== void 0 || cancel !== void 0) && {
5500
+ title: "Parked work",
5501
+ value: () => runParkedWork(env, actions),
5502
+ description: "resume or cancel waiting work"
5503
+ },
5504
+ tools !== void 0 && {
5505
+ title: "Tool support",
5506
+ value: done(tools),
5507
+ description: "declare which models can call tools"
5508
+ },
5509
+ doctor !== void 0 && {
5510
+ title: "Doctor",
5511
+ value: done(() => runDoctor(env, doctor)),
5512
+ description: "check setup and compatibility"
5513
+ },
5514
+ preferences !== void 0 && {
5515
+ title: "Preferences",
5516
+ value: done(preferences),
5517
+ description: "transcript notes and pinned-tier escalation"
5518
+ },
5519
+ defaults !== void 0 && {
5520
+ title: "Use Adaptive for new sessions",
5521
+ value: done(defaults),
5522
+ description: "Pi settings to copy"
5523
+ }
5524
+ ].filter((row) => row !== false);
5525
+ }
5296
5526
  async function runAdaptive(env, initial) {
5297
5527
  const loaded = await tryApi(env, () => env.api.settings());
5298
5528
  if (loaded === void 0) return;
@@ -5326,40 +5556,12 @@ async function runAdaptive(env, initial) {
5326
5556
  const tierPinned = sessionID === void 0 ? void 0 : await env.api.tierRequest(sessionID).catch(() => void 0);
5327
5557
  const choice = next ?? await env.view.select({
5328
5558
  title: `Adaptive (${override ?? settings.mode})`,
5329
- options: [...mainMenuOptions(settings, status, tierPinned, override ?? settings.mode), ...env.actions === void 0 ? [] : [
5330
- {
5331
- title: "Health",
5332
- value: "health",
5333
- description: "inspect or reset resource health"
5334
- },
5335
- {
5336
- title: "Parked work",
5337
- value: "parked",
5338
- description: "resume or cancel waiting work"
5339
- },
5340
- {
5341
- title: "Doctor",
5342
- value: "doctor",
5343
- description: "check setup and compatibility"
5344
- },
5345
- {
5346
- title: "Preferences",
5347
- value: "preferences",
5348
- description: "transcript notes and pinned-tier escalation"
5349
- },
5350
- {
5351
- title: "Use Adaptive for new sessions",
5352
- value: "defaults",
5353
- description: "Pi settings to copy"
5354
- }
5355
- ]]
5559
+ options: [...mainMenuOptions(settings, status, tierPinned, override ?? settings.mode), ...actionOptions(env)]
5356
5560
  });
5357
5561
  next = void 0;
5358
5562
  if (choice === void 0) return;
5359
- if (choice === "health" || choice === "parked" || choice === "doctor" || choice === "preferences" || choice === "defaults") {
5360
- await tryApi(env, async () => {
5361
- await env.actions?.[choice]();
5362
- });
5563
+ if (typeof choice === "function") {
5564
+ if (await tryApi(env, choice) === true) return;
5363
5565
  continue;
5364
5566
  }
5365
5567
  if (choice === "status") {
@@ -5675,11 +5877,17 @@ async function runReceiptsView(env) {
5675
5877
  });
5676
5878
  }
5677
5879
  }
5678
- // src/host/commands.ts
5679
- const SUBCOMMANDS = [
5880
+ // ../core/src/flow/command.ts
5881
+ /**
5882
+ * `/adaptive <subcommand>`, shared by every host (ADP-062): one parser, one
5883
+ * copy, so a subcommand typed in Pi behaves the same in OpenCode. Action-backed
5884
+ * subcommands exist only where the host wires the action.
5885
+ */
5886
+ const ADAPTIVE_SUBCOMMANDS = [
5680
5887
  "status",
5681
5888
  "why",
5682
5889
  "tiers",
5890
+ "tools",
5683
5891
  "health",
5684
5892
  "resume",
5685
5893
  "cancel",
@@ -5687,49 +5895,83 @@ const SUBCOMMANDS = [
5687
5895
  "observe off",
5688
5896
  "doctor"
5689
5897
  ];
5898
+ const REQUIRES = {
5899
+ tools: "tools",
5900
+ health: "resetHealth",
5901
+ resume: "resume",
5902
+ cancel: "cancel",
5903
+ doctor: "doctor"
5904
+ };
5905
+ /** The subcommands a host with these actions can run, in menu order. */
5906
+ function adaptiveSubcommands(actions) {
5907
+ return ADAPTIVE_SUBCOMMANDS.filter((command) => {
5908
+ const needs = REQUIRES[command];
5909
+ return needs === void 0 || actions?.[needs] !== void 0;
5910
+ });
5911
+ }
5912
+ /** Runs `/adaptive <input>`; an empty input opens the menu. API failures propagate to the host. */
5913
+ async function runAdaptiveCommand(env, input) {
5914
+ const command = input.trim().replace(/\s+/g, " ");
5915
+ const { resetHealth, resume, cancel, tools, doctor } = env.actions ?? {};
5916
+ switch (command) {
5917
+ case "":
5918
+ await runAdaptive(env);
5919
+ return;
5920
+ case "status":
5921
+ await runAdaptive(env, "status");
5922
+ return;
5923
+ case "tiers":
5924
+ await runAdaptive(env, "models");
5925
+ return;
5926
+ case "why": {
5927
+ const latest = (await env.api.receipts()).find((receipt) => receipt.sessionID === env.sessionID);
5928
+ await env.view.alert({
5929
+ title: "Latest decision",
5930
+ message: latest === void 0 ? "No decisions recorded in this session yet." : receiptDetail(latest, env.copy, (env.clock ?? Date.now)())
5931
+ });
5932
+ return;
5933
+ }
5934
+ case "observe on":
5935
+ case "observe off": {
5936
+ const result = await env.api.setSettings({ mode: command === "observe on" ? "observe" : "off" });
5937
+ env.view.toast(result.ok ? "Observation setting saved. Your selected model is unchanged." : result.errors.join("; "), result.ok ? "success" : "error");
5938
+ return;
5939
+ }
5940
+ case "tools":
5941
+ if (tools === void 0) break;
5942
+ await tools();
5943
+ return;
5944
+ case "health":
5945
+ if (resetHealth === void 0) break;
5946
+ await runHealthReset(env, resetHealth);
5947
+ return;
5948
+ case "resume":
5949
+ if (resume === void 0) break;
5950
+ await resumeHere(env, resume);
5951
+ return;
5952
+ case "cancel":
5953
+ if (cancel === void 0) break;
5954
+ await cancelHere(env, cancel);
5955
+ return;
5956
+ case "doctor":
5957
+ if (doctor === void 0) break;
5958
+ await runDoctor(env, doctor);
5959
+ return;
5960
+ }
5961
+ env.view.toast(`Unknown adaptive command. Use: ${adaptiveSubcommands(env.actions).join(", ")}.`, "error");
5962
+ }
5963
+ // src/host/commands.ts
5690
5964
  async function runCommand(args, ctx, options) {
5691
5965
  if (!ctx.hasUI) {
5692
5966
  ctx.ui.notify("Open /adaptive in an interactive or RPC session.", "warning");
5693
5967
  return;
5694
5968
  }
5695
- const command = args.trim().replace(/\s+/g, " ");
5696
- const env = await options.env(ctx);
5697
- if (command === "resume") {
5698
- await options.resume(ctx);
5699
- return;
5700
- }
5701
- if (command === "cancel") {
5702
- await options.cancel(ctx);
5703
- return;
5704
- }
5705
- if (command === "doctor" || command === "health") {
5706
- await env.actions?.[command]();
5707
- return;
5708
- }
5709
- if (command === "observe on" || command === "observe off") {
5710
- const result = await env.api.setSettings({ mode: command === "observe on" ? "observe" : "off" });
5711
- env.view.toast(result.ok ? "Observation setting saved. Your selected model is unchanged." : result.errors.join("; "), result.ok ? "success" : "error");
5712
- return;
5713
- }
5714
- if (command === "why") {
5715
- const latest = (await env.api.receipts()).find((receipt) => receipt.sessionID === env.sessionID);
5716
- await env.view.alert({
5717
- title: "Latest decision",
5718
- message: latest === void 0 ? "No decisions recorded in this session yet." : receiptDetail(latest, PI_COPY, options.clock())
5719
- });
5720
- return;
5721
- }
5722
- if (command === "" || command === "status" || command === "tiers") {
5723
- if (command === "") await runAdaptive(env, void 0);
5724
- else await runAdaptive(env, command === "tiers" ? "models" : "status");
5725
- return;
5726
- }
5727
- env.view.toast(`Unknown adaptive command. Use: ${SUBCOMMANDS.join(", ")}.`, "error");
5969
+ await runAdaptiveCommand(await options.env(ctx), args);
5728
5970
  }
5729
5971
  function registerCommands(pi, options) {
5730
5972
  pi.registerCommand("adaptive", {
5731
5973
  description: "Adaptive settings, decisions and diagnostics",
5732
- getArgumentCompletions: (prefix) => SUBCOMMANDS.filter((command) => command.startsWith(prefix)).map((command) => ({
5974
+ getArgumentCompletions: (prefix) => ADAPTIVE_SUBCOMMANDS.filter((command) => command.startsWith(prefix)).map((command) => ({
5733
5975
  value: command,
5734
5976
  label: command
5735
5977
  })),
@@ -5824,6 +6066,61 @@ function controlApi(options) {
5824
6066
  }
5825
6067
  };
5826
6068
  }
6069
+ // src/ui/doctor.ts
6070
+ function parts(version) {
6071
+ if (!/^\d+\.\d+\.\d+$/.test(version)) return void 0;
6072
+ const [major, minor, patch] = version.split(".");
6073
+ return [
6074
+ Number(major),
6075
+ Number(minor),
6076
+ Number(patch)
6077
+ ];
6078
+ }
6079
+ function compare(a, b) {
6080
+ for (const index of [
6081
+ 0,
6082
+ 1,
6083
+ 2
6084
+ ]) {
6085
+ const difference = a[index] - b[index];
6086
+ if (difference !== 0) return difference;
6087
+ }
6088
+ return 0;
6089
+ }
6090
+ function versionChecks(version, minimum, testedUpTo) {
6091
+ const actual = parts(version);
6092
+ const floor = parts(minimum);
6093
+ const tested = testedUpTo === void 0 ? void 0 : parts(testedUpTo);
6094
+ if (actual === void 0 || floor === void 0) return [{
6095
+ state: "warning",
6096
+ message: `Pi ${version}: version support could not be checked`,
6097
+ action: "Pick a normal model and report this version."
6098
+ }];
6099
+ if (actual[0] !== 1 || compare(actual, floor) < 0) return [{
6100
+ state: "error",
6101
+ message: `Pi ${version} is unsupported (requires ≥${minimum} <2)`,
6102
+ action: "Install a supported Pi version or pick a normal model."
6103
+ }];
6104
+ const checks = [{
6105
+ state: "ok",
6106
+ message: `Pi ${version} (supported: ≥${minimum} <2)`
6107
+ }];
6108
+ if (tested === void 0) checks.push({
6109
+ state: "warning",
6110
+ message: "Packed-host tested-up-to evidence is not recorded",
6111
+ action: "Run the complete Pi host matrix before release."
6112
+ });
6113
+ else if (compare(actual, tested) > 0) checks.push({
6114
+ state: "warning",
6115
+ message: `Pi ${version} is newer than last tested ${String(testedUpTo)}`,
6116
+ action: "If routing misbehaves, pick a normal model and report it."
6117
+ });
6118
+ else checks.push({
6119
+ state: "ok",
6120
+ message: `Packed hosts tested up to ${String(testedUpTo)}`
6121
+ });
6122
+ return checks;
6123
+ }
5827
6124
  // src/host/doctor.ts
5828
6125
  /** Probe failures use fixed copy: upstream errors/configs may contain credentials. */
5829
6126
  async function doctorChecks(options) {
@@ -5842,27 +6139,20 @@ async function doctorChecks(options) {
5842
6139
  const capabilities = await options.capabilities();
5843
6140
  const credentialed = [];
5844
6141
  const undeclared = /* @__PURE__ */ new Set();
5845
- for (const tier of TIERS) {
5846
- if (settings.tiers[tier].length === 0) checks.push({
5847
- state: "warning",
5848
- message: `${tier}: no configured models`,
5849
- action: "Open /adaptive tiers to add a model."
6142
+ for (const tier of TIERS) for (const ref of settings.tiers[tier]) {
6143
+ const model = options.registry.find(ref.providerID, ref.id);
6144
+ if (model === void 0) checks.push({
6145
+ state: "error",
6146
+ message: `${resourceLabel(ref)} (${tier}): model missing`,
6147
+ action: "Refresh Pi's model catalog or remove this tier entry."
5850
6148
  });
5851
- for (const ref of settings.tiers[tier]) {
5852
- const model = options.registry.find(ref.providerID, ref.id);
5853
- if (model === void 0) checks.push({
5854
- state: "error",
5855
- message: `${resourceLabel(ref)} (${tier}): model missing`,
5856
- action: "Refresh Pi's model catalog or remove this tier entry."
5857
- });
5858
- else if (!options.registry.hasConfiguredAuth(model)) checks.push({
5859
- state: "error",
5860
- message: `${resourceLabel(ref)} (${tier}): no credentials configured`,
5861
- action: `Run /login ${ref.providerID}, or remove it from the tier.`
5862
- });
5863
- else credentialed.push(resourceLabel(ref));
5864
- if (!capabilities.some((entry) => entry.providerID === ref.providerID && entry.id === ref.id && entry.tools)) undeclared.add(resourceLabel(ref));
5865
- }
6149
+ else if (!options.registry.hasConfiguredAuth(model)) checks.push({
6150
+ state: "error",
6151
+ message: `${resourceLabel(ref)} (${tier}): no credentials configured`,
6152
+ action: `Run /login ${ref.providerID}, or remove it from the tier.`
6153
+ });
6154
+ else credentialed.push(resourceLabel(ref));
6155
+ if (!capabilities.some((entry) => entry.providerID === ref.providerID && entry.id === ref.id && entry.tools)) undeclared.add(resourceLabel(ref));
5866
6156
  }
5867
6157
  if (credentialed.length > 0) checks.push({
5868
6158
  state: "ok",
@@ -5871,7 +6161,7 @@ async function doctorChecks(options) {
5871
6161
  if (undeclared.size > 0) checks.push({
5872
6162
  state: "warning",
5873
6163
  message: `Tool support not declared: ${[...undeclared].join(", ")}`,
5874
- action: "List them in adaptive/capabilities.json; Adaptive never guesses from a name."
6164
+ action: "Open /adaptive tools to declare them; Adaptive never guesses from a name."
5875
6165
  });
5876
6166
  } catch {
5877
6167
  checks.push({
@@ -5909,31 +6199,12 @@ async function doctorChecks(options) {
5909
6199
  });
5910
6200
  }
5911
6201
  try {
5912
- const limited = await options.health();
5913
- checks.push({
5914
- state: "ok",
5915
- message: limited === 0 ? "Health: no resources limited" : `Health: ${String(limited)} ${limited === 1 ? "resource" : "resources"} limited`,
5916
- ...limited === 0 ? {} : { action: "Inspect /adaptive health; reset only after checking the provider." }
5917
- });
6202
+ checks.push(...statusChecks(await options.status()));
5918
6203
  } catch {
5919
6204
  checks.push({
5920
6205
  state: "error",
5921
- message: "Shared health could not be read",
5922
- action: "Inspect adaptive/health.json; do not delete it while another Pi process is running."
5923
- });
5924
- }
5925
- try {
5926
- const parked = await options.parked();
5927
- checks.push({
5928
- state: "ok",
5929
- message: parked === 0 ? "No parked work" : `${String(parked)} parked ${parked === 1 ? "task" : "tasks"}`,
5930
- ...parked === 0 ? {} : { action: "Use /adaptive resume or /adaptive cancel in the waiting session." }
5931
- });
5932
- } catch {
5933
- checks.push({
5934
- state: "error",
5935
- message: "Parked work could not be read",
5936
- action: "Inspect adaptive/parked.json before resuming; no task was dispatched."
6206
+ message: "Health or parked work could not be read",
6207
+ action: "Inspect adaptive/health.json and parked.json; do not delete them while another Pi process is running."
5937
6208
  });
5938
6209
  }
5939
6210
  return checks;
@@ -5955,6 +6226,9 @@ function order(left, right) {
5955
6226
  if (l !== r) return l < r ? -1 : 1;
5956
6227
  return Number(left.reasoning) - Number(right.reasoning) || left.contextWindow - right.contextWindow || Number(left.thinkingLevels.includes("high")) - Number(right.thinkingLevels.includes("high"));
5957
6228
  }
6229
+ /** A proposal is a starting point, not the catalog: a primary and a fallback per tier keep
6230
+ * the first-run decision small; the tier editor offers every configured model. */
6231
+ const PROPOSAL_TIER_CAP = 2;
5958
6232
  /** Settings bootstrap only: never consulted during routing, and never persists. */
5959
6233
  function proposeTiers(models) {
5960
6234
  const unique = /* @__PURE__ */ new Map();
@@ -5962,7 +6236,7 @@ function proposeTiers(models) {
5962
6236
  const ordered = [...unique.values()];
5963
6237
  const first = Math.ceil(ordered.length / 3);
5964
6238
  const second = Math.ceil(ordered.length * 2 / 3);
5965
- const refs = (group, advanced) => group.slice(0, 32).map((model) => ({
6239
+ const refs = (group, advanced) => group.slice(0, PROPOSAL_TIER_CAP).map((model) => ({
5966
6240
  providerID: model.ref.providerID,
5967
6241
  id: model.ref.id,
5968
6242
  ...advanced && model.reasoning && model.thinkingLevels.includes("high") ? { variant: "high" } : {}
@@ -6368,38 +6642,27 @@ function adaptiveSurface(pi, sources, host, parking, feedback) {
6368
6642
  view,
6369
6643
  models: () => modelSummaries(availablePhysicalModels(ctx.modelRegistry)),
6370
6644
  sessionID: ctx.sessionManager.getSessionId(),
6645
+ clock: sources.parking.clock,
6371
6646
  actions: {
6372
- health: async () => {
6373
- await healthAction(view, await configured(), (ref) => resetResourceHealth(sources.health.file, ref));
6374
- },
6375
- parked: async () => {
6376
- const { waiting } = await api.status();
6377
- const now = sources.parking.clock();
6378
- await view.alert({
6379
- title: "Parked work",
6380
- message: waiting.length === 0 ? "No parked work." : [...waiting.map((entry) => `${entry.sessionID === ctx.sessionManager.getSessionId() ? "This session" : entry.sessionID} · ${entry.tier} · ${waitingLabel(entry.resumeAt, now)}`), "Use /adaptive resume or /adaptive cancel in the waiting session."].join("\n")
6381
- });
6382
- },
6383
- doctor: async () => {
6384
- const checks = await doctorChecks({
6385
- version: host.version,
6386
- minimum: host.minimum,
6387
- testedUpTo: host.testedUpTo,
6388
- registry: ctx.modelRegistry,
6389
- settings: () => readSettings(host.settings),
6390
- capabilities: sources.capabilities,
6391
- storage: () => host.settings.update(() => ({
6392
- next: void 0,
6393
- result: void 0
6394
- })),
6395
- health: async () => (await readSharedHealth(sources.health.file)).filter((record) => record.health !== "healthy").length,
6396
- parked: async () => (await readParked(sources.parking.file)).length
6397
- });
6398
- await view.table({
6399
- title: "Adaptive doctor",
6400
- lines: doctorLines(checks)
6401
- });
6647
+ resetHealth: (ref) => resetResourceHealth(sources.health.file, ref),
6648
+ resume: () => parking.resume(ctx),
6649
+ cancel: () => parking.cancel(ctx),
6650
+ tools: async () => {
6651
+ await toolSupportAction(view, host.capabilities, await configured(), await sources.capabilities());
6402
6652
  },
6653
+ doctor: () => doctorChecks({
6654
+ version: host.version,
6655
+ minimum: host.minimum,
6656
+ testedUpTo: host.testedUpTo,
6657
+ registry: ctx.modelRegistry,
6658
+ settings: () => readSettings(host.settings),
6659
+ capabilities: sources.capabilities,
6660
+ storage: () => host.settings.update(() => ({
6661
+ next: void 0,
6662
+ result: void 0
6663
+ })),
6664
+ status: api.status
6665
+ }),
6403
6666
  preferences: () => preferencesAction(view, sources.preferences),
6404
6667
  defaults: () => view.alert({
6405
6668
  title: "Use Adaptive for new sessions",
@@ -6408,12 +6671,7 @@ function adaptiveSurface(pi, sources, host, parking, feedback) {
6408
6671
  }
6409
6672
  });
6410
6673
  };
6411
- registerCommands(pi, {
6412
- env: environment,
6413
- resume: (ctx) => parking.resume(ctx),
6414
- cancel: (ctx) => parking.cancel(ctx),
6415
- clock: sources.health.clock
6416
- });
6674
+ registerCommands(pi, { env: environment });
6417
6675
  return { async onboard(ctx) {
6418
6676
  const env = await environment(ctx);
6419
6677
  await runOnboarding({
@@ -6795,9 +7053,10 @@ async function acquire(lock, clock) {
6795
7053
  } catch (error) {
6796
7054
  if (errorCode(error) !== "EEXIST") throw error;
6797
7055
  }
7056
+ const seen = await readFile(lock, "utf8").catch(() => void 0);
6798
7057
  const held = await stat(lock).catch(() => void 0);
6799
- if (held !== void 0 && clock() - held.mtimeMs > STALE_LOCK_MS) {
6800
- await rm(lock, { force: true });
7058
+ if (seen !== void 0 && held !== void 0 && clock() - held.mtimeMs > STALE_LOCK_MS) {
7059
+ if (await readFile(lock, "utf8").catch(() => void 0) === seen) await rm(lock, { force: true });
6801
7060
  continue;
6802
7061
  }
6803
7062
  if (clock() >= deadline) throw new Error(`Adaptive state: ${lock} is held by another Pi process`);
@@ -6881,6 +7140,7 @@ function adaptive(pi) {
6881
7140
  capabilities: () => loadToolCapabilities(join(getAgentDir(), "adaptive", "capabilities.json"), (file) => readFile(file, "utf8"))
6882
7141
  }, {
6883
7142
  settings: jsonFile(path),
7143
+ capabilities: jsonFile(join(getAgentDir(), "adaptive", "capabilities.json")),
6884
7144
  version: VERSION,
6885
7145
  minimum,
6886
7146
  testedUpTo