axstack 0.28.0 → 0.30.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 +1 -1
- package/docs/concepts.md +1 -1
- package/docs/host-operations.md +39 -32
- package/docs/installation.md +6 -2
- package/docs/workflows.md +5 -2
- package/package.json +1 -1
- package/profiles/presets/claude-only.json +6 -4
- package/profiles/presets/mixed.json +6 -4
- package/skills/axstack/references/contracts.md +2 -0
- package/skills/axstack/references/role-roster.md +4 -1
- package/skills/axstack/scripts/account-usage.js +3 -2
- package/skills/axstack/scripts/sync-default.js +62 -24
package/README.md
CHANGED
|
@@ -126,7 +126,7 @@ Eligible own PRs use guarded automatic merge; see [merge boundaries, exclusions,
|
|
|
126
126
|
|
|
127
127
|
- T3 Code is the only supported active runtime. Axstack adds no daemon or runtime
|
|
128
128
|
database. The exception is one opt-in systemd user timer `axstack-default-sync`,
|
|
129
|
-
running a stateless script that rewrites only T3 `defaultModelSelection
|
|
129
|
+
running a stateless script that rewrites only T3 `defaultModelSelection`.
|
|
130
130
|
It has no workflow state and launches no agent; see
|
|
131
131
|
[Host operations](docs/host-operations.md#new-chat-default-sync).
|
|
132
132
|
- The recorded owning watch thread merges eligible own PRs; excluded PRs use a merge card.
|
package/docs/concepts.md
CHANGED
|
@@ -103,7 +103,7 @@ and effort remain fixed. Dispatched roles never fail over mid-thread.
|
|
|
103
103
|
See [Provider bindings](../skills/axstack/references/t3-runtime.md#preflight-and-binding)
|
|
104
104
|
for schedule rebinding and the [policy carve-out](workflows.md#automatic-merge-boundaries).
|
|
105
105
|
|
|
106
|
-
The opt-in `sync-default.js`
|
|
106
|
+
The opt-in `sync-default.js` rotates the New-chat default by elapsed-minus-used pace at a ten-point lead or current exclusion. `AXSTACK_SYNC_POOL` enables cross-driver weekly comparison and model/effort selection; without it, only the same-driver account changes.
|
|
107
107
|
|
|
108
108
|
## Account selection environment
|
|
109
109
|
|
package/docs/host-operations.md
CHANGED
|
@@ -46,7 +46,7 @@ executor MCP, and live schedule behavior need separate preflights.
|
|
|
46
46
|
Installation creates no production schedule and never enables a timer.
|
|
47
47
|
Axstack allows one opt-in systemd user timer `axstack-default-sync` as the
|
|
48
48
|
scheduler exception: it runs a stateless script that rewrites only T3
|
|
49
|
-
`defaultModelSelection
|
|
49
|
+
`defaultModelSelection`, has no workflow state and launches no agent.
|
|
50
50
|
Every verified own-PR publication arms or joins the driver's chat-run watch.
|
|
51
51
|
Its bound T3 schedule uses the cadence in [Chat-run watch activation](#chat-run-watch-activation).
|
|
52
52
|
See [Chat-run PR watch](workflows.md#chat-run-pr-watch) for authority, schedule identity and stop conditions.
|
|
@@ -178,30 +178,34 @@ canary described by the operational contract.
|
|
|
178
178
|
|
|
179
179
|
## New-chat default sync
|
|
180
180
|
|
|
181
|
-
This opt-in
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
181
|
+
This opt-in script changes only root `defaultModelSelection` in
|
|
182
|
+
`~/.t3/userdata/settings.json`. Without a pool it changes only the instance
|
|
183
|
+
among enabled accounts of the current Claude/Codex driver.
|
|
184
|
+
`AXSTACK_SYNC_POOL` compares drivers by weekly clocks (>=604800s), without
|
|
185
|
+
plan weights. Rotation needs a ten-point elapsed-minus-used pace lead;
|
|
186
|
+
excluded/unauthorized/skipped current accounts can escape to known candidates.
|
|
187
|
+
Any window at 95% or a reached limit excludes. Non-current absent credentials
|
|
188
|
+
exclude; malformed/unreadable credentials and unknown/stale usage hold.
|
|
189
|
+
Cache expires after five minutes. Missing defaults stay untouched; OAuth tokens
|
|
190
|
+
are never refreshed. Each host's pool uses only enabled, credentialed drivers:
|
|
191
191
|
|
|
192
192
|
```sh
|
|
193
|
-
|
|
193
|
+
AXSTACK_SYNC_POOL=claudeAgent=claude-opus-5-5,codex=gpt-6.1-sol:xhigh \
|
|
194
|
+
bun ~/.agents/skills/axstack/scripts/sync-default.js --dry-run
|
|
194
195
|
```
|
|
195
196
|
|
|
196
|
-
Exit 0 prints one token-free JSON decision
|
|
197
|
-
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
197
|
+
Exit 0 prints one token-free JSON decision with proposed selection; dry-run
|
|
198
|
+
never writes. `--settings <path>` selects another file. Pool entries are
|
|
199
|
+
`driver=model[:effort]`: Claude `effort` or Codex `reasoningEffort` options;
|
|
200
|
+
omitted effort omits options. Rotation writes the full pool selection,
|
|
201
|
+
overwriting manual model/effort picks only then. Pin with `AXSTACK_SYNC_DEFAULT=0`
|
|
202
|
+
or a default driver outside the pool. Exit 1 means invalid pool/settings or write
|
|
203
|
+
failure. Save the full selection for rollback. Other bytes stay intact.
|
|
204
|
+
A fsynced sibling preserves mode; original bytes are checked before rename and
|
|
205
|
+
read back afterward. The accepted sub-ms T3 write race can lose an edit;
|
|
206
|
+
read-back mismatch is an error.
|
|
203
207
|
|
|
204
|
-
|
|
208
|
+
Under host-mutation authority, install and enable:
|
|
205
209
|
|
|
206
210
|
```sh
|
|
207
211
|
mkdir -p ~/.config/systemd/user
|
|
@@ -213,18 +217,21 @@ systemctl --user list-timers axstack-default-sync.timer
|
|
|
213
217
|
journalctl --user -u axstack-default-sync.service
|
|
214
218
|
```
|
|
215
219
|
|
|
216
|
-
The
|
|
217
|
-
minutes. `Persistent=true`
|
|
218
|
-
|
|
219
|
-
`
|
|
220
|
-
|
|
221
|
-
|
|
220
|
+
The timer starts about one minute after user-manager startup, then every ten
|
|
221
|
+
minutes. Its monotonic `Persistent=true` timer never replays calendar ticks.
|
|
222
|
+
Override Bun/skills paths via `ExecStart`/`Environment` in
|
|
223
|
+
`systemctl --user edit axstack-default-sync.service`. Add the per-host pool:
|
|
224
|
+
|
|
225
|
+
```ini
|
|
226
|
+
[Service]
|
|
227
|
+
Environment=AXSTACK_SYNC_POOL=claudeAgent=claude-opus-5-5,codex=gpt-6.1-sol:xhigh
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Reload after edits. Logout needs authorized linger; installation never activates units.
|
|
222
231
|
|
|
223
232
|
Disable with `systemctl --user disable --now axstack-default-sync.timer` and
|
|
224
233
|
stop any active one-shot with `systemctl --user stop axstack-default-sync.service`.
|
|
225
|
-
|
|
226
|
-
The
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
exact empty directory and retries acquisition once, recovering on a later tick
|
|
230
|
-
from a killed writer. If the retry finds a fresh lock, it still prints locked.
|
|
234
|
+
Rollback: remove the pool drop-in, reload, and restore the full selection in T3 Settings.
|
|
235
|
+
The sibling `<settings>.axstack-default-sync.lock` directory prevents overlap
|
|
236
|
+
(unchanged/locked). Empty locks older than sixty seconds are removed and retried
|
|
237
|
+
once; fresh contention reports locked.
|
package/docs/installation.md
CHANGED
|
@@ -93,7 +93,7 @@ for the picker cache directory and test-only usage endpoint variables.
|
|
|
93
93
|
Run these with Bun from the installed `<skills-dir>/axstack/scripts/` directory:
|
|
94
94
|
`resolve-models.js` (model IDs), `pick-instance.js` (dispatch account),
|
|
95
95
|
`pr-digest.js` (PR discovery), `archive-evidence.js` (evidence archive), and
|
|
96
|
-
`sync-default.js [--settings <path>] [--dry-run]` (New-chat
|
|
96
|
+
`sync-default.js [--settings <path>] [--dry-run]` (New-chat default; `AXSTACK_SYNC_POOL` opts into cross-driver rotation).
|
|
97
97
|
`account-usage.js` shares usage retrieval and the five-minute cache between
|
|
98
98
|
picker and sync. The opt-in systemd user unit files ship in `axstack/systemd/`;
|
|
99
99
|
installation copies them as assets and never enables the timer.
|
|
@@ -299,7 +299,11 @@ the user explicitly changes it and accepts the resulting evidence invalidation.
|
|
|
299
299
|
|
|
300
300
|
The `mixed` and `claude-only` presets assign `axstack-auditor`,
|
|
301
301
|
`axstack-research-requirements`, `axstack-research-code`, `axstack-research-web`,
|
|
302
|
-
`axstack-explore-execution
|
|
302
|
+
`axstack-explore-execution` to the Claude Sonnet class at high effort.
|
|
303
|
+
`axstack-explore-codebase` and `axstack-monitor` adopt Haiku 5.5 high with
|
|
304
|
+
`modelClass: haiku` and the exact `model: claude-haiku-5-5` pin.
|
|
305
|
+
Activation requires that exact ID and high effort in saved T3 capabilities;
|
|
306
|
+
missing either holds these roles without substitution, including Haiku 4.5.
|
|
303
307
|
The `codex-only` assignments for these roles are unchanged.
|
|
304
308
|
The three `-sol` pair seats for auditor, research-code, and explore-execution
|
|
305
309
|
use Sol high in `mixed` and `codex-only`; `claude-only` records intentional
|
package/docs/workflows.md
CHANGED
|
@@ -124,8 +124,11 @@ role.
|
|
|
124
124
|
| `claude-only` | Opus medium | Opus medium; Sonnet high | unavailable / Opus xhigh | Sonnet high (Sol absent) |
|
|
125
125
|
|
|
126
126
|
In `mixed` and `claude-only`, `axstack-auditor`, `axstack-research-requirements`,
|
|
127
|
-
`axstack-research-code`, `axstack-research-web`, `axstack-explore-execution
|
|
128
|
-
|
|
127
|
+
`axstack-research-code`, `axstack-research-web`, and `axstack-explore-execution`
|
|
128
|
+
use the Claude Sonnet class at high effort. `axstack-explore-codebase` and
|
|
129
|
+
`axstack-monitor` adopt Haiku 5.5 high, with class `haiku` and exact pin
|
|
130
|
+
`claude-haiku-5-5`. Missing that ID or high effort in saved T3 capabilities
|
|
131
|
+
holds those roles without substitution. `codex-only` keeps its Codex
|
|
129
132
|
assignments for those roles. Mixed web-google and X retain their source-specific
|
|
130
133
|
Antigravity and Grok routes.
|
|
131
134
|
The new `-sol` auditor, research-code, and explore-execution seats use Sol high
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "axstack",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.30.0",
|
|
4
4
|
"description": "Axstack installer and setup CLI: installs owned chat skills and role data, configures supported harness settings, and checks T3 Code capabilities.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -169,8 +169,9 @@
|
|
|
169
169
|
"provider": "claude",
|
|
170
170
|
"modeId": "bypassPermissions",
|
|
171
171
|
"thinkingOptionId": "high",
|
|
172
|
-
"notes": "
|
|
173
|
-
"modelClass": "
|
|
172
|
+
"notes": "Haiku 5.5 high codebase mapper: explores repository structure and interfaces for research and handoff context. T3 uses the exact claude-haiku-5-5 pin from saved capabilities; a Claude rejection holds. Missing model or high effort holds without substitution.",
|
|
173
|
+
"modelClass": "haiku",
|
|
174
|
+
"model": "claude-haiku-5-5"
|
|
174
175
|
},
|
|
175
176
|
{
|
|
176
177
|
"id": "axstack-explore-execution",
|
|
@@ -196,8 +197,9 @@
|
|
|
196
197
|
"provider": "claude",
|
|
197
198
|
"modeId": "bypassPermissions",
|
|
198
199
|
"thinkingOptionId": "high",
|
|
199
|
-
"notes": "Optional
|
|
200
|
-
"modelClass": "
|
|
200
|
+
"notes": "Optional independent read-only Haiku 5.5 high observer for a standalone PR watch. Reads GitHub, feedback, and checks, persists event IDs, and wakes the owner only for a new actionable event. Never sends, authors, reviews, replies, or acts as either reusable PR manager. Healthy snapshots stay quiet. Chat-run mode: one same-host native read-only observer per Run; fresh finite passes report precise deltas internally to the original Run/driver and may disable/read back only their own automation at verified stop. No repair, dispatch, public notification, or replacement coordinator. Effective scheduled model/effort and wake require live proof. T3 uses the exact claude-haiku-5-5 pin from saved capabilities; a Claude rejection holds. Missing model or high effort holds without substitution.",
|
|
201
|
+
"modelClass": "haiku",
|
|
202
|
+
"model": "claude-haiku-5-5"
|
|
201
203
|
},
|
|
202
204
|
{
|
|
203
205
|
"id": "axstack-auditor",
|
|
@@ -169,8 +169,9 @@
|
|
|
169
169
|
"provider": "claude",
|
|
170
170
|
"modeId": "bypassPermissions",
|
|
171
171
|
"thinkingOptionId": "high",
|
|
172
|
-
"notes": "
|
|
173
|
-
"modelClass": "
|
|
172
|
+
"notes": "Haiku 5.5 high codebase mapper: explores repository structure and interfaces for research and handoff context. T3 uses the exact claude-haiku-5-5 pin from saved capabilities; a Claude rejection holds. Missing model or high effort holds without substitution.",
|
|
173
|
+
"modelClass": "haiku",
|
|
174
|
+
"model": "claude-haiku-5-5"
|
|
174
175
|
},
|
|
175
176
|
{
|
|
176
177
|
"id": "axstack-explore-execution",
|
|
@@ -196,8 +197,9 @@
|
|
|
196
197
|
"provider": "claude",
|
|
197
198
|
"modeId": "bypassPermissions",
|
|
198
199
|
"thinkingOptionId": "high",
|
|
199
|
-
"notes": "Optional
|
|
200
|
-
"modelClass": "
|
|
200
|
+
"notes": "Optional independent read-only Haiku 5.5 high observer for a standalone PR watch. Reads GitHub, feedback, and checks, persists event IDs, and wakes the owner only for a new actionable event. Never sends, authors, reviews, replies, or acts as either reusable PR manager. Healthy snapshots stay quiet. Chat-run mode: one same-host native read-only observer per Run; fresh finite passes report precise deltas internally to the original Run/driver and may disable/read back only their own automation at verified stop. No repair, dispatch, public notification, or replacement coordinator. Effective scheduled model/effort and wake require live proof. T3 uses the exact claude-haiku-5-5 pin from saved capabilities; a Claude rejection holds. Missing model or high effort holds without substitution.",
|
|
201
|
+
"modelClass": "haiku",
|
|
202
|
+
"model": "claude-haiku-5-5"
|
|
201
203
|
},
|
|
202
204
|
{
|
|
203
205
|
"id": "axstack-auditor",
|
|
@@ -48,6 +48,8 @@ Never infer any other route from quota state or subscription entitlement. Every
|
|
|
48
48
|
substitution requires the user's decision: configured alternatives are not
|
|
49
49
|
defaults. Rejection, timeout, quota and auth failures hold affected work.
|
|
50
50
|
Substitution here means changing provider or model, excluding that account selection.
|
|
51
|
+
Exception: opt-in default sync can change only the New-chat default within the user-configured pool.
|
|
52
|
+
Running threads, dispatched roles, schedules and roles.json keep existing rules.
|
|
51
53
|
Read the [T3 runtime boundary](t3-runtime.md) before dispatch, receipt consumption
|
|
52
54
|
or recovery; resolve models from the saved capabilities snapshot and verify
|
|
53
55
|
requested and effective settings separately.
|
|
@@ -30,8 +30,11 @@
|
|
|
30
30
|
for every interactive page, bound to the exact bytes and SHA-256.
|
|
31
31
|
- `axstack-auditor`/`axstack-research-requirements`/
|
|
32
32
|
`axstack-research-code`/`axstack-research-web`/
|
|
33
|
-
`axstack-explore-execution
|
|
33
|
+
`axstack-explore-execution`:
|
|
34
34
|
`claude/sonnet` high in mixed/claude-only.
|
|
35
|
+
- `axstack-explore-codebase`/`axstack-monitor`: `claude/haiku` high in
|
|
36
|
+
mixed/claude-only, pinned to `claude-haiku-5-5`. Missing that exact model
|
|
37
|
+
or high effort holds these roles without substitution.
|
|
35
38
|
`axstack-monitor`: standalone watch never sends; chat-run watch: bounded
|
|
36
39
|
internal reports to its Run and original driver.
|
|
37
40
|
- Sol pairs `axstack-auditor-sol`/`axstack-research-code-sol`/
|
|
@@ -33,8 +33,9 @@ export async function accountUsage(instanceId, instance, provider) {
|
|
|
33
33
|
const valid = provider === 'claude' ? credentials.claudeAiOauth?.accessToken
|
|
34
34
|
: credentials.tokens?.access_token && credentials.tokens?.account_id;
|
|
35
35
|
if (!valid) throw new Error();
|
|
36
|
-
} catch {
|
|
37
|
-
return { instanceId, state: 'skipped', reason: 'missing credentials'
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return { instanceId, state: 'skipped', reason: 'missing credentials',
|
|
38
|
+
credentialsAbsent: error.code === 'ENOENT' };
|
|
38
39
|
}
|
|
39
40
|
const headers = provider === 'claude'
|
|
40
41
|
? { authorization: `Bearer ${credentials.claudeAiOauth.accessToken}`,
|
|
@@ -2,55 +2,86 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import { accountUsage, expandHome } from './account-usage.js';
|
|
3
3
|
|
|
4
4
|
const drivers = { claudeAgent: 'claude', codex: 'codex' };
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
function parsePool(value) {
|
|
6
|
+
if (!value) return null;
|
|
7
|
+
const pool = {};
|
|
8
|
+
for (const entry of value.split(',')) {
|
|
9
|
+
const match = /^(claudeAgent|codex)=([^:,=\s]+)(?::([a-z]+))?$/.exec(entry);
|
|
10
|
+
if (!match || Object.hasOwn(pool, match[1])) throw new Error('invalid pool');
|
|
11
|
+
const [, driver, model, effort] = match;
|
|
12
|
+
pool[driver] = { model, ...(effort && { options: [
|
|
13
|
+
{ id: driver === 'codex' ? 'reasoningEffort' : 'effort', value: effort },
|
|
14
|
+
] }) };
|
|
15
|
+
}
|
|
16
|
+
return pool;
|
|
17
|
+
}
|
|
18
|
+
const candidates = (settings, pool) => Object.entries(settings.providerInstances ?? {}).filter(([, instance]) =>
|
|
19
|
+
instance.enabled && Object.hasOwn(pool, instance.driver));
|
|
7
20
|
|
|
8
|
-
export function decide(settings, usage, now) {
|
|
21
|
+
export function decide(settings, usage, now, pool = null) {
|
|
9
22
|
const from = settings.defaultModelSelection?.instanceId ?? null;
|
|
10
23
|
const result = { action: 'unchanged', reason: 'unmanaged', from, to: from, instances: [] };
|
|
11
24
|
const driver = settings.providerInstances?.[from]?.driver;
|
|
12
|
-
|
|
13
|
-
|
|
25
|
+
const pooled = pool !== null;
|
|
26
|
+
pool ??= { [driver]: null };
|
|
27
|
+
if (!Object.hasOwn(drivers, driver) || !Object.hasOwn(pool, driver)) return result;
|
|
28
|
+
const updated = (best, reason) => ({ ...result, action: 'updated', reason, to: best.instanceId,
|
|
29
|
+
selection: pooled ? { instanceId: best.instanceId, ...pool[settings.providerInstances[best.instanceId].driver] }
|
|
30
|
+
: { ...settings.defaultModelSelection, instanceId: best.instanceId } });
|
|
31
|
+
result.instances = candidates(settings, pool).map(([instanceId, instance]) => {
|
|
14
32
|
const account = usage.find((entry) => entry.instanceId === instanceId);
|
|
15
|
-
const window = driver === 'claudeAgent' ? account?.windows?.[1]
|
|
33
|
+
const window = instance.driver === 'claudeAgent' ? account?.windows?.[1]
|
|
16
34
|
: account?.windows?.[1] ?? account?.windows?.[0];
|
|
17
|
-
const excluded = account?.utilization >= 95 || account?.limitReached
|
|
35
|
+
const excluded = account?.utilization >= 95 || account?.limitReached || account?.credentialsAbsent
|
|
18
36
|
|| (instanceId === from && (account?.unauthorized || account?.state === 'skipped'));
|
|
19
37
|
const known = account && ['fresh', 'cached'].includes(account.state)
|
|
20
38
|
&& Number.isFinite(account.updatedAt) && now - account.updatedAt < 300000
|
|
21
39
|
&& Number.isFinite(window?.used) && Number.isFinite(window?.seconds) && window.seconds > 0
|
|
22
|
-
&& Number.isFinite(window?.resetAt) && window.resetAt > now
|
|
40
|
+
&& Number.isFinite(window?.resetAt) && window.resetAt > now
|
|
41
|
+
&& (!pooled || window.seconds >= 604800);
|
|
23
42
|
return { instanceId, state: excluded ? 'excluded' : known ? 'eligible' : 'unknown',
|
|
24
43
|
pace: known ? 100 * (1 - (window.resetAt - now) / (window.seconds * 1000)) - window.used : null };
|
|
25
44
|
});
|
|
26
45
|
const current = result.instances.find((entry) => entry.instanceId === from);
|
|
27
46
|
const best = result.instances.filter((entry) => entry.state === 'eligible').sort((a, b) => b.pace - a.pace)[0];
|
|
28
47
|
const excluded = !current || current.state === 'excluded';
|
|
29
|
-
if (excluded && best) return
|
|
48
|
+
if (excluded && best) return updated(best, 'current-excluded');
|
|
30
49
|
if (result.instances.some((entry) => entry.state === 'unknown')) return { ...result, reason: 'unknown' };
|
|
31
50
|
if (!best) return { ...result, reason: 'no-eligible' };
|
|
32
51
|
if (best.instanceId !== from && best.pace - current.pace >= 10) {
|
|
33
|
-
return
|
|
52
|
+
return updated(best, 'pace');
|
|
34
53
|
}
|
|
35
54
|
return { ...result, reason: 'margin' };
|
|
36
55
|
}
|
|
37
56
|
|
|
38
|
-
//
|
|
39
|
-
function selectionBytes(original,
|
|
57
|
+
// Preserve the instance-only edit when the rest of the selection is already identical.
|
|
58
|
+
function selectionBytes(original, selection) {
|
|
59
|
+
const previous = JSON.parse(original).defaultModelSelection;
|
|
60
|
+
const instanceOnly = deepEqual({ ...previous, instanceId: selection.instanceId }, selection);
|
|
61
|
+
let start;
|
|
40
62
|
const tokens = [...original.matchAll(/"(?:\\.|[^"\\])*"|[{}\[\]:,]|[^\s{}\[\]:,]+/g)];
|
|
41
63
|
let depth = 0;
|
|
42
64
|
let selectionDepth = 0;
|
|
43
65
|
for (let i = 0; i < tokens.length; i++) {
|
|
44
66
|
const token = tokens[i][0];
|
|
45
67
|
if (depth === 1 && token.startsWith('"') && JSON.parse(token) === 'defaultModelSelection'
|
|
46
|
-
&& tokens[i + 1]?.[0] === ':' && tokens[i + 2]?.[0] === '{')
|
|
47
|
-
|
|
68
|
+
&& tokens[i + 1]?.[0] === ':' && tokens[i + 2]?.[0] === '{') {
|
|
69
|
+
selectionDepth = depth + 1;
|
|
70
|
+
start = tokens[i + 2].index;
|
|
71
|
+
}
|
|
72
|
+
if (instanceOnly && selectionDepth && depth === selectionDepth && token.startsWith('"') && JSON.parse(token) === 'instanceId'
|
|
48
73
|
&& tokens[i + 1]?.[0] === ':') {
|
|
49
74
|
const value = tokens[i + 2];
|
|
50
|
-
return original.slice(0, value.index) + JSON.stringify(instanceId) + original.slice(value.index + value[0].length);
|
|
75
|
+
return original.slice(0, value.index) + JSON.stringify(selection.instanceId) + original.slice(value.index + value[0].length);
|
|
51
76
|
}
|
|
52
77
|
if (token === '{' || token === '[') depth++;
|
|
53
|
-
if (token === '}' || token === ']') {
|
|
78
|
+
if (token === '}' || token === ']') {
|
|
79
|
+
if (depth === selectionDepth) {
|
|
80
|
+
if (!instanceOnly) return original.slice(0, start) + JSON.stringify(selection) + original.slice(tokens[i].index + 1);
|
|
81
|
+
selectionDepth = 0;
|
|
82
|
+
}
|
|
83
|
+
depth--;
|
|
84
|
+
}
|
|
54
85
|
}
|
|
55
86
|
throw new Error('missing instance string');
|
|
56
87
|
}
|
|
@@ -62,15 +93,18 @@ function deepEqual(a, b) {
|
|
|
62
93
|
return keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && deepEqual(a[key], b[key]));
|
|
63
94
|
}
|
|
64
95
|
|
|
65
|
-
export function commit(path, originalBytes, nextBytes) {
|
|
96
|
+
export function commit(path, originalBytes, nextBytes, selection) {
|
|
66
97
|
const original = JSON.parse(originalBytes);
|
|
67
98
|
const next = JSON.parse(nextBytes);
|
|
68
99
|
const from = original.defaultModelSelection?.instanceId;
|
|
69
100
|
const to = next.defaultModelSelection?.instanceId;
|
|
70
101
|
if (typeof from !== 'string' || typeof to !== 'string') throw new Error('invalid instance string');
|
|
71
|
-
original.defaultModelSelection
|
|
72
|
-
if (!deepEqual(
|
|
73
|
-
|
|
102
|
+
selection ??= { ...original.defaultModelSelection, instanceId: to };
|
|
103
|
+
if (!deepEqual(next.defaultModelSelection, selection)) throw new Error('refusing undecided selection');
|
|
104
|
+
const unchanged = deepEqual(original.defaultModelSelection, selection);
|
|
105
|
+
original.defaultModelSelection = selection;
|
|
106
|
+
if (!deepEqual(original, next)) throw new Error('refusing change outside defaultModelSelection');
|
|
107
|
+
if (unchanged) return 'unchanged';
|
|
74
108
|
const lock = `${path}.axstack-default-sync.lock`;
|
|
75
109
|
try { fs.mkdirSync(lock, { mode: 0o700 }); }
|
|
76
110
|
catch (error) {
|
|
@@ -131,12 +165,16 @@ async function main() {
|
|
|
131
165
|
|| (Object.hasOwn(settings, 'providerInstances') && (!settings.providerInstances
|
|
132
166
|
|| typeof settings.providerInstances !== 'object' || Array.isArray(settings.providerInstances)))) throw new Error('invalid settings');
|
|
133
167
|
const driver = settings.providerInstances?.[settings.defaultModelSelection?.instanceId]?.driver;
|
|
134
|
-
const
|
|
168
|
+
const pool = parsePool(process.env.AXSTACK_SYNC_POOL);
|
|
135
169
|
const usage = [];
|
|
136
|
-
if (
|
|
137
|
-
|
|
170
|
+
if (Object.hasOwn(drivers, driver) && (!pool || Object.hasOwn(pool, driver))) {
|
|
171
|
+
for (const [id, instance] of candidates(settings, pool ?? { [driver]: null })) {
|
|
172
|
+
usage.push(await accountUsage(id, instance, drivers[instance.driver]));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
let decision = decide(settings, usage, Date.now(), pool);
|
|
138
176
|
if (decision.action === 'updated' && !dryRun) {
|
|
139
|
-
const reason = commit(path, original, selectionBytes(original, decision.
|
|
177
|
+
const reason = commit(path, original, selectionBytes(original, decision.selection), decision.selection);
|
|
140
178
|
if (reason !== 'updated') decision = { ...decision, action: 'unchanged', reason, to: decision.from };
|
|
141
179
|
}
|
|
142
180
|
console.log(JSON.stringify(decision));
|