axstack 0.55.0 → 0.56.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/docs/concepts.md +3 -1
- package/docs/installation.md +5 -2
- package/docs/workflows.md +3 -1
- package/package.json +1 -1
- package/skills/axstack/references/fleet-dispatch.md +166 -0
- package/skills/axstack/references/fleet-route-schema.md +11 -0
- package/skills/axstack/references/fleet-verification.md +102 -0
- package/skills/axstack/references/lifecycle.md +19 -2
- package/skills/axstack/references/role-roster.md +6 -0
- package/skills/axstack/references/routing.md +12 -0
- package/skills/axstack/references/run-record.md +83 -0
- package/skills/axstack/references/t3-runtime.md +173 -6
- package/skills/axstack/references/workspace-hygiene.md +8 -0
- package/skills/axstack/scripts/dispatch-plan.js +19 -2
- package/skills/axstack/scripts/fleet-destination.js +39 -0
- package/skills/axstack/scripts/fleet-dispatch.js +6 -0
- package/skills/axstack/scripts/fleet-facts.js +12 -1
- package/skills/axstack/scripts/fleet-inventory.js +76 -0
- package/skills/axstack/scripts/fleet-receipt.js +130 -0
- package/skills/axstack/scripts/fleet-route.js +4 -3
- package/skills/axstack-cleanup/SKILL.md +21 -0
- package/skills/axstack-implement/SKILL.md +12 -5
- package/skills/axstack-watch/SKILL.md +3 -0
- package/skills/axstack-watch/references/watch-runtime.md +18 -5
- package/skills/voa/SKILL.md +96 -0
- package/skills/voa/references/observations.md +79 -0
- package/skills/voa/references/recovery.md +94 -0
- package/skills/voa/references/verification.md +35 -0
- package/skills/voa/scripts/cleanup.js +39 -0
- package/skills/voa/scripts/evaluate.js +122 -0
- package/skills/voa/scripts/observations.js +68 -0
- package/skills/voa/scripts/records.js +48 -0
- package/skills/voa/scripts/voa.js +43 -0
- package/src/installer.js +3 -2
- package/src/instructions.js +11 -3
|
@@ -71,6 +71,125 @@ The first dispatch for each (provider, model, effort) must reach a running or
|
|
|
71
71
|
completed run before its siblings launch. Rejection stops siblings of that
|
|
72
72
|
configuration; unrelated configurations remain eligible.
|
|
73
73
|
|
|
74
|
+
## Coordinating multiple drivers
|
|
75
|
+
|
|
76
|
+
The Coordinator owns the overall outcome and coordinates work across projects.
|
|
77
|
+
A Driver owns delivery within one project: implementation, review, merge and deployment.
|
|
78
|
+
A Worker performs a bounded task for a Driver, including authoring, research or review.
|
|
79
|
+
Parent describes a relationship, not a role name.
|
|
80
|
+
|
|
81
|
+
A logical Coordinator can coordinate multiple project-bound
|
|
82
|
+
drivers, each using T3 delegation for multiple scoped Workers. Reuse a suitable
|
|
83
|
+
current Coordinator. Suggest a new Coordinator only when useful.
|
|
84
|
+
Every top-level thread launch requires an explicit user request, including a
|
|
85
|
+
new Coordinator, project driver or launch fallback. On resume, preserve existing
|
|
86
|
+
explicit accepted authorization within its scope. This skill never grants
|
|
87
|
+
blanket launch permission. Missing launch authorization holds only the affected
|
|
88
|
+
launch. After a blocked launch, continue independent authorized work, subject
|
|
89
|
+
to its dependencies and scoped holds under [Autopilot](autopilot.md#advance-and-hold).
|
|
90
|
+
|
|
91
|
+
For cross-project work, use the correct project-bound driver, selecting the
|
|
92
|
+
projectId and workspaceStrategy in t3_thread_launch when an authorized launch
|
|
93
|
+
is needed. Reuse an existing appropriate driver after reconciling its ownership
|
|
94
|
+
and scope, and verify the resulting project/worktree binding before dispatch.
|
|
95
|
+
A local delegate_task cannot select another project. Never retry the same
|
|
96
|
+
invalid project request. A routing failure holds that route and its dependent
|
|
97
|
+
work, rather than the whole authorized run.
|
|
98
|
+
|
|
99
|
+
Record the logical coordinator link separately from native parentThreadId,
|
|
100
|
+
including when null. The Coordinator remains the logical role when T3 records project Drivers as
|
|
101
|
+
separate top-level threads with null parentThreadId; native ancestry does not
|
|
102
|
+
change the recorded coordination ownership. For each driver, record its
|
|
103
|
+
exact link, project, worktree, scope, owner and record pointer. Record acceptance
|
|
104
|
+
and completion handbacks, dependencies, held state and resume condition. An
|
|
105
|
+
acceptance handback binds the driver identity to accepted scope and authority;
|
|
106
|
+
completion handbacks carry exact revisions and verified receipt pointers under
|
|
107
|
+
[Lifecycle](lifecycle.md#receipts-bind-each-decision-to-evidence). Reconcile native
|
|
108
|
+
state before advancing dependencies, rather than accepting a message alone.
|
|
109
|
+
|
|
110
|
+
Each driver owns its own PR mutations and watches and is the sole writer of its
|
|
111
|
+
own run record. The Coordinator owns aggregate coordination and its aggregate record,
|
|
112
|
+
using driver handbacks rather than editing their records or taking over their PRs.
|
|
113
|
+
Never allow competing writers for a candidate or record across drivers.
|
|
114
|
+
Existing author exclusivity and accepted-transfer rules still apply.
|
|
115
|
+
|
|
116
|
+
This is an optional coordination shape: single-driver runs remain unchanged.
|
|
117
|
+
The Coordinator and Drivers have no default model role in presets. Existing model,
|
|
118
|
+
permission and remote-author restrictions remain in force, with no substitution
|
|
119
|
+
or remote-author enablement. Coordination adds no daemon, scheduler or runtime DB.
|
|
120
|
+
|
|
121
|
+
## Waiting turns and diagnosis
|
|
122
|
+
|
|
123
|
+
Before ending a waiting turn, follow [Waiting-turn and closing records](run-record.md#waiting-turn-and-closing-records).
|
|
124
|
+
The Driver diagnoses stale reviews, failing checks and relevant upstream fixes
|
|
125
|
+
immediately before ending a waiting turn. Inspect the blocker itself and record
|
|
126
|
+
its cause or a specific unexplained blocker with `diagnosis: none` and an already
|
|
127
|
+
authorized read-only diagnostic next action. Timers never replace reasoning.
|
|
128
|
+
Diagnosing a blocker preserves downstream human, auth, release and serious-risk
|
|
129
|
+
holds; it grants no dismissal, approval or merge rights.
|
|
130
|
+
|
|
131
|
+
## Bounded recovery boundary
|
|
132
|
+
|
|
133
|
+
Ordinary status and periodic status sweeps remain read-only. Recovery requires
|
|
134
|
+
an explicit user recovery invocation naming scope or an authorized schedule
|
|
135
|
+
bound to a Coordinator-designated and recorded existing recovery thread.
|
|
136
|
+
Agent messages never activate it. The accepted implementation follow-up binds
|
|
137
|
+
the bounded synthetic/owned-resource canary to expiry, rollback and exact-ID
|
|
138
|
+
cleanup. Run-scoped activation requires passing canary receipts plus recorded
|
|
139
|
+
recovery host/thread, schedule ID, scope, model/effort, finite budget, expiry and
|
|
140
|
+
cleanup owner. Record the schedule ID at creation and verify its binding before
|
|
141
|
+
any send. The already authorized bounded canary and run-scoped activation need
|
|
142
|
+
no repeat approval when those conditions pass. A persistent shared service
|
|
143
|
+
requires separate explicit user authorization. Reading these instructions alone
|
|
144
|
+
grants no new top-level launch or activation authority. Keep finite pass limits
|
|
145
|
+
shorter than the 30-minute cadence, included owners and coverage cursor bound;
|
|
146
|
+
no catch-up loop. VOA implementation/source ownership is separate.
|
|
147
|
+
|
|
148
|
+
Read the current owner record under [Waiting-turn and closing records](run-record.md#waiting-turn-and-closing-records).
|
|
149
|
+
Match exact owner host/environment/thread and accepted ownership, not title,
|
|
150
|
+
parentThreadId or age. Unknown ownership/history/queue/activity, incomplete
|
|
151
|
+
records or unavailable delivery/idempotency proof remain report-only.
|
|
152
|
+
Any active run or active/pending child work makes the owner active: skip it.
|
|
153
|
+
Delegated owners are status-only under their parent, with no continuation to
|
|
154
|
+
either parent or child. Failed/interrupted execution and completed work with
|
|
155
|
+
cleanup remaining are report-only; never relaunch or redo product work.
|
|
156
|
+
Held queues, pending requests, parked user-reply holds and unknown request/queue
|
|
157
|
+
state are report-only. Only the user resumes a held queue. Explicit stops,
|
|
158
|
+
pauses and snoozes skip recovery. These guards take precedence over diagnosis.
|
|
159
|
+
Credentials, session/pairing, permission and new-thread authorization requests
|
|
160
|
+
are report-only, with no diagnosis exception.
|
|
161
|
+
|
|
162
|
+
A diagnosis: none record is eligible only for an idle owner with a specific
|
|
163
|
+
unexplained non-human blocker and an already authorized read-only diagnostic
|
|
164
|
+
next action. This may coexist with a downstream forge-resolved user-merge,
|
|
165
|
+
npm-stage or human-review hold, including an auth-code PR. A human gate alone
|
|
166
|
+
or an already diagnosed human-only hold receives zero continuations. Diagnosis
|
|
167
|
+
never clears or answers that gate. The other eligible case is a named external
|
|
168
|
+
dependency proven cleared, with the owner's authorized next action revalidated.
|
|
169
|
+
Inspect the blocker itself; silence and a completed turn prove neither stall
|
|
170
|
+
nor completion. Verify native PR watch support for the exact external dependency
|
|
171
|
+
before promising its wake. Prefer native events. An owner wake suppresses
|
|
172
|
+
recovery only before the recorded nextEligibleCheck or while same-key wake
|
|
173
|
+
delivery is queued or running in the owner thread. An armed unfired watch or
|
|
174
|
+
schedule, or one unable to observe the named blocker, never suppresses eligible
|
|
175
|
+
recovery. Unsupported events permit checking only the named external condition
|
|
176
|
+
under the recorded canary and activation conditions.
|
|
177
|
+
|
|
178
|
+
Send one bounded continuation with its condition key to the exact existing
|
|
179
|
+
top-level owning Coordinator/Driver. Revalidate all guards, owner history,
|
|
180
|
+
queue and deduplication receipts immediately before delivery. Include structured
|
|
181
|
+
evidence references by URL/ID/SHA and a recovery summary, with the recorded next
|
|
182
|
+
action: “Revalidate; act only within your existing authorized next action.”
|
|
183
|
+
Exclude quoted third-party/tool/webhook text. A recovery continuation is never
|
|
184
|
+
a user reply, answer, approval or authorization. The receiving owner independently
|
|
185
|
+
revalidates evidence and retains its original authority and holds. Recovery
|
|
186
|
+
never answers pending questions or performs forge mutations, delegation,
|
|
187
|
+
thread creation/settlement/cancel/interrupt, settings changes, or other owners'
|
|
188
|
+
queue/watch/schedule edits. Its writes are bounded continuations, own-thread
|
|
189
|
+
receipts and only its separately authorized schedule/duplicate-message cleanup.
|
|
190
|
+
Cross-host sends remain report-only until installed C4 instructions and current
|
|
191
|
+
directed C5 write-link proof exist; never redirect or relay outside native T3.
|
|
192
|
+
|
|
74
193
|
## Role dispatch by permitted writes
|
|
75
194
|
|
|
76
195
|
| Roles | Mechanism and permitted workspace | Completion |
|
|
@@ -183,6 +302,13 @@ After each delegated completion the driver must check its own `HEAD` and
|
|
|
183
302
|
`git status --porcelain` against their pre-dispatch values: both stay unchanged,
|
|
184
303
|
including untracked entries. Any change is a hold before advancing that work.
|
|
185
304
|
|
|
305
|
+
Native delegate completion owns the wake for `delegate_task` children. Never
|
|
306
|
+
status-check running delegate_task children on scheduled wakes. Never create a
|
|
307
|
+
schedule merely because delegate_task children exist. A lost completion wake
|
|
308
|
+
remains active/unknown in read-only status, rather than triggering polling,
|
|
309
|
+
silence-based cancellation or a replacement author. Native completion receipts
|
|
310
|
+
are reconciled on delivery, not sought through hourly status checks.
|
|
311
|
+
|
|
186
312
|
## Launch recovery, repairs and questions
|
|
187
313
|
|
|
188
314
|
For a lost delegated dispatch or repair response, retry with the same stable
|
|
@@ -233,12 +359,13 @@ A replacement after terminal failure must use `a<n+1>`, a new branch and title;
|
|
|
233
359
|
the failed attempt's branch is kept until salvage. Unknown liveness holds
|
|
234
360
|
replacement; reconcile the old writer before admitting another.
|
|
235
361
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
`
|
|
362
|
+
The bound lifecycle rule below applies only to owned launched-fallback Writers
|
|
363
|
+
awaiting completion. With an unsettled launched thread, the driver turn must end only while a bound
|
|
364
|
+
`schedule_task` with `bindToCurrentThread:true` is armed at the 5-minute lifecycle
|
|
365
|
+
cadence `everyMs:300000` under
|
|
239
366
|
[Chat-run watch runtime](../../axstack-watch/references/watch-runtime.md#chat-run-watch),
|
|
240
|
-
with its ID recorded. Each wake must reconcile
|
|
241
|
-
writer that died without sending; failed runs hold incomplete work. The watch
|
|
367
|
+
with its ID recorded. Each wake must reconcile unsettled launched-fallback Writer
|
|
368
|
+
runs, including a writer that died without sending; failed runs hold incomplete work. The watch
|
|
242
369
|
inherits the driver model/workspace and adds no runtime of Axstack's own.
|
|
243
370
|
T3 queues a scheduled wake even while the driver is busy, so copies pile up.
|
|
244
371
|
After a provider error, server restart or Stop, T3 holds the queue: held
|
|
@@ -324,7 +451,7 @@ An open PR left unwatched by a user Stop never triggers the 5-minute recovery ca
|
|
|
324
451
|
After T3 auto-settles a multi-PR driver on one PR merge, it re-arms remaining
|
|
325
452
|
open members through the recovery steps before ending the turn.
|
|
326
453
|
If rate-limited, use the 5-minute schedule and next-wake recovery above.
|
|
327
|
-
The schedule
|
|
454
|
+
The schedule reconciles delivered receipts, readiness and release, plus launched-fallback Writer runs.
|
|
328
455
|
When a PR merges or closes, or its watch is torn down, call `unwatch_pull_request`
|
|
329
456
|
and keep its link.
|
|
330
457
|
|
|
@@ -409,3 +536,43 @@ For own PRs, automatic merge is the default under the
|
|
|
409
536
|
[watch predicate](../../axstack-watch/SKILL.md#5-state-readiness-precisely).
|
|
410
537
|
The recorded owning watch thread merges; a missing or idle owner requires
|
|
411
538
|
lifecycle reconciliation and explicit transfer first.
|
|
539
|
+
|
|
540
|
+
## Fleet nonwriter boundary (C3a)
|
|
541
|
+
|
|
542
|
+
Before dispatch, the driver must capture capability, project, settings and access
|
|
543
|
+
through native forwarded APIs on the selected destination, following
|
|
544
|
+
[Fleet dispatch and receipts](fleet-dispatch.md). Origin catalogs and caller
|
|
545
|
+
assertions alone cannot establish destination readiness. Preserve exact catalog
|
|
546
|
+
bytes/digest, explicit observation/evaluation times, config digest and recommendation.
|
|
547
|
+
|
|
548
|
+
`t3_thread_configuration` is currently unavailable in the advertised tools.
|
|
549
|
+
Unavailable effective fields remain unknown; the driver must not invent an API,
|
|
550
|
+
readiness or effective settings. Use advertised dispatch/thread facts, keep requested
|
|
551
|
+
and effective values separate, and hold any required unobserved field.
|
|
552
|
+
|
|
553
|
+
For ambiguous submission, reconcile the exact clientRequestId/attempt and remote task
|
|
554
|
+
before any retry. Unknown liveness prohibits replacement. A confirmed not-submitted
|
|
555
|
+
failure may exclude that route and recompute from current facts; it never permits
|
|
556
|
+
model or permission substitution. No hidden SSH agent or new top-level thread is
|
|
557
|
+
an execution fallback; a new thread requires an explicit user request.
|
|
558
|
+
|
|
559
|
+
The driver must read task_status on the origin and terminal/thread facts on the
|
|
560
|
+
actual destination for the latest run. Pending children or an unanswered question
|
|
561
|
+
prevent success even after a completed turn. Read the native pending-question list
|
|
562
|
+
with `t3_pending_request` action `list` on that destination; the older
|
|
563
|
+
`t3_pending_request_list` spelling above is not an advertised callable tool.
|
|
564
|
+
A null local childThreadId, idle state, silence or old summary proves no completion.
|
|
565
|
+
|
|
566
|
+
The remote author coordinator remains DISABLED: C3b's all-refs-unchanged pilot
|
|
567
|
+
failed because of T3 checkpoint refs. Drivers must not enable or rerun the pilot
|
|
568
|
+
under C3a. Direct remote worktree authors remain ineligible on T3 v0.1.5;
|
|
569
|
+
local isolated authors remain the fallback even when overloaded. C4 personal
|
|
570
|
+
policy/Haoshoku changes and C6 deployment/live acceptance remain later phases.
|
|
571
|
+
|
|
572
|
+
Remote nonwriters use native `delegate_task` with exact selected target.environmentId,
|
|
573
|
+
projectId, providerInstanceId, model, target.options and runtimeMode. Remote reviewers
|
|
574
|
+
use T3 `isolation:"worktree"`; never make their child checkouts in the shell.
|
|
575
|
+
Use no raw agent CLI over SSH. SSH is restricted to telemetry and authorized
|
|
576
|
+
artifact/commit transport. Follow host-qualified archive/readback/retirement in
|
|
577
|
+
[Fleet dispatch and receipts](fleet-dispatch.md#retrieval-and-retirement).
|
|
578
|
+
Only the driver publishes, links, watches or merges PRs.
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Workspace hygiene for driver-owned T3 runs
|
|
2
2
|
|
|
3
|
+
For remote nonwriters, apply [host-qualified retrieval and retirement](fleet-dispatch.md#retrieval-and-retirement).
|
|
4
|
+
Resolve task status on origin and thread/run/settlement on the actual destination.
|
|
5
|
+
Archive and read back exact evidence on origin before retiring only the owned
|
|
6
|
+
T3-created reviewer checkout. Bind host/environment/path and candidate/base SHA;
|
|
7
|
+
never use an origin filesystem path as a destination path. Unknown liveness,
|
|
8
|
+
ownership or user takeover preserves that resource. Validation precedes deletion
|
|
9
|
+
in separate sequential calls; preserve unknown ignored artifacts.
|
|
10
|
+
|
|
3
11
|
This is a prompt contract, not a cleanup daemon. Follow [T3 runtime](t3-runtime.md)
|
|
4
12
|
for native identities, terminal run evidence and schema. Dispatched workers never
|
|
5
13
|
sweep or remove another session. A scheduled pass with recorded cleanup authority
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { inspectFleetReceipt } from './fleet-receipt.js';
|
|
2
3
|
import { bindFleet } from './fleet-dispatch.js';
|
|
3
4
|
|
|
4
5
|
const fleetFlags = ['--recommendation', '--fleet-input', '--now', '--config-digest'];
|
|
@@ -32,6 +33,9 @@ Fleet --capabilities is the selected destination's raw saved catalog, digest-bou
|
|
|
32
33
|
by its destination facts. It is never the origin catalog by default.
|
|
33
34
|
Fleet verification requires environmentId/projectId and fleetBinding alongside
|
|
34
35
|
the effective T3 configuration. C3 collects that host-qualified evidence envelope.
|
|
36
|
+
Optional input.nativeDestination validates saved native forwarded reads.
|
|
37
|
+
Optional --verify fleetAttempt/fleetReceipt validates host-qualified terminal facts;
|
|
38
|
+
--out retains per-term receipt evidence. Synthetic bindings cannot authenticate reads.
|
|
35
39
|
Coordinator routes remain unsupported here. A plan grants no launch authority.
|
|
36
40
|
Exit codes: 0 ok, 1 error, 2 hold (including incomplete read-back).
|
|
37
41
|
`;
|
|
@@ -95,7 +99,15 @@ function verify(configuration, provider, model, options, fleet) {
|
|
|
95
99
|
for (const [field, expected] of Object.entries(fleet.destination)) compare(field, expected, configuration?.[field]);
|
|
96
100
|
for (const [field, expected] of Object.entries(fleet.fleetBinding)) compare(field, expected, configuration?.fleetBinding?.[field]);
|
|
97
101
|
}
|
|
98
|
-
|
|
102
|
+
let receiptTerms;
|
|
103
|
+
if (fleet && configuration?.fleetReceipt !== undefined) {
|
|
104
|
+
receiptTerms = inspectFleetReceipt(configuration.fleetReceipt, {
|
|
105
|
+
attempt: configuration.fleetAttempt, recommendation: fleet.recommendation,
|
|
106
|
+
recommendationJSON: fleet.recommendationJSON, now: fleet.now,
|
|
107
|
+
});
|
|
108
|
+
for (const term of receiptTerms) if (term.state !== 'pass') mismatches.push(`${term.code}: ${term.state}; evidence ${JSON.stringify(term.evidence)}`);
|
|
109
|
+
}
|
|
110
|
+
return { mismatches, ...(receiptTerms ? { receiptTerms } : {}), ...(selection?.provider == null ? {
|
|
99
111
|
providerEvidence: `provider: unknown; expected ${JSON.stringify(provider)}; observed missing; `
|
|
100
112
|
+ 'provider binding checked via instanceId and saved capabilities',
|
|
101
113
|
} : {}) };
|
|
@@ -159,9 +171,14 @@ function plan(argv, io) {
|
|
|
159
171
|
let fleet;
|
|
160
172
|
if (args['--recommendation']) {
|
|
161
173
|
if (!/^\d+$/.test(args['--now'])) throw new Error('evaluation-time');
|
|
162
|
-
|
|
174
|
+
const recommendationJSON = readFileSync(args['--recommendation'], 'utf8');
|
|
175
|
+
const recommendation = JSON.parse(recommendationJSON);
|
|
176
|
+
fleet = bindFleet(readJSON(args['--fleet-input']), recommendation,
|
|
163
177
|
args['--config-digest'], Number(args['--now']), readFileSync(args['--capabilities'], 'utf8'),
|
|
164
178
|
resolution, role.id === 'axstack-author');
|
|
179
|
+
fleet.recommendation = recommendation;
|
|
180
|
+
fleet.recommendationJSON = recommendationJSON;
|
|
181
|
+
fleet.now = Number(args['--now']);
|
|
165
182
|
}
|
|
166
183
|
const options = fleet?.options ?? (effortOption ? [effortOption] : []);
|
|
167
184
|
if (args['--verify']) {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Validate explicit saved native reads. These bindings do not authenticate input.
|
|
2
|
+
import { canonical, text, time } from './fleet-config.js';
|
|
3
|
+
|
|
4
|
+
export function inspectDestination(observation, recommendation, capabilitiesText, now) {
|
|
5
|
+
const terms = [];
|
|
6
|
+
const term = (code, pass, evidence, missing = evidence == null) => terms.push({ code,
|
|
7
|
+
state: pass ? 'pass' : missing ? 'unknown' : 'fail', evidence: evidence ?? null });
|
|
8
|
+
const s = recommendation.selected;
|
|
9
|
+
const o = observation ?? {};
|
|
10
|
+
for (const [name, tool, environmentId, args] of [
|
|
11
|
+
['environment', 't3_environment_read', s.environmentId, {}],
|
|
12
|
+
['project', 't3_project_read', s.environmentId, { projectId: s.projectId }],
|
|
13
|
+
['capabilities', 'orchestrator_capabilities', s.environmentId, {}],
|
|
14
|
+
['links', 't3_environment_links', recommendation.originEnvironmentId, {}],
|
|
15
|
+
]) {
|
|
16
|
+
const read = o[name];
|
|
17
|
+
term('destination-observation', read?.tool === tool && read?.environmentId === environmentId
|
|
18
|
+
&& canonical(read?.args) === canonical(args) && time(read?.observedAt)
|
|
19
|
+
&& read.observedAt <= now && text(read?.evidence), read);
|
|
20
|
+
}
|
|
21
|
+
term('destination-identity', o.environment?.value?.environmentId === s.environmentId,
|
|
22
|
+
o.environment?.value?.environmentId);
|
|
23
|
+
term('destination-project', o.project?.value?.id === s.projectId && text(o.project?.value?.workspaceRoot),
|
|
24
|
+
o.project?.value);
|
|
25
|
+
term('destination-repository', o.project?.value?.repositoryIdentity?.canonicalKey === s.repositoryId,
|
|
26
|
+
o.project?.value?.repositoryIdentity);
|
|
27
|
+
term('destination-catalog', o.capabilities?.valueText === capabilitiesText, o.capabilities?.valueText);
|
|
28
|
+
const catalog = JSON.parse(capabilitiesText);
|
|
29
|
+
const provider = catalog.providers?.find((v) => v.providerInstanceId === recommendation.request.providerInstanceId);
|
|
30
|
+
term('destination-execution', provider?.canRunChildTask === true
|
|
31
|
+
&& provider?.runtimeModes?.includes(recommendation.request.runtimeMode), provider,
|
|
32
|
+
provider?.canRunChildTask == null || !Array.isArray(provider?.runtimeModes));
|
|
33
|
+
const local = s.environmentId === recommendation.originEnvironmentId;
|
|
34
|
+
const link = o.links?.value?.links?.find((v) => v.environmentId === s.environmentId);
|
|
35
|
+
term('destination-access', o.links?.value?.environmentId === recommendation.originEnvironmentId
|
|
36
|
+
&& (local || (link?.status === 'reachable' && link?.access === recommendation.request.runtimeMode
|
|
37
|
+
&& Date.parse(link?.expiresAt) > now)), local ? o.links?.value : link);
|
|
38
|
+
return terms;
|
|
39
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// Bind supplied placement facts to a plan. No reads, dispatch or authority grant.
|
|
2
|
+
import { inspectDestination } from './fleet-destination.js';
|
|
2
3
|
import { canonical, record, text } from './fleet-config.js';
|
|
3
4
|
import { recommend } from './fleet-route.js';
|
|
4
5
|
|
|
@@ -16,6 +17,11 @@ export function bindFleet(input, saved, configDigest, now, capabilitiesText, res
|
|
|
16
17
|
const digest = selected.destinationCapabilitiesDigest;
|
|
17
18
|
if (typeof digest !== 'string' || !/^[a-f0-9]{64}$/.test(digest)) hold('capabilities-digest-missing');
|
|
18
19
|
if (new Bun.CryptoHasher('sha256').update(capabilitiesText).digest('hex') !== digest) hold('capabilities-digest-mismatch');
|
|
20
|
+
if (input.nativeDestination !== undefined) {
|
|
21
|
+
const mismatch = inspectDestination(input.nativeDestination, current, capabilitiesText, now)
|
|
22
|
+
.find((term) => term.state !== 'pass');
|
|
23
|
+
if (mismatch) hold(mismatch.code);
|
|
24
|
+
}
|
|
19
25
|
const request = current.request;
|
|
20
26
|
const { model, providerInstanceId, effortOption } = resolved;
|
|
21
27
|
if (request.providerInstanceId !== providerInstanceId || request.model !== model
|
|
@@ -1,10 +1,21 @@
|
|
|
1
|
+
import { nativeAssignments } from './fleet-inventory.js';
|
|
1
2
|
import { canonical, evidence, fail, record, text, time } from './fleet-config.js';
|
|
2
3
|
|
|
3
4
|
const terminal = new Set(['completed', 'failed', 'cancelled', 'canceled', 'rejected']);
|
|
4
5
|
const states = new Set([...terminal, 'pending', 'queued', 'running', 'waiting', 'blocked']);
|
|
5
6
|
const qualified = (value) => text(value) && /^.+:.+$/.test(value);
|
|
6
7
|
|
|
7
|
-
export function assignmentCounts(input, hosts) {
|
|
8
|
+
export function assignmentCounts(input, hosts, now) {
|
|
9
|
+
if (input.nativeInventory !== undefined) {
|
|
10
|
+
const normalized = nativeAssignments(input.nativeInventory, hosts, now);
|
|
11
|
+
const counts = assignmentCounts(normalized, hosts);
|
|
12
|
+
return { ...counts, inventoryReasons: normalized.inventoryReasons,
|
|
13
|
+
inventoryEvidence: normalized.inventoryEvidence,
|
|
14
|
+
...(normalized.inventoryStatus === 'unknown' ? {
|
|
15
|
+
pendingLowerBound: counts.byEnvironment,
|
|
16
|
+
byEnvironment: Object.fromEntries(hosts.map((h) => [h.environmentId, null])),
|
|
17
|
+
} : {}) };
|
|
18
|
+
}
|
|
8
19
|
const byEnvironment = Object.fromEntries(hosts.map((h) => [h.environmentId, 0]));
|
|
9
20
|
const inventory = new Map();
|
|
10
21
|
const clients = new Map();
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Normalize saved native observations only. No discovery, reservations or dispatch.
|
|
2
|
+
import { canonical, text, time } from './fleet-config.js';
|
|
3
|
+
|
|
4
|
+
const active = new Set(['queued', 'running', 'waiting']);
|
|
5
|
+
const terminal = new Set(['completed', 'failed', 'cancelled', 'interrupted']);
|
|
6
|
+
// JSON tuple encoding avoids collisions between IDs containing punctuation.
|
|
7
|
+
const identity = (environmentId, taskId) => JSON.stringify([environmentId, taskId]);
|
|
8
|
+
|
|
9
|
+
export function nativeAssignments(snapshot, hosts, now) {
|
|
10
|
+
const reasons = new Set();
|
|
11
|
+
const unknown = (code) => reasons.add(code);
|
|
12
|
+
const environments = new Set(hosts.map((h) => h.environmentId));
|
|
13
|
+
const tasks = new Map();
|
|
14
|
+
const assignments = new Map();
|
|
15
|
+
const pending = new Map();
|
|
16
|
+
if (snapshot?.complete !== true || !time(snapshot?.observedAt) || !time(now) || snapshot.observedAt > now || !text(snapshot?.evidence)
|
|
17
|
+
|| !Array.isArray(snapshot?.tasks) || !Array.isArray(snapshot?.pending)) unknown('inventory-incomplete');
|
|
18
|
+
for (const item of Array.isArray(snapshot?.tasks) ? snapshot.tasks : []) {
|
|
19
|
+
if (!text(item?.origin?.environmentId) || !text(item?.origin?.threadId) || !text(item?.task?.taskId)) {
|
|
20
|
+
unknown('origin-unknown'); continue;
|
|
21
|
+
}
|
|
22
|
+
const key = identity(item.origin.environmentId, item.task.taskId);
|
|
23
|
+
const previous = tasks.get(key);
|
|
24
|
+
if (previous && canonical(previous) !== canonical(item)) unknown('assignment-conflict');
|
|
25
|
+
tasks.set(key, item);
|
|
26
|
+
}
|
|
27
|
+
for (const [key, item] of tasks) {
|
|
28
|
+
const task = item.task;
|
|
29
|
+
const environmentId = task.remoteChild?.environmentId
|
|
30
|
+
?? (text(task.childThreadId) ? item.origin.environmentId : null);
|
|
31
|
+
if (!text(environmentId)) { unknown('destination-unknown'); continue; }
|
|
32
|
+
if (!environments.has(environmentId)) { unknown('host-unmapped'); continue; }
|
|
33
|
+
if (task.remoteChild && !text(task.remoteChild.threadId)) { unknown('destination-unknown'); continue; }
|
|
34
|
+
if (!active.has(task.status) && !terminal.has(task.status)) { unknown('task-state-unknown'); continue; }
|
|
35
|
+
let rootKey = key;
|
|
36
|
+
if (item.assignmentRoot) {
|
|
37
|
+
rootKey = identity(item.assignmentRoot.environmentId, item.assignmentRoot.taskId);
|
|
38
|
+
const root = tasks.get(rootKey);
|
|
39
|
+
if (!root || root.assignmentRoot) { unknown('assignment-root-unknown'); continue; }
|
|
40
|
+
const rootEnvironment = root.task.remoteChild?.environmentId
|
|
41
|
+
?? (text(root.task.childThreadId) ? root.origin.environmentId : null);
|
|
42
|
+
if (rootEnvironment !== environmentId) { unknown('assignment-conflict'); continue; }
|
|
43
|
+
}
|
|
44
|
+
const previous = assignments.get(rootKey);
|
|
45
|
+
const state = active.has(task.status) || task.hasPendingChildRuns === true
|
|
46
|
+
|| task.workState === 'waiting_for_children' ? 'running' : 'completed';
|
|
47
|
+
// A terminal coordinator with live descendants remains active.
|
|
48
|
+
assignments.set(rootKey, { assignmentId: `task:${rootKey}`, environmentId,
|
|
49
|
+
state: previous?.state === 'running' ? 'running' : state });
|
|
50
|
+
}
|
|
51
|
+
for (const item of Array.isArray(snapshot?.pending) ? snapshot.pending : []) {
|
|
52
|
+
if (!text(item?.origin?.environmentId) || !text(item?.origin?.threadId) || !text(item?.clientRequestId)) {
|
|
53
|
+
unknown('origin-unknown'); continue;
|
|
54
|
+
}
|
|
55
|
+
if (!environments.has(item.environmentId)) { unknown('host-unmapped'); continue; }
|
|
56
|
+
const key = JSON.stringify([item.origin.environmentId, item.origin.threadId, item.clientRequestId]);
|
|
57
|
+
const value = { clientRequestId: key, environmentId: item.environmentId };
|
|
58
|
+
if (item.taskId !== undefined) {
|
|
59
|
+
if (!text(item.taskId)) { unknown('origin-unknown'); continue; }
|
|
60
|
+
const taskKey = identity(item.origin.environmentId, item.taskId);
|
|
61
|
+
const known = tasks.get(taskKey);
|
|
62
|
+
const rootKey = known?.assignmentRoot
|
|
63
|
+
? identity(known.assignmentRoot.environmentId, known.assignmentRoot.taskId) : taskKey;
|
|
64
|
+
value.taskId = `task:${rootKey}`;
|
|
65
|
+
const reflected = assignments.get(rootKey);
|
|
66
|
+
if (reflected && reflected.environmentId !== item.environmentId) unknown('assignment-conflict');
|
|
67
|
+
}
|
|
68
|
+
const previous = pending.get(key);
|
|
69
|
+
if (previous && canonical(previous) !== canonical(value)) unknown('assignment-conflict');
|
|
70
|
+
pending.set(key, value);
|
|
71
|
+
}
|
|
72
|
+
return { inventoryStatus: reasons.size ? 'unknown' : 'known',
|
|
73
|
+
inventoryObservedAt: snapshot?.observedAt ?? null, inventoryReasons: [...reasons],
|
|
74
|
+
inventoryEvidence: snapshot?.evidence ?? null,
|
|
75
|
+
assignments: [...assignments.values()], pendingAssignments: [...pending.values()] };
|
|
76
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Saved evidence validator. A pass binds supplied facts; it cannot authenticate them.
|
|
2
|
+
import { canonical, record, text, time } from './fleet-config.js';
|
|
3
|
+
|
|
4
|
+
function options(value) {
|
|
5
|
+
if (!Array.isArray(value)) return record(value) ? value : null;
|
|
6
|
+
if (value.some((v) => !text(v?.id) || !['string', 'boolean'].includes(typeof v.value))
|
|
7
|
+
|| new Set(value.map((v) => v.id)).size !== value.length) return null;
|
|
8
|
+
return Object.fromEntries(value.map((v) => [v.id, v.value]));
|
|
9
|
+
}
|
|
10
|
+
const same = (a, b) => a !== undefined && b !== undefined && canonical(a) === canonical(b);
|
|
11
|
+
const absolute = (path) => text(path) && path.startsWith('/') && !path.split('/').some((p) => p === '..' || p === '.');
|
|
12
|
+
|
|
13
|
+
export function inspectFleetReceipt(receipt, expected) {
|
|
14
|
+
const terms = [];
|
|
15
|
+
const term = (code, pass, observed, missing = observed === undefined || observed === null) => {
|
|
16
|
+
terms.push({ code, state: pass ? 'pass' : missing ? 'unknown' : 'fail', evidence: observed ?? null });
|
|
17
|
+
};
|
|
18
|
+
const r = receipt ?? {};
|
|
19
|
+
term('receipt-version', r.schemaVersion === 1, r.schemaVersion);
|
|
20
|
+
const a = expected.attempt;
|
|
21
|
+
const s = expected.recommendation.selected;
|
|
22
|
+
const request = expected.recommendation.request;
|
|
23
|
+
term('receipt-origin', text(a?.origin?.threadId) && a?.origin?.environmentId === expected.recommendation.originEnvironmentId
|
|
24
|
+
&& same(r.attempt?.origin, a?.origin), r.attempt?.origin,
|
|
25
|
+
!text(a?.origin?.threadId) || r.attempt?.origin == null);
|
|
26
|
+
term('receipt-attempt', text(a?.key) && text(a?.clientRequestId) && text(a?.origin?.taskId)
|
|
27
|
+
&& same(r.attempt, a), r.attempt);
|
|
28
|
+
term('receipt-recommendation', r.recommendationJSON === expected.recommendationJSON, r.recommendationJSON);
|
|
29
|
+
term('receipt-config', r.configDigest === expected.recommendation.configDigest, r.configDigest);
|
|
30
|
+
const status = r.status?.value;
|
|
31
|
+
const submitted = r.submission;
|
|
32
|
+
const thread = r.thread?.value?.thread;
|
|
33
|
+
const remote = s.environmentId !== expected.recommendation.originEnvironmentId;
|
|
34
|
+
const child = (v) => remote ? v?.remoteChild?.threadId : v?.childThreadId;
|
|
35
|
+
const env = (v) => remote ? v?.remoteChild?.environmentId : a?.origin?.environmentId;
|
|
36
|
+
term('receipt-destination', env(status) === s.environmentId && env(submitted) === s.environmentId
|
|
37
|
+
&& text(child(status)) && child(status) === child(submitted)
|
|
38
|
+
&& r.thread?.environmentId === s.environmentId && thread?.threadId === child(status),
|
|
39
|
+
{ submitted: submitted?.remoteChild ?? submitted?.childThreadId,
|
|
40
|
+
status: status?.remoteChild ?? status?.childThreadId, thread: r.thread?.environmentId });
|
|
41
|
+
term('receipt-title', text(a?.title) && thread?.title === a.title, thread?.title);
|
|
42
|
+
term('receipt-project', thread?.projectId === s.projectId, thread?.projectId);
|
|
43
|
+
for (const [name, tool, environmentId, args] of [
|
|
44
|
+
['status', 'task_status', a?.origin?.environmentId, { taskId: a?.origin?.taskId }],
|
|
45
|
+
['thread', 't3_thread_read', s.environmentId, { threadId: child(status) }],
|
|
46
|
+
['terminal', 't3_thread_wait', s.environmentId, { threadId: child(status), runId: thread?.latestRunId }],
|
|
47
|
+
]) {
|
|
48
|
+
const read = r[name];
|
|
49
|
+
term('receipt-observation', read?.tool === tool && read?.environmentId === environmentId
|
|
50
|
+
&& same(read?.args, args) && time(read?.observedAt) && read.observedAt <= expected.now
|
|
51
|
+
&& text(read?.evidence), read);
|
|
52
|
+
}
|
|
53
|
+
term('receipt-origin', status?.taskId === a?.origin?.taskId && submitted?.taskId === a?.origin?.taskId,
|
|
54
|
+
status?.taskId);
|
|
55
|
+
const target = r.requested?.target;
|
|
56
|
+
term('receipt-requested', target?.environmentId === s.environmentId && target?.projectId === s.projectId
|
|
57
|
+
&& target?.providerInstanceId === request.providerInstanceId && target?.model === request.model
|
|
58
|
+
&& same(options(target?.options), request.options) && r.requested?.runtimeMode === request.runtimeMode,
|
|
59
|
+
r.requested);
|
|
60
|
+
term('receipt-effective', status?.providerInstanceId === request.providerInstanceId
|
|
61
|
+
&& status?.model === request.model && thread?.providerInstanceId === request.providerInstanceId
|
|
62
|
+
&& thread?.model === request.model && same(options(status?.options), request.options)
|
|
63
|
+
&& thread?.runtimeMode === request.runtimeMode, { provider: status?.providerInstanceId,
|
|
64
|
+
model: status?.model, options: status?.options ?? null, runtimeMode: thread?.runtimeMode },
|
|
65
|
+
status?.options == null || thread?.runtimeMode == null);
|
|
66
|
+
const terminal = r.terminal?.value;
|
|
67
|
+
term('receipt-terminal', status?.status === 'completed' && status?.latestTerminalStatus === 'completed'
|
|
68
|
+
&& status?.workState === 'result_available' && status?.hasPendingChildRuns === false
|
|
69
|
+
&& ['completed', 'idle'].includes(thread?.status) && thread?.activeRunId === null
|
|
70
|
+
&& terminal?.status === 'completed' && terminal?.timedOut === false, { status, terminal },
|
|
71
|
+
status?.status == null || status?.workState == null || status?.hasPendingChildRuns == null
|
|
72
|
+
|| status?.latestTerminalStatus == null || terminal?.status == null || terminal?.timedOut == null
|
|
73
|
+
|| thread?.activeRunId === undefined);
|
|
74
|
+
term('receipt-question', status?.hasPendingQuestion === false && status?.pendingApproval === null
|
|
75
|
+
&& thread?.pendingRequestCount === 0, { hasPendingQuestion: status?.hasPendingQuestion,
|
|
76
|
+
pendingApproval: status?.pendingApproval, pendingRequestCount: thread?.pendingRequestCount },
|
|
77
|
+
status?.hasPendingQuestion == null || status?.pendingApproval === undefined || thread?.pendingRequestCount == null);
|
|
78
|
+
const questions = r.questions;
|
|
79
|
+
term('receipt-question', questions?.tool === 't3_pending_request'
|
|
80
|
+
&& questions?.environmentId === s.environmentId
|
|
81
|
+
&& same(questions?.args, { action: 'list', threadId: child(status) })
|
|
82
|
+
&& time(questions?.observedAt) && questions.observedAt <= expected.now
|
|
83
|
+
&& text(questions?.evidence) && Array.isArray(questions?.value?.requests)
|
|
84
|
+
&& questions.value.requests.length === 0, questions,
|
|
85
|
+
questions == null || questions.value?.requests === undefined);
|
|
86
|
+
const current = thread?.latestRunId;
|
|
87
|
+
const runs = r.thread?.value?.recentRuns;
|
|
88
|
+
const latest = Array.isArray(runs) ? runs.find((v) => v.runId === current) : null;
|
|
89
|
+
term('receipt-run', text(current) && status?.latestTerminalRunId === current
|
|
90
|
+
&& terminal?.threadId === child(status) && terminal?.runId === current
|
|
91
|
+
&& (!terminal?.resultRun || (terminal.resultRun.runId === current && terminal.resultRun.status === 'completed'))
|
|
92
|
+
&& (!remote || (r.remoteChild?.environmentId === s.environmentId
|
|
93
|
+
&& r.remoteChild?.threadId === child(status) && r.remoteChild?.runId === current))
|
|
94
|
+
&& latest?.status === 'completed' && Number.isSafeInteger(latest?.ordinal)
|
|
95
|
+
&& runs.every((v) => Number.isSafeInteger(v.ordinal) && v.ordinal <= latest.ordinal),
|
|
96
|
+
{ current, originTerminal: status?.latestTerminalRunId, terminal, runs },
|
|
97
|
+
!current || !status?.latestTerminalRunId || !terminal?.runId || !Array.isArray(runs)
|
|
98
|
+
|| (remote && !r.remoteChild?.runId));
|
|
99
|
+
const marker = `AXSTACK-DONE key=${a?.key} head=${a?.candidate} report=${r.evidence?.path}`;
|
|
100
|
+
const finalMarker = (value) => {
|
|
101
|
+
if (typeof value !== 'string') return null;
|
|
102
|
+
const lines = value.trim().split('\n');
|
|
103
|
+
const markers = lines.filter((line) => line.startsWith('AXSTACK-'));
|
|
104
|
+
if (markers.length !== 1) return null;
|
|
105
|
+
const trailer = lines.slice(lines.indexOf(markers[0]) + 1);
|
|
106
|
+
return trailer.length === 0 || (trailer.length === 1
|
|
107
|
+
&& /^PR https?:\/\/\S+, head [a-f0-9]{7,40}, checks and reviews .+$/.test(trailer[0]))
|
|
108
|
+
? markers[0] : null;
|
|
109
|
+
};
|
|
110
|
+
const items = r.thread?.value?.items;
|
|
111
|
+
const final = Array.isArray(items) ? items.filter((v) => v.runId === current && v.createdBy === 'agent').at(-1) : null;
|
|
112
|
+
term('receipt-marker', /^[a-f0-9]{40}$/.test(a?.candidate ?? '')
|
|
113
|
+
&& finalMarker(status?.latestTerminalSummary) === marker && finalMarker(final?.text) === marker,
|
|
114
|
+
{ origin: status?.latestTerminalSummary, destination: final?.text });
|
|
115
|
+
term('receipt-host-path', r.evidence?.hostId === s.hostId && r.evidence?.environmentId === s.environmentId
|
|
116
|
+
&& absolute(r.evidence?.path) && finalMarker(status?.latestTerminalSummary)?.endsWith(` report=${r.evidence.path}`), r.evidence);
|
|
117
|
+
if (a?.title?.startsWith('Reviewer: ')) {
|
|
118
|
+
term('receipt-isolation', r.requested?.isolation === 'worktree', r.requested?.isolation);
|
|
119
|
+
}
|
|
120
|
+
if (a?.title?.startsWith('Reviewer: ') || r.checkout != null || thread?.worktreePath != null || r.requested?.isolation === 'worktree') {
|
|
121
|
+
const c = r.checkout;
|
|
122
|
+
term('receipt-checkout', c?.hostId === s.hostId && c?.environmentId === s.environmentId
|
|
123
|
+
&& absolute(c?.path) && c.path === thread?.worktreePath
|
|
124
|
+
&& /^[a-f0-9]{40}$/.test(a?.base ?? '') && c?.base === a.base && c?.candidate === a.candidate
|
|
125
|
+
&& c?.readback?.hostId === s.hostId && c?.readback?.environmentId === s.environmentId
|
|
126
|
+
&& c?.readback?.path === c.path && c?.readback?.base === a.base && c?.readback?.candidate === a.candidate
|
|
127
|
+
&& text(c?.readback?.evidence), c);
|
|
128
|
+
}
|
|
129
|
+
return terms;
|
|
130
|
+
}
|
|
@@ -6,7 +6,7 @@ export { parseFleetConfig } from './fleet-config.js';
|
|
|
6
6
|
export function recommend(input, now) {
|
|
7
7
|
validateInput(input, now);
|
|
8
8
|
const config = parseFleetConfig(input.configText, input.configDigest);
|
|
9
|
-
const counts = assignmentCounts(input, config.hosts);
|
|
9
|
+
const counts = assignmentCounts(input, config.hosts, now);
|
|
10
10
|
const preference = Object.hasOwn(config.taskOrders, input.request.task)
|
|
11
11
|
? config.taskOrders[input.request.task] : config.defaultOrder;
|
|
12
12
|
const eligible = [];
|
|
@@ -46,12 +46,13 @@ export function recommend(input, now) {
|
|
|
46
46
|
const measured = (h) => h.telemetry.status === 'measured';
|
|
47
47
|
const rank = (h) => preference.indexOf(h.hostId);
|
|
48
48
|
const byLoad = (a, b) => a.telemetry.loadBp - b.telemetry.loadBp || rank(a) - rank(b);
|
|
49
|
+
const rankingCounts = counts.pendingLowerBound ?? counts.byEnvironment;
|
|
49
50
|
const under = pool.filter((h) => measured(h) && h.telemetry.loadBp <= config.balanceLimitBp);
|
|
50
51
|
let stage; let reason; let selected = null;
|
|
51
52
|
if (!eligible.length) { stage = 'no-route'; reason = 'no-executable-route'; }
|
|
52
53
|
else if (under.length) {
|
|
53
54
|
stage = 'balance'; reason = 'pool-under-limit';
|
|
54
|
-
selected = under.sort((a, b) => a.
|
|
55
|
+
selected = under.sort((a, b) => rankingCounts[a.environmentId] - rankingCounts[b.environmentId] || byLoad(a, b))[0];
|
|
55
56
|
} else if (pool.length && pool.every((h) => !measured(h))) {
|
|
56
57
|
stage = 'pool-unknown'; reason = 'pool-all-unknown'; selected = pool[0];
|
|
57
58
|
} else {
|
|
@@ -65,7 +66,7 @@ export function recommend(input, now) {
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
const flags = {
|
|
68
|
-
countsUnknown:
|
|
69
|
+
countsUnknown: counts.inventoryStatus === 'unknown', poolEmpty: pool.length === 0,
|
|
69
70
|
poolPartlyUnknown: pool.some(measured) && pool.some((h) => !measured(h)),
|
|
70
71
|
telemetryUnknown: eligible.some((h) => !measured(h)),
|
|
71
72
|
overloaded: selected ? measured(selected) && selected.telemetry.loadBp > selected.fallbackLimitBp : false,
|
|
@@ -43,6 +43,27 @@ Preserve unknown files, unmerged author work, ambiguous publication, and evidenc
|
|
|
43
43
|
Do not force removal, bulk-clean, edit a runtime database, or add a scheduler,
|
|
44
44
|
daemon or state machine. Report other projects' resources; never touch them.
|
|
45
45
|
|
|
46
|
+
## Wait registrations and owned wakes
|
|
47
|
+
|
|
48
|
+
Follow [Bounded recovery boundary](../axstack/references/t3-runtime.md#bounded-recovery-boundary)
|
|
49
|
+
and [Waiting-turn and closing records](../axstack/references/run-record.md#waiting-turn-and-closing-records)
|
|
50
|
+
when ending an owner condition. On completion/cancellation/handoff, reconcile
|
|
51
|
+
ownership and delete only run-owned schedule IDs with absence readback.
|
|
52
|
+
Stop owned watches and clear only identified owned duplicate scheduled messages,
|
|
53
|
+
recording exact IDs, scope, owner, lifetime and receipts. Preserve user messages
|
|
54
|
+
and shared persistent schedules. Run completion unregisters only its own
|
|
55
|
+
registration and preserves the shared service and other registrations.
|
|
56
|
+
Without separately authorized shared-service lifetime, recovery ends at run
|
|
57
|
+
close, empty scope or expiry, whichever comes first. Empty scope means no open
|
|
58
|
+
wait registrations, rather than no eligible action in this pass. After expiry,
|
|
59
|
+
perform cleanup-only with zero sends even if deletion failed.
|
|
60
|
+
Unknown deletion or queue state is a scoped cleanup finding, never a reason to
|
|
61
|
+
redo product work. Keep that resource ID and its cleanup owner until reconciled.
|
|
62
|
+
Verify Stop/restart/provider-failure queue behavior before promising a timer
|
|
63
|
+
wake; a timer cannot override a held queue. Use advertised interval/fixed-time/
|
|
64
|
+
webhook operations, never an invented dedicated one-shot trigger. Recovery owns
|
|
65
|
+
only its own resources; each Driver cleans its own watches, queues and schedules.
|
|
66
|
+
|
|
46
67
|
## Reconcile each candidate
|
|
47
68
|
|
|
48
69
|
Take a fresh native inventory and bind every candidate to its exact projectId, attempt key, taskId/childThreadId/childRunId
|