arkaos 5.17.1 → 5.17.2
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/VERSION +1 -1
- package/arka/skills/costs/SKILL.md +3 -3
- package/config/cognition/schedules.yaml +2 -2
- package/core/harness/drift.py +21 -10
- package/core/harness/manager.py +25 -5
- package/core/runtime/claude_code.py +13 -1
- package/core/runtime/gateway/litellm_config.py +4 -3
- package/core/runtime/model_router.py +16 -10
- package/core/runtime/pricing.py +27 -5
- package/core/runtime/runtime_models.py +9 -7
- package/dashboard/app/pages/models.vue +1 -1
- package/installer/fallback-model.js +80 -29
- package/installer/index.js +7 -4
- package/installer/update.js +5 -2
- package/knowledge/skills-manifest.json +1 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
5.17.
|
|
1
|
+
5.17.2
|
|
@@ -70,10 +70,10 @@ Outside this skill's scope but worth surfacing alongside the summary:
|
|
|
70
70
|
| Lever | Where | Effect |
|
|
71
71
|
| --- | --- | --- |
|
|
72
72
|
| `ARKAOS_LLM_PROVIDER` | env | Switch between Anthropic, OpenAI, Gemini adapters per the multi-LLM router. |
|
|
73
|
-
| `fallbackModel` | `~/.claude/settings.json` (Claude Code 2.1.166+) | Up to three models tried in order when the primary is overloaded or unavailable. `npx arkaos install` / `update` seed `["claude-opus-5", "claude-sonnet-5"]` when the key is absent and never overwrite an operator chain; `/arka status` shows the configured chain. |
|
|
74
|
-
| Schedule `model:` / `fallback_models:` | `~/.arkaos/schedules.yaml` | A pinned `model` goes to `claude --model`; without one the Model Fabric `strategy` role becomes `ANTHROPIC_DEFAULT_MODEL` (Claude Code 2.1.236+). `fallback_models` goes to `--fallback-model` (default `claude-opus-5 → claude-sonnet-5`) so one overload or model 404 no longer ends a nightly cycle. |
|
|
73
|
+
| `fallbackModel` | `~/.claude/settings.json` (Claude Code 2.1.166+) | Up to three models tried in order when the primary is overloaded or unavailable. `npx arkaos install` / `update` seed `["claude-opus-5-5", "claude-sonnet-5"]` when the key is absent, upgrade a chain that is exactly a previous ArkaOS default, and never overwrite an operator chain; `/arka status` shows the configured chain. |
|
|
74
|
+
| Schedule `model:` / `fallback_models:` | `~/.arkaos/schedules.yaml` | A pinned `model` goes to `claude --model`; without one the Model Fabric `strategy` role becomes `ANTHROPIC_DEFAULT_MODEL` (Claude Code 2.1.236+). `fallback_models` goes to `--fallback-model` (default `claude-opus-5-5 → claude-sonnet-5`) so one overload or model 404 no longer ends a nightly cycle. |
|
|
75
75
|
| `ANTHROPIC_BEDROCK_SERVICE_TIER` | env (Claude Code 2.1.122+) | `default` / `flex` / `priority`. Only relevant when routing through AWS Bedrock — `flex` cuts cost ~50 % on non-urgent workloads at the price of higher tail latency. |
|
|
76
|
-
| Agent `model:` field | `departments/*/agents/*.yaml` | Per-agent override per the model-routing matrix in CLAUDE.md. Mechanical roles default to `sonnet` at low effort, C-suite to `opus` (Opus 5); the Quality Gate row below lifts Marta / Eduardo / Francisca to the frontier tier. |
|
|
76
|
+
| Agent `model:` field | `departments/*/agents/*.yaml` | Per-agent override per the model-routing matrix in CLAUDE.md. Mechanical roles default to `sonnet` at low effort, C-suite to `opus` (Opus 5.5); the Quality Gate row below lifts Marta / Eduardo / Francisca to the frontier tier. |
|
|
77
77
|
| Quality Gate model | constitution `quality_gate.model_policy` | Marta / Eduardo / Francisca run on the best model available (frontier tier) regardless of the cost ceiling — review quality is never the place to save tokens. |
|
|
78
78
|
|
|
79
79
|
## Non-negotiables
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
#
|
|
6
6
|
# Model pin and fallback chain (Runtime Sync PR3) — per schedule, optional:
|
|
7
7
|
# model: claude-sonnet-5 # pinned: goes to `claude --model`
|
|
8
|
-
# fallback_models: [claude-opus-5] # `--fallback-model a,b`; [] disables
|
|
8
|
+
# fallback_models: [claude-opus-5-5] # `--fallback-model a,b`; [] disables
|
|
9
9
|
# Without `model`, the Model Fabric `strategy` role (~/.arkaos/models.yaml)
|
|
10
10
|
# becomes ANTHROPIC_DEFAULT_MODEL for the run. Without `fallback_models`,
|
|
11
|
-
# the chain is claude-opus-5 → claude-sonnet-5. The runtime caps chains at
|
|
11
|
+
# the chain is claude-opus-5-5 → claude-sonnet-5. The runtime caps chains at
|
|
12
12
|
# three models after de-duplication, removes ids outside `availableModels`
|
|
13
13
|
# before the walk and fails over past an id it cannot reach (a retired
|
|
14
14
|
# model); the ArkaOS gateway plays no part in the chain.
|
package/core/harness/drift.py
CHANGED
|
@@ -462,25 +462,36 @@ def _check_worktree(report: DriftReport, settings: dict[str, Any]) -> None:
|
|
|
462
462
|
)
|
|
463
463
|
|
|
464
464
|
|
|
465
|
+
def _fallback_finding(status: DriftStatus, detail: str) -> DriftFinding:
|
|
466
|
+
return DriftFinding("settings:fallbackModel", status, "fallbackModel", detail)
|
|
467
|
+
|
|
468
|
+
|
|
465
469
|
def _check_fallback_model(report: DriftReport, settings: dict[str, Any]) -> None:
|
|
466
470
|
"""MISSING when the key is absent or JSON null (the seeder writes the
|
|
467
|
-
default in both cases);
|
|
468
|
-
|
|
469
|
-
|
|
471
|
+
default in both cases); DIVERGED when the chain is exactly a default
|
|
472
|
+
ArkaOS seeded in an earlier release (assert upgrades it); ADOPTED for
|
|
473
|
+
any other value that is not the default — an array, an empty array,
|
|
474
|
+
or the legacy string form."""
|
|
475
|
+
from core.runtime.claude_code import (
|
|
476
|
+
DEFAULT_FALLBACK_MODELS,
|
|
477
|
+
is_previous_fallback_default,
|
|
478
|
+
)
|
|
470
479
|
|
|
471
480
|
chain = settings.get("fallbackModel")
|
|
472
481
|
if chain is None:
|
|
473
482
|
report.findings.append(
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
483
|
+
_fallback_finding(DriftStatus.MISSING, "fallbackModel chain default not seeded")
|
|
484
|
+
)
|
|
485
|
+
elif is_previous_fallback_default(chain):
|
|
486
|
+
detail = (
|
|
487
|
+
f"previous ArkaOS default chain ({' → '.join(chain)}); "
|
|
488
|
+
f"`arka harness assert` upgrades it to {' → '.join(DEFAULT_FALLBACK_MODELS)}"
|
|
478
489
|
)
|
|
490
|
+
report.findings.append(_fallback_finding(DriftStatus.DIVERGED, detail))
|
|
479
491
|
elif chain != list(DEFAULT_FALLBACK_MODELS):
|
|
480
492
|
report.findings.append(
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
"operator-configured fallback chain; seed policy adopts it",
|
|
493
|
+
_fallback_finding(
|
|
494
|
+
DriftStatus.ADOPTED, "operator-configured fallback chain; seed policy adopts it"
|
|
484
495
|
)
|
|
485
496
|
)
|
|
486
497
|
|
package/core/harness/manager.py
CHANGED
|
@@ -41,7 +41,10 @@ from pathlib import Path
|
|
|
41
41
|
from core.harness import drift, json_store, paths
|
|
42
42
|
from core.harness.manifest import OwnershipManifest
|
|
43
43
|
from core.harness.spec import HookRegistration, spec_for
|
|
44
|
-
from core.runtime.claude_code import
|
|
44
|
+
from core.runtime.claude_code import (
|
|
45
|
+
DEFAULT_FALLBACK_MODELS,
|
|
46
|
+
is_previous_fallback_default,
|
|
47
|
+
)
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
@dataclass
|
|
@@ -49,8 +52,8 @@ class SurfaceAction:
|
|
|
49
52
|
"""One thing assert did (or deliberately did not do) to a surface."""
|
|
50
53
|
|
|
51
54
|
surface: str
|
|
52
|
-
# created | repaired | unrepaired | reseeded |
|
|
53
|
-
# | refused | noop
|
|
55
|
+
# created | repaired | unrepaired | reseeded | upgraded
|
|
56
|
+
# | adopted-skip | refused | noop
|
|
54
57
|
action: str
|
|
55
58
|
detail: str = ""
|
|
56
59
|
|
|
@@ -353,13 +356,23 @@ class ClaudeConfigManager:
|
|
|
353
356
|
def _assert_seed(
|
|
354
357
|
self, settings: dict, report: AssertReport, reseed: bool
|
|
355
358
|
) -> None:
|
|
356
|
-
for surface, key, default, is_ours in _seed_surfaces(self):
|
|
359
|
+
for surface, key, default, is_ours, is_previous in _seed_surfaces(self):
|
|
357
360
|
current = settings.get(key)
|
|
358
361
|
if current is None:
|
|
359
362
|
settings[key] = default
|
|
360
363
|
report.actions.append(SurfaceAction(surface, "created"))
|
|
361
364
|
elif is_ours(current):
|
|
362
365
|
report.actions.append(SurfaceAction(surface, "noop"))
|
|
366
|
+
elif is_previous(current):
|
|
367
|
+
# A chain ArkaOS itself seeded before is not an operator
|
|
368
|
+
# decision: move it to the current default.
|
|
369
|
+
settings[key] = default
|
|
370
|
+
report.actions.append(
|
|
371
|
+
SurfaceAction(
|
|
372
|
+
surface, "upgraded",
|
|
373
|
+
"previous ArkaOS default; moved to the current default",
|
|
374
|
+
)
|
|
375
|
+
)
|
|
363
376
|
elif reseed:
|
|
364
377
|
settings[key] = default
|
|
365
378
|
report.actions.append(SurfaceAction(surface, "reseeded"))
|
|
@@ -531,20 +544,27 @@ def _user_deny_extensions(home: Path | None) -> list[str]:
|
|
|
531
544
|
return [r for r in raw if isinstance(r, str) and r]
|
|
532
545
|
|
|
533
546
|
|
|
547
|
+
def _never_previous(_value: object) -> bool:
|
|
548
|
+
"""Surfaces with no history of ArkaOS-authored defaults to upgrade."""
|
|
549
|
+
return False
|
|
550
|
+
|
|
551
|
+
|
|
534
552
|
def _seed_surfaces(manager: ClaudeConfigManager):
|
|
535
553
|
statusline = _statusline_default(manager)
|
|
536
554
|
return (
|
|
537
555
|
(
|
|
538
556
|
"settings:statusLine", "statusLine", statusline,
|
|
539
|
-
drift._is_arkaos_statusline,
|
|
557
|
+
drift._is_arkaos_statusline, _never_previous,
|
|
540
558
|
),
|
|
541
559
|
(
|
|
542
560
|
"settings:worktree", "worktree", {"baseRef": "head"},
|
|
543
561
|
lambda v: isinstance(v, dict) and v.get("baseRef") == "head",
|
|
562
|
+
_never_previous,
|
|
544
563
|
),
|
|
545
564
|
(
|
|
546
565
|
"settings:fallbackModel", "fallbackModel", list(DEFAULT_FALLBACK_MODELS),
|
|
547
566
|
lambda v: v == list(DEFAULT_FALLBACK_MODELS),
|
|
567
|
+
is_previous_fallback_default,
|
|
548
568
|
),
|
|
549
569
|
)
|
|
550
570
|
|
|
@@ -108,7 +108,19 @@ _PROBE_TIMEOUT_SECONDS = 10
|
|
|
108
108
|
# model is overloaded or unavailable; the runtime caps chains at three after
|
|
109
109
|
# de-duplication. installer/fallback-model.js carries the same list —
|
|
110
110
|
# tests/python/test_scheduler_daemon.py pins the two to each other.
|
|
111
|
-
DEFAULT_FALLBACK_MODELS: tuple[str, ...] = ("claude-opus-5", "claude-sonnet-5")
|
|
111
|
+
DEFAULT_FALLBACK_MODELS: tuple[str, ...] = ("claude-opus-5-5", "claude-sonnet-5")
|
|
112
|
+
# Chains ArkaOS ITSELF seeded in earlier releases. A settings.json holding one
|
|
113
|
+
# of these byte for byte is not an operator decision, so the seeder and
|
|
114
|
+
# `arka harness assert` upgrade it to DEFAULT_FALLBACK_MODELS; any other
|
|
115
|
+
# present value is the operator's and stays. Append, never edit — the JS twin
|
|
116
|
+
# in installer/fallback-model.js is pinned to this list by the same test.
|
|
117
|
+
PREVIOUS_FALLBACK_DEFAULTS: tuple[tuple[str, ...], ...] = (("claude-opus-5", "claude-sonnet-5"),)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def is_previous_fallback_default(chain: object) -> bool:
|
|
121
|
+
"""True when ``chain`` is exactly a chain ArkaOS seeded in the past."""
|
|
122
|
+
return isinstance(chain, list) and tuple(chain) in PREVIOUS_FALLBACK_DEFAULTS
|
|
123
|
+
|
|
112
124
|
|
|
113
125
|
# Cache for the `claude --version` probe, keyed by the binary probed ("" =
|
|
114
126
|
# the PATH lookup). In-memory only: a disk cache would let one session
|
|
@@ -51,10 +51,11 @@ _SLOT_ROLE: dict[str, str] = {
|
|
|
51
51
|
|
|
52
52
|
# runtime-tier tokens -> real Anthropic model ids the proxy calls upstream.
|
|
53
53
|
# Real ids (claude-fable-5-1, claude-*) pass straight through.
|
|
54
|
-
# Runtime Sync 2026-09-03: `best` is Fable 5.1,
|
|
55
|
-
#
|
|
54
|
+
# Runtime Sync 2026-09-03 (+ Opus 5.5 sweep 2026-09-22): `best` is Fable 5.1,
|
|
55
|
+
# `opus` is Opus 5.5 (the id the Claude Code 2.1.280 alias resolves to), and
|
|
56
|
+
# the weakest lane (`haiku`/`fast`) is Sonnet 5 — ArkaOS never routes to Haiku.
|
|
56
57
|
_RUNTIME_TO_ANTHROPIC: dict[str, str] = {
|
|
57
|
-
"opus": "claude-opus-5",
|
|
58
|
+
"opus": "claude-opus-5-5",
|
|
58
59
|
"best": "claude-fable-5-1",
|
|
59
60
|
"fable": "claude-fable-5-1",
|
|
60
61
|
"sonnet": "claude-sonnet-5",
|
|
@@ -55,18 +55,23 @@ KNOWN_EFFORTS = ("low", "medium", "high", "max")
|
|
|
55
55
|
|
|
56
56
|
_ALIAS_NAMES = frozenset({"best", "default", "fast"})
|
|
57
57
|
|
|
58
|
-
# Runtime Sync 2026-09-03
|
|
59
|
-
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
# visible without
|
|
58
|
+
# Runtime Sync 2026-09-03 (Opus 4.x → Opus 5, Fable 5 → Fable 5.1, Haiku →
|
|
59
|
+
# Sonnet 5) and the Opus 5.5 sweep 2026-09-22 (Opus 5 → Opus 5.5) — ids that
|
|
60
|
+
# no longer exist, or lanes ArkaOS no longer routes to. Normalised when a
|
|
61
|
+
# role is RESOLVED; the operator's models.yaml is never rewritten. One
|
|
62
|
+
# stderr notice per process per id, so a stale pin is visible without
|
|
63
|
+
# becoming noise. Legacy ids map straight to the CURRENT lane — the lookup
|
|
64
|
+
# is a single dict read, not a transitive walk, so no value may be a key.
|
|
65
|
+
LEGACY_SYNC_DATE = "2026-09-22"
|
|
63
66
|
LEGACY_MODEL_IDS: dict[str, str] = {
|
|
64
67
|
"claude-fable-5": "claude-fable-5-1",
|
|
65
68
|
"claude-fable-5[1m]": "claude-fable-5-1[1m]",
|
|
66
|
-
"claude-opus-
|
|
67
|
-
"claude-opus-
|
|
68
|
-
"claude-opus-4-
|
|
69
|
-
"claude-opus-4-
|
|
69
|
+
"claude-opus-5": "claude-opus-5-5",
|
|
70
|
+
"claude-opus-5[1m]": "claude-opus-5-5[1m]",
|
|
71
|
+
"claude-opus-4-8": "claude-opus-5-5",
|
|
72
|
+
"claude-opus-4-8[1m]": "claude-opus-5-5[1m]",
|
|
73
|
+
"claude-opus-4-7": "claude-opus-5-5",
|
|
74
|
+
"claude-opus-4-6": "claude-opus-5-5",
|
|
70
75
|
"claude-haiku-4-5-20251001": "claude-sonnet-5",
|
|
71
76
|
"claude-haiku-4-5": "claude-sonnet-5",
|
|
72
77
|
"haiku": "sonnet",
|
|
@@ -89,7 +94,8 @@ def normalise_model_id(model: str, source: str = "models.yaml") -> str:
|
|
|
89
94
|
_LEGACY_NOTICED.add((source, model))
|
|
90
95
|
print(
|
|
91
96
|
f"[arka:warn] {source} pins legacy model {model!r}; resolving as "
|
|
92
|
-
f"{current!r} (
|
|
97
|
+
f"{current!r} (retired lane; last sweep {LEGACY_SYNC_DATE}). "
|
|
98
|
+
"Update the pin to silence this.",
|
|
93
99
|
file=sys.stderr,
|
|
94
100
|
)
|
|
95
101
|
return current
|
package/core/runtime/pricing.py
CHANGED
|
@@ -6,7 +6,8 @@ their public per-token pricing. It is used to compute an optional
|
|
|
6
6
|
any model-selection logic — that is explicitly forbidden by the
|
|
7
7
|
LLM-agnostic contract.
|
|
8
8
|
|
|
9
|
-
Snapshot 2026-09-03 (Runtime Sync PR-2)
|
|
9
|
+
Snapshot 2026-09-03 (Runtime Sync PR-2), Opus 5.5 rows added 2026-09-22:
|
|
10
|
+
values sourced from the public
|
|
10
11
|
Anthropic pricing page (https://platform.claude.com/docs/en/about-claude/pricing) and
|
|
11
12
|
OpenAI (https://openai.com/api/pricing). Refresh when model families
|
|
12
13
|
change. Unknown models return `None` from `estimate_cost_usd` and are
|
|
@@ -61,11 +62,32 @@ PRICING: dict[str, dict[str, float]] = {
|
|
|
61
62
|
"cache_read": 1.00,
|
|
62
63
|
"cache_write": 12.50,
|
|
63
64
|
},
|
|
64
|
-
# Snapshot 2026-
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
65
|
+
# Snapshot 2026-09-22 (Opus 5.5 sweep): $4 / $20 per MTok and cache
|
|
66
|
+
# reads at $0.20 (0.05x input) per the public pricing page; the Claude
|
|
67
|
+
# Code 2.1.280 alias `opus` resolves here, so this is the lane every
|
|
68
|
+
# `model: opus` agent runs on. cache_write is NOT published yet: 5.00
|
|
69
|
+
# is the standard 1.25x five-minute multiplier — the same assumption
|
|
70
|
+
# the Fable 5.1 row shipped with — replace it when the page lists it.
|
|
71
|
+
# 1M context is native; the [1m] alias prices the same.
|
|
72
|
+
"claude-opus-5-5": {
|
|
73
|
+
"input": 4.00,
|
|
74
|
+
"output": 20.00,
|
|
75
|
+
"cache_read": 0.20,
|
|
76
|
+
"cache_write": 5.00,
|
|
77
|
+
},
|
|
78
|
+
"claude-opus-5-5[1m]": {
|
|
79
|
+
"input": 4.00,
|
|
80
|
+
"output": 20.00,
|
|
81
|
+
"cache_read": 0.20,
|
|
82
|
+
"cache_write": 5.00,
|
|
83
|
+
},
|
|
84
|
+
# History — snapshot 2026-08-09 (Gate Economy PR-8): drop-in at Opus
|
|
85
|
+
# 4.8's rates per the official model catalog — 1M context is the
|
|
86
|
+
# default with NO long-context premium, so the [1m] alias prices the
|
|
87
|
+
# same. The row was missing while claude-opus-5 carried 28% of weekly
|
|
68
88
|
# input tokens, pricing them at $0.00 and blinding the CostGovernor.
|
|
89
|
+
# Retained so 2026-08/09 sessions stay priced; the live lane is
|
|
90
|
+
# claude-opus-5-5 above.
|
|
69
91
|
"claude-opus-5": {
|
|
70
92
|
"input": 5.00,
|
|
71
93
|
"output": 25.00,
|
|
@@ -3,14 +3,16 @@
|
|
|
3
3
|
The Model Fabric ``runtime`` provider shells out to the active CLI. This
|
|
4
4
|
module lists the concrete models that CLI accepts, with human labels and
|
|
5
5
|
tier, so the dashboard dropdown and ``/arka-fusion`` can offer real
|
|
6
|
-
choices (Fable 5.1, Opus 5, Sonnet 5) instead of only the abstract
|
|
6
|
+
choices (Fable 5.1, Opus 5.5, Sonnet 5) instead of only the abstract
|
|
7
7
|
best/default/fast aliases. Haiku is deliberately absent: the weakest
|
|
8
8
|
lane ArkaOS routes to is Sonnet 5 (operator decision, Runtime Sync 2026-09-03).
|
|
9
9
|
|
|
10
|
-
Model IDs verified against the Claude Code
|
|
11
|
-
and the model-config reference (2026-09). This is the single place
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
Model IDs verified against the Claude Code 2.1.280 alias table (2026-09-22:
|
|
11
|
+
`opus` → claude-opus-5-5) and the model-config reference (2026-09). This is the single place
|
|
12
|
+
the picker catalogue is hand-listed — update HERE when the runtime ships new
|
|
13
|
+
models; the dashboard and CLI read from this module. The ids also live in
|
|
14
|
+
pricing.py, model_router.py, claude_code.py, gateway/litellm_config.py and
|
|
15
|
+
installer/fallback-model.js (one sweep touches all six). Codex /
|
|
14
16
|
Gemini / Cursor models are left empty on purpose: we do not hardcode
|
|
15
17
|
another vendor's catalogue, and the UI falls back to a free-text field
|
|
16
18
|
so the user types the exact id their runtime accepts.
|
|
@@ -24,8 +26,8 @@ from __future__ import annotations
|
|
|
24
26
|
CLAUDE_CODE_MODELS: list[dict[str, str]] = [
|
|
25
27
|
{"value": "claude-fable-5-1", "label": "Fable 5.1", "tier": "frontier",
|
|
26
28
|
"note": "most capable — 1M context, cache reads $0.25/MTok"},
|
|
27
|
-
{"value": "opus", "label": "Opus 5", "tier": "frontier",
|
|
28
|
-
"note": "frontier"},
|
|
29
|
+
{"value": "opus", "label": "Opus 5.5", "tier": "frontier",
|
|
30
|
+
"note": "frontier — $4/$20 per MTok, cache reads $0.20"},
|
|
29
31
|
{"value": "sonnet", "label": "Sonnet 5", "tier": "balanced",
|
|
30
32
|
"note": "balanced speed/quality — also the mechanical lane"},
|
|
31
33
|
]
|
|
@@ -128,7 +128,7 @@ const targetOptions = computed(() => {
|
|
|
128
128
|
const o = overview.value
|
|
129
129
|
if (!o) return []
|
|
130
130
|
const options: { label: string, value: string }[] = []
|
|
131
|
-
// Concrete runtime models first (Fable 5.1, Opus 5, Sonnet 5) so the
|
|
131
|
+
// Concrete runtime models first (Fable 5.1, Opus 5.5, Sonnet 5) so the
|
|
132
132
|
// operator picks a real model, not just an abstract alias.
|
|
133
133
|
for (const m of o.runtime?.models ?? []) {
|
|
134
134
|
options.push({ label: `runtime / ${m.label} · ${m.note}`, value: `runtime/${m.value}` })
|
|
@@ -5,64 +5,115 @@
|
|
|
5
5
|
// unavailable (the runtime de-duplicates and caps the chain at three).
|
|
6
6
|
// Without it a single overload or model 404 ends the session — the
|
|
7
7
|
// scheduler's nightly cycles died exactly that way. ArkaOS seeds the
|
|
8
|
-
// chain Opus 5 → Sonnet 5, the lanes below Fable 5.1 in the Model Fabric.
|
|
8
|
+
// chain Opus 5.5 → Sonnet 5, the lanes below Fable 5.1 in the Model Fabric.
|
|
9
9
|
//
|
|
10
10
|
// Behaviour (same contract as worktree-baseref.js):
|
|
11
11
|
// - No-op when runtime is not Claude Code.
|
|
12
|
-
// -
|
|
13
|
-
// chain
|
|
14
|
-
//
|
|
12
|
+
// - Sets the value when the key is absent or JSON null, and UPGRADES a
|
|
13
|
+
// chain that is exactly a default ArkaOS itself seeded in an earlier
|
|
14
|
+
// release (PREVIOUS_FALLBACK_DEFAULTS). Any other operator chain — an
|
|
15
|
+
// array, an explicit empty array, or the legacy string form — is
|
|
16
|
+
// preserved byte for byte.
|
|
15
17
|
// - Atomic write via .tmp + rename.
|
|
16
18
|
// - Never raises — failures are non-fatal.
|
|
17
19
|
//
|
|
18
|
-
// core/runtime/claude_code.py
|
|
19
|
-
//
|
|
20
|
+
// core/runtime/claude_code.py carries the same DEFAULT_FALLBACK_MODELS and
|
|
21
|
+
// PREVIOUS_FALLBACK_DEFAULTS; tests/python/test_scheduler_daemon.py pins
|
|
22
|
+
// the two files to each other.
|
|
20
23
|
|
|
21
24
|
import { existsSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
22
25
|
import { homedir } from "node:os";
|
|
23
26
|
import { join } from "node:path";
|
|
24
27
|
|
|
25
28
|
|
|
26
|
-
export const DEFAULT_FALLBACK_MODELS = ["claude-opus-5", "claude-sonnet-5"];
|
|
29
|
+
export const DEFAULT_FALLBACK_MODELS = ["claude-opus-5-5", "claude-sonnet-5"];
|
|
30
|
+
// Chains ArkaOS itself seeded before — upgraded, not preserved. Append only;
|
|
31
|
+
// keep each literal on one line (the Python parity test JSON-parses it).
|
|
32
|
+
export const PREVIOUS_FALLBACK_DEFAULTS = [["claude-opus-5", "claude-sonnet-5"]];
|
|
27
33
|
|
|
34
|
+
function sameChain(a, b) {
|
|
35
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length
|
|
36
|
+
&& a.every((id, i) => id === b[i]);
|
|
37
|
+
}
|
|
28
38
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
const settingsPath = join(home, ".claude", "settings.json");
|
|
39
|
+
function isPreviousDefault(value) {
|
|
40
|
+
return PREVIOUS_FALLBACK_DEFAULTS.some((prev) => sameChain(prev, value));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
// The settings object, or the `skipped` reason the seeder reports as-is.
|
|
45
|
+
function readSettingsObject(settingsPath) {
|
|
38
46
|
if (!existsSync(settingsPath)) {
|
|
39
|
-
return { skipped: "claude-settings-not-found"
|
|
47
|
+
return { skipped: "claude-settings-not-found" };
|
|
40
48
|
}
|
|
41
49
|
let settings;
|
|
42
50
|
try {
|
|
43
51
|
settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
|
44
52
|
} catch {
|
|
45
|
-
return { skipped: "settings-not-parseable"
|
|
53
|
+
return { skipped: "settings-not-parseable" };
|
|
46
54
|
}
|
|
47
55
|
if (typeof settings !== "object" || settings === null || Array.isArray(settings)) {
|
|
48
|
-
return { skipped: "settings-not-object"
|
|
56
|
+
return { skipped: "settings-not-object" };
|
|
49
57
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
return { settings };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// What the seeder does with the value already under `fallbackModel`:
|
|
62
|
+
// created — key absent or JSON null (the runtime, drift and the config
|
|
63
|
+
// manager all read null as unset);
|
|
64
|
+
// upgraded — exactly a chain ArkaOS itself seeded before, and not the
|
|
65
|
+
// chain being seeded now (a custom defaultValue may equal it):
|
|
66
|
+
// written by us, not by the operator, so it moves on;
|
|
67
|
+
// noop — any other present value is the operator's choice: an array,
|
|
68
|
+
// an empty array (chain disabled on purpose) or the legacy
|
|
69
|
+
// single string, preserved byte for byte.
|
|
70
|
+
function seedAction(existing, defaultValue) {
|
|
71
|
+
if (existing === undefined || existing === null) {
|
|
72
|
+
return "created";
|
|
57
73
|
}
|
|
58
|
-
|
|
59
|
-
|
|
74
|
+
if (isPreviousDefault(existing) && !sameChain(existing, defaultValue)) {
|
|
75
|
+
return "upgraded";
|
|
76
|
+
}
|
|
77
|
+
return "noop";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Atomic write via .tmp + rename; false on any failure (the seeder never raises).
|
|
81
|
+
function writeSettingsAtomic(settingsPath, settings) {
|
|
60
82
|
const tmp = `${settingsPath}.tmp-${process.pid}`;
|
|
61
83
|
try {
|
|
62
84
|
writeFileSync(tmp, JSON.stringify(settings, null, 2) + "\n");
|
|
63
85
|
renameSync(tmp, settingsPath);
|
|
86
|
+
return true;
|
|
64
87
|
} catch {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function seedFallbackModel({
|
|
93
|
+
runtime = "claude-code",
|
|
94
|
+
home = homedir(),
|
|
95
|
+
defaultValue = DEFAULT_FALLBACK_MODELS,
|
|
96
|
+
} = {}) {
|
|
97
|
+
if (runtime !== "claude-code") {
|
|
98
|
+
return { skipped: "runtime-not-claude-code", action: null };
|
|
99
|
+
}
|
|
100
|
+
const settingsPath = join(home, ".claude", "settings.json");
|
|
101
|
+
const loaded = readSettingsObject(settingsPath);
|
|
102
|
+
if (loaded.skipped) {
|
|
103
|
+
return { skipped: loaded.skipped, action: null };
|
|
104
|
+
}
|
|
105
|
+
const { settings } = loaded;
|
|
106
|
+
const existing = settings.fallbackModel;
|
|
107
|
+
const action = seedAction(existing, defaultValue);
|
|
108
|
+
if (action === "noop") {
|
|
109
|
+
return { skipped: null, action, value: existing };
|
|
110
|
+
}
|
|
111
|
+
const value = [...defaultValue];
|
|
112
|
+
settings.fallbackModel = value;
|
|
113
|
+
if (!writeSettingsAtomic(settingsPath, settings)) {
|
|
65
114
|
return { skipped: "write-failed", action: null };
|
|
66
115
|
}
|
|
67
|
-
return
|
|
116
|
+
return action === "upgraded"
|
|
117
|
+
? { skipped: null, action, value, previous: existing }
|
|
118
|
+
: { skipped: null, action, value };
|
|
68
119
|
}
|
package/installer/index.js
CHANGED
|
@@ -433,15 +433,18 @@ export async function install({ runtime, path, force, skipSystem, withOllama, pr
|
|
|
433
433
|
detail(` Warning: could not seed worktree.baseRef (${err.message})`);
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
// Runtime Sync PR3 — seed fallbackModel =
|
|
437
|
-
// so an overloaded or unavailable primary degrades
|
|
438
|
-
// session.
|
|
439
|
-
//
|
|
436
|
+
// Runtime Sync PR3 — seed fallbackModel = DEFAULT_FALLBACK_MODELS (see
|
|
437
|
+
// fallback-model.js) so an overloaded or unavailable primary degrades
|
|
438
|
+
// instead of failing the session. Absent keys are seeded and a chain that
|
|
439
|
+
// is exactly a previous ArkaOS default is upgraded; an operator chain
|
|
440
|
+
// (array or legacy string) is never touched.
|
|
440
441
|
try {
|
|
441
442
|
const { seedFallbackModel } = await import("./fallback-model.js");
|
|
442
443
|
const fbResult = seedFallbackModel({ runtime });
|
|
443
444
|
if (!fbResult.skipped && fbResult.action === "created") {
|
|
444
445
|
detail(` fallbackModel set to ${JSON.stringify(fbResult.value)}.`);
|
|
446
|
+
} else if (!fbResult.skipped && fbResult.action === "upgraded") {
|
|
447
|
+
detail(` fallbackModel upgraded from ${JSON.stringify(fbResult.previous)} to ${JSON.stringify(fbResult.value)}.`);
|
|
445
448
|
}
|
|
446
449
|
} catch (err) {
|
|
447
450
|
detail(` Warning: could not seed fallbackModel (${err.message})`);
|
package/installer/update.js
CHANGED
|
@@ -244,13 +244,16 @@ export async function update({ skillsFlag = "" } = {}) {
|
|
|
244
244
|
warn(`Output style update failed (${err.message})`);
|
|
245
245
|
}
|
|
246
246
|
// Runtime Sync PR3 — seed fallbackModel on update too, so machines that
|
|
247
|
-
// installed before the chain existed get it
|
|
248
|
-
//
|
|
247
|
+
// installed before the chain existed get it, and a chain that is exactly
|
|
248
|
+
// a previous ArkaOS default moves to the current one. An operator chain
|
|
249
|
+
// (array, empty array or legacy string) is never touched.
|
|
249
250
|
try {
|
|
250
251
|
const { seedFallbackModel } = await import("./fallback-model.js");
|
|
251
252
|
const fbResult = seedFallbackModel({ runtime: manifest.runtime || "claude-code" });
|
|
252
253
|
if (!fbResult.skipped && fbResult.action === "created") {
|
|
253
254
|
ok(`fallbackModel set to ${JSON.stringify(fbResult.value)}`);
|
|
255
|
+
} else if (!fbResult.skipped && fbResult.action === "upgraded") {
|
|
256
|
+
ok(`fallbackModel upgraded from ${JSON.stringify(fbResult.previous)} to ${JSON.stringify(fbResult.value)}`);
|
|
254
257
|
}
|
|
255
258
|
} catch (err) {
|
|
256
259
|
warn(`fallbackModel seed failed (${err.message})`);
|
package/package.json
CHANGED