@avantf/dsh-mission 0.3.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/LICENSE +21 -0
- package/README.md +249 -0
- package/cordis.patch.yml +13 -0
- package/lib/client.js +225 -0
- package/lib/dsh-build.json +16 -0
- package/lib/envinit-bootstrap.js +334 -0
- package/lib/index.js +7021 -0
- package/lib/interface-version.json +4 -0
- package/lib/mission-core/capacity.d.ts +100 -0
- package/lib/mission-core/continuation.d.ts +106 -0
- package/lib/mission-core/dispatch.d.ts +165 -0
- package/lib/mission-core/engine.d.ts +331 -0
- package/lib/mission-core/index.d.ts +25 -0
- package/lib/mission-core/liveness.d.ts +99 -0
- package/lib/mission-core/prompt.d.ts +112 -0
- package/lib/mission-core/resources.d.ts +57 -0
- package/lib/mission-core/timing.d.ts +37 -0
- package/lib/mission-core/tree.d.ts +447 -0
- package/lib/mission-core/trouble.d.ts +40 -0
- package/lib/mission-core/types.d.ts +409 -0
- package/lib/mission-core/wellformed.d.ts +98 -0
- package/lib/types/claims.d.ts +3 -0
- package/lib/types/claims.d.ts.map +1 -0
- package/lib/types/client/MissionTreeView.d.ts +247 -0
- package/lib/types/client/MissionTreeView.d.ts.map +1 -0
- package/lib/types/client/api.d.ts +184 -0
- package/lib/types/client/api.d.ts.map +1 -0
- package/lib/types/client/contract.d.ts +209 -0
- package/lib/types/client/contract.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +48 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/seat.d.ts +26 -0
- package/lib/types/client/seat.d.ts.map +1 -0
- package/lib/types/client/styles.d.ts +6 -0
- package/lib/types/client/styles.d.ts.map +1 -0
- package/lib/types/coldResume.d.ts +123 -0
- package/lib/types/coldResume.d.ts.map +1 -0
- package/lib/types/domain.d.ts +93 -0
- package/lib/types/domain.d.ts.map +1 -0
- package/lib/types/envinit.d.ts +126 -0
- package/lib/types/envinit.d.ts.map +1 -0
- package/lib/types/executorSession.d.ts +129 -0
- package/lib/types/executorSession.d.ts.map +1 -0
- package/lib/types/faces.d.ts +30 -0
- package/lib/types/faces.d.ts.map +1 -0
- package/lib/types/host.d.ts +752 -0
- package/lib/types/host.d.ts.map +1 -0
- package/lib/types/index.d.ts +48 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/interface_gate.d.ts +51 -0
- package/lib/types/interface_gate.d.ts.map +1 -0
- package/lib/types/log.d.ts +18 -0
- package/lib/types/log.d.ts.map +1 -0
- package/lib/types/projectionCache.d.ts +22 -0
- package/lib/types/projectionCache.d.ts.map +1 -0
- package/lib/types/prompt.d.ts +106 -0
- package/lib/types/prompt.d.ts.map +1 -0
- package/lib/types/source.d.ts +22 -0
- package/lib/types/source.d.ts.map +1 -0
- package/lib/types/store.d.ts +20 -0
- package/lib/types/store.d.ts.map +1 -0
- package/lib/types/timeFormat.d.ts +56 -0
- package/lib/types/timeFormat.d.ts.map +1 -0
- package/lib/types/tools.d.ts +34 -0
- package/lib/types/tools.d.ts.map +1 -0
- package/lib/types/wellformed.d.ts +43 -0
- package/lib/types/wellformed.d.ts.map +1 -0
- package/lib/types/wire.d.ts +178 -0
- package/lib/types/wire.d.ts.map +1 -0
- package/lib/types/workerEvents.d.ts +36 -0
- package/lib/types/workerEvents.d.ts.map +1 -0
- package/lib/types/workerSessions.d.ts +256 -0
- package/lib/types/workerSessions.d.ts.map +1 -0
- package/package.json +145 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capacity vocabulary: how much of ONE machine a mission may occupy, and how the machine's own
|
|
3
|
+
* capacity is derived. Both are plain numbers, deliberately — this module never imports `node:os`
|
|
4
|
+
* (the core builds without Node types) and never guesses at the hardware.
|
|
5
|
+
*
|
|
6
|
+
* ## The two numbers, and why they are not the same
|
|
7
|
+
*
|
|
8
|
+
* - **`weight`** is declared by a NODE: "this mission will occupy about N cores-equivalent while it
|
|
9
|
+
* runs". It defaults to 1, which is the ordinary slot every mission used to take.
|
|
10
|
+
* - **`capacity`** is derived for the HOST: how many of those units may run at once on this machine.
|
|
11
|
+
*
|
|
12
|
+
* The engine admits a candidate only while `Σ running.weight + candidate.weight ≤ capacity`. That is
|
|
13
|
+
* the master gate; `maxConcurrent` survives as a ceiling on the NUMBER of units (see `engine.ts`).
|
|
14
|
+
* Both are injected into the core as numbers, so every scheduling decision is deterministic under
|
|
15
|
+
* test; the machine reading itself happens at the plugin/host seam, in `resource.ts`'s probe.
|
|
16
|
+
*
|
|
17
|
+
* @module @avantf/mission-core/capacity
|
|
18
|
+
*/
|
|
19
|
+
/** Ceiling on derived capacity. 64 is far past any single machine this engine is meant to drive, and
|
|
20
|
+
* it keeps a wild reading (a container reporting 4096 CPUs) from materialising 4000 claims. */
|
|
21
|
+
export declare const CAPACITY_CEILING = 64;
|
|
22
|
+
/** Parallelism assumed when the host cannot read the machine at all (chain step ④). */
|
|
23
|
+
export declare const CAPACITY_FALLBACK_PARALLELISM = 4;
|
|
24
|
+
/** Cores left to the host/UI: one, matching this family's long-standing `cores - 1` convention. The
|
|
25
|
+
* engine itself, the owner's turns and the browser half all live on the same box. */
|
|
26
|
+
export declare const RESERVED_CORES = 1;
|
|
27
|
+
/**
|
|
28
|
+
* Where a capacity reading came from, in priority order. The literal is part of the start-up log
|
|
29
|
+
* (`capacity=N (source=…)`) and is therefore observed by operators, not just by tests.
|
|
30
|
+
*/
|
|
31
|
+
export type CapacitySource = 'config' | 'availableParallelism' | 'cores' | 'default';
|
|
32
|
+
export interface CapacityInput {
|
|
33
|
+
/** Step ①: the operator's explicit number. Used AS GIVEN (clamped), with no `-1` reservation:
|
|
34
|
+
* an explicit number is already the answer, and silently subtracting from it would make a
|
|
35
|
+
* configured 4 mean 3. */
|
|
36
|
+
readonly configured?: number | null;
|
|
37
|
+
/** Step ②: `os.availableParallelism()` — respects cgroup quotas and CPU affinity. */
|
|
38
|
+
readonly availableParallelism?: number | null;
|
|
39
|
+
/** Step ③: `os.cpus().length` — the physical/logical core count, ignoring quotas. */
|
|
40
|
+
readonly cores?: number | null;
|
|
41
|
+
}
|
|
42
|
+
export interface CapacityReading {
|
|
43
|
+
/** The number the engine gates on. */
|
|
44
|
+
readonly capacity: number;
|
|
45
|
+
readonly source: CapacitySource;
|
|
46
|
+
/** The raw parallelism the source reported, BEFORE the host reservation and the clamp. */
|
|
47
|
+
readonly parallelism: number;
|
|
48
|
+
/** Cores withheld from the engine: 1 for a derived reading, 0 for an explicit configuration. */
|
|
49
|
+
readonly reserved: number;
|
|
50
|
+
}
|
|
51
|
+
/** Clamp one capacity into `[1, CAPACITY_CEILING]`; a non-number degrades to the floor. */
|
|
52
|
+
export declare function clampCapacity(value: number): number;
|
|
53
|
+
/**
|
|
54
|
+
* The derivation chain, in ONE pure function so "which source won" is decided (and tested) in one
|
|
55
|
+
* place:
|
|
56
|
+
*
|
|
57
|
+
* ① explicit config → ② `os.availableParallelism()` → ③ `os.cpus().length` → ④ 4,
|
|
58
|
+
* then `capacity = clamp(max(1, derived - RESERVED_CORES), 1, CAPACITY_CEILING)`.
|
|
59
|
+
*
|
|
60
|
+
* Step ① is the exception to the reservation: an explicit number is used as-is (clamped). Measured
|
|
61
|
+
* on the development host (2026-10-02): `availableParallelism` = 12 → capacity 11, `cpus().length`
|
|
62
|
+
* = 12, cgroup `cpu.max` = `max 100000` (no quota).
|
|
63
|
+
*/
|
|
64
|
+
export declare function deriveCapacity(input?: CapacityInput): CapacityReading;
|
|
65
|
+
/** The weight of a node that declares none: one ordinary slot. */
|
|
66
|
+
export declare const DEFAULT_WEIGHT = 1;
|
|
67
|
+
/** Lowest meaningful weight: a mission that runs at all occupies at least one slot. */
|
|
68
|
+
export declare const MIN_WEIGHT = 1;
|
|
69
|
+
/** Highest declared weight. Matches the capacity ceiling, so even a "give me the whole machine"
|
|
70
|
+
* declaration (`weight` far above any real capacity) clamps to a number the engine can compare
|
|
71
|
+
* without a special case; `weight > capacity` remains reachable on every real host. */
|
|
72
|
+
export declare const MAX_WEIGHT = 64;
|
|
73
|
+
/**
|
|
74
|
+
* Read a declared weight into range. Missing, dirty or non-numeric values read as
|
|
75
|
+
* {@link DEFAULT_WEIGHT}, exactly like a record written before the field existed; a fractional value
|
|
76
|
+
* floors; anything below 1 rises to 1 and anything above {@link MAX_WEIGHT} falls to it.
|
|
77
|
+
*
|
|
78
|
+
* This is the ONE normalizer: the tool layer, the persistence load path and `makeNode` all call it,
|
|
79
|
+
* so a weight cannot mean two different things in two places.
|
|
80
|
+
*/
|
|
81
|
+
export declare function normalizeWeight(raw: unknown): number;
|
|
82
|
+
/**
|
|
83
|
+
* How long a node may be repeatedly deferred by the capacity gate before it RESERVES the machine:
|
|
84
|
+
* past this, no new node is admitted until the reserved one fits. Five minutes sits inside the
|
|
85
|
+
* recommended 3–10 minute band for one reason on each side: shorter windows turn an ordinary burst
|
|
86
|
+
* of weight-1 work into a reservation (which idles capacity the moment the heavy node cannot fit),
|
|
87
|
+
* while longer windows let a heavy mission wait through a whole work cycle before the engine stops
|
|
88
|
+
* feeding the queue in front of it.
|
|
89
|
+
*/
|
|
90
|
+
export declare const DEFAULT_CAPACITY_WAIT_MS: number;
|
|
91
|
+
/** Floor on a configured aging window. Below a minute, a reservation is indistinguishable from a
|
|
92
|
+
* scheduling hiccup, and work-conserving filling would be defeated by normal jitter. */
|
|
93
|
+
export declare const MIN_CAPACITY_WAIT_MS: number;
|
|
94
|
+
/**
|
|
95
|
+
* Default free-memory floor. A worker session is a model call plus tooling; below ~256 MiB the
|
|
96
|
+
* process is at real OOM risk. Deliberately low, because the host's floor signal is `os.freemem()`,
|
|
97
|
+
* which is a COARSE lower bound (see the probe) and a high threshold would stall ordinary work.
|
|
98
|
+
* `0` disables the gate.
|
|
99
|
+
*/
|
|
100
|
+
export declare const DEFAULT_MIN_FREE_MEMORY_BYTES: number;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Continuation drift: what changed on a node since the prompt its session was handed, and the
|
|
3
|
+
* threshold at which continuing that session stops being honest.
|
|
4
|
+
*
|
|
5
|
+
* The question this answers is "what does a COLD WAKE owe the session it is about to resume?" A
|
|
6
|
+
* parked session is woken while its own picture is still current (it parked cleanly one turn ago);
|
|
7
|
+
* a session that was interrupted mid-thought may wake up over a node that moved underneath it. The
|
|
8
|
+
* delta is that difference, computed by subtracting the dispatch baseline the session's own prompt
|
|
9
|
+
* left behind.
|
|
10
|
+
*
|
|
11
|
+
* @module @avantf/mission-core/continuation
|
|
12
|
+
*/
|
|
13
|
+
import type { NodeRecord } from './types.js';
|
|
14
|
+
/** The node's accumulating channels as the delta reads them. */
|
|
15
|
+
export interface ContinuationDelta {
|
|
16
|
+
/** `false` when the node carries no baseline (a record written before the field existed, or one
|
|
17
|
+
* whose prompt was never built by this plugin). The counts below are then the conservative
|
|
18
|
+
* reading — never "nothing changed" — and the wake renders an honest caveat instead. */
|
|
19
|
+
readonly baselineKnown: boolean;
|
|
20
|
+
/** Corrections this session has NOT seen, in order. The union of two marks: the delivery
|
|
21
|
+
* watermark (`correctionsDeliveredUpTo`, advanced by a live steer or by a previous wake) and the
|
|
22
|
+
* baseline's own count (everything that already existed when this session's prompt was built and
|
|
23
|
+
* was therefore rendered into it). Taking the LATER of the two is what keeps a fresh spawn's
|
|
24
|
+
* corrections from being reported as unseen forever. On an unknown baseline it falls back to the
|
|
25
|
+
* watermark alone, which is exactly the previous generation's reading. */
|
|
26
|
+
readonly corrections: readonly string[];
|
|
27
|
+
/** Notes appended to the node since the baseline. Only a session that held the node can append,
|
|
28
|
+
* so these are normally the interrupted attempt's OWN notes — reported back so the resumed
|
|
29
|
+
* session sees what it had already concluded. */
|
|
30
|
+
readonly notes: readonly string[];
|
|
31
|
+
/** Children that reached a terminal state since the baseline. Rendered, never a reason to refuse
|
|
32
|
+
* the continuation; `0` when the baseline is unknown. */
|
|
33
|
+
readonly terminalChildren: number;
|
|
34
|
+
/** Whether title or description changed since the baseline; `false` when unknown. */
|
|
35
|
+
readonly titleOrContentChanged: boolean;
|
|
36
|
+
/** The node's latest analysis was written by somebody OTHER than the session this baseline belongs
|
|
37
|
+
* to, and the node carries notes at all. It is the closest thing the record has to "another
|
|
38
|
+
* executor has been writing this node's judgement", which is a freshness signal the next wake must
|
|
39
|
+
* not ignore.
|
|
40
|
+
*
|
|
41
|
+
* Compared by IDENTITY (`analysisAuthor` vs the baseline's `holder`) whenever both are known: the
|
|
42
|
+
* generation number alone misreads the common case this field exists for — a session's OWN note
|
|
43
|
+
* outliving its dispatch, which happens to every parent that wrote its analysis and decomposed.
|
|
44
|
+
* When either side is missing (a record written before the fields existed) it falls back to the
|
|
45
|
+
* generation comparison (`analysisAttempt` vs `baseline.attempts`), which is the conservative
|
|
46
|
+
* reading: a false positive only costs a fresh executor. */
|
|
47
|
+
readonly analysisFromAnotherDispatch: boolean;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A compact fingerprint of the mission HEADLINE (title + description), hashed rather than stored
|
|
51
|
+
* verbatim because a description can be long and this rides every node record; the fingerprint is
|
|
52
|
+
* only ever COMPARED, never read. Two 32-bit FNV-1a passes with different seeds (64 bits together)
|
|
53
|
+
* are far more than the "did somebody rewrite this mission" question needs, and the core stays
|
|
54
|
+
* dependency-free — it is deliberately built without `node:crypto`.
|
|
55
|
+
*/
|
|
56
|
+
export declare function nodeFingerprint(title: string, description: string): string;
|
|
57
|
+
/**
|
|
58
|
+
* Subtract the node's dispatch baseline from its current state. Pure, so the arithmetic is testable
|
|
59
|
+
* without a tree.
|
|
60
|
+
*
|
|
61
|
+
* `terminalChildren` is passed in rather than read off the node because the node record carries
|
|
62
|
+
* child IDS, not their statuses.
|
|
63
|
+
*/
|
|
64
|
+
export declare function computeContinuationDelta(node: NodeRecord, terminalChildren: number): ContinuationDelta;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the drift since a session's own prompt is large enough that continuing it would be
|
|
67
|
+
* dishonest, and a fresh executor is the better answer.
|
|
68
|
+
*
|
|
69
|
+
* The rule is a disjunction of STRONG signals, each of which already carries its own threshold; it
|
|
70
|
+
* is deliberately not a weighted score, because every one of these means "this session's picture of
|
|
71
|
+
* the mission cannot be trusted", and there is no reading under which two weak signals should
|
|
72
|
+
* outvote one strong one.
|
|
73
|
+
*
|
|
74
|
+
* 1. `corrections` non-empty — the owner changed the direction of the mission while the session was
|
|
75
|
+
* away. The session's plan was built under the old direction, and a correction handed to a
|
|
76
|
+
* session that is already committed to a plan is exactly the case the fresh path is better at
|
|
77
|
+
* (the new executor reads the correction before it reads anything else). This is also why the
|
|
78
|
+
* delta's correction clause is normally empty: unread corrections do not reach the delta, they
|
|
79
|
+
* change the route.
|
|
80
|
+
* 2. `analysisFromAnotherDispatch` — the node's latest analysis was written by somebody other than
|
|
81
|
+
* this session, i.e. the node's judgement channel has been advanced by a different executor.
|
|
82
|
+
* Conservative by construction: a false positive only costs a fresh executor, which is always a
|
|
83
|
+
* correct answer, while a false negative would resume a session over a judgement it never wrote.
|
|
84
|
+
* Compared by holder identity where the record carries it; the pre-identity generation comparison
|
|
85
|
+
* is the fallback (see the field's own note).
|
|
86
|
+
* 3. `titleOrContentChanged` — the mission was re-defined under the session. It would be resuming a
|
|
87
|
+
* mission it was never handed.
|
|
88
|
+
*
|
|
89
|
+
* **`terminalChildren` is deliberately absent, and this is a hard requirement.** For a PARKED
|
|
90
|
+
* session, "all children reached a terminal state" is not drift — it is the ENGINE's own trigger
|
|
91
|
+
* for the wake (`decompose_mission` is followed by a synchronous `pump()`, so the parent parks the
|
|
92
|
+
* moment its children are created). Counting it as material would demote every parked wake to a
|
|
93
|
+
* fresh spawn and delete the feature the previous generation shipped. Excluding it here means the
|
|
94
|
+
* exclusion survives even if a later change routes both wakes through one decision point; the
|
|
95
|
+
* regression is pinned by a test on both sides (`the parked session is woken, not replaced`).
|
|
96
|
+
*
|
|
97
|
+
* An UNKNOWN baseline answers `false`: the caller continues the session and says so honestly (the
|
|
98
|
+
* prompt renders "the drift cannot be determined, trust the current view over your memory"). The
|
|
99
|
+
* opposite choice — treat unknown as material — would throw the session away on the one-time
|
|
100
|
+
* migration of every in-flight mission, which is a permanent loss of context for a question the
|
|
101
|
+
* prompt can answer in one sentence. That also settles the one signal an unknown baseline could
|
|
102
|
+
* still offer: its `corrections` are the raw delivery-watermark tail, i.e. exactly what the previous
|
|
103
|
+
* generation rendered into the wake, and a record from before the baseline existed must keep being
|
|
104
|
+
* resumed on that reading rather than be re-judged by a rule it predates.
|
|
105
|
+
*/
|
|
106
|
+
export declare function isMaterialChange(delta: ContinuationDelta): boolean;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { type NodeRecord, type TreeRecord, type WaitingFor } from './types.js';
|
|
2
|
+
/** One tree as the admission policy sees it. Structurally satisfied by `TreeState`, so this module
|
|
3
|
+
* never imports the class that owns the state. */
|
|
4
|
+
export interface DispatchScope {
|
|
5
|
+
readonly tree: TreeRecord;
|
|
6
|
+
readonly nodes: ReadonlyMap<string, NodeRecord>;
|
|
7
|
+
}
|
|
8
|
+
export declare function spawnBackoffMs(n: number): number;
|
|
9
|
+
/** Order candidates: oldest first (cross-tree fairness), then by id for stability. */
|
|
10
|
+
export declare function byCreatedAtThenId(a: NodeRecord, b: NodeRecord): number;
|
|
11
|
+
/**
|
|
12
|
+
* A declared unit as stored: trimmed, path-normalized, and `null` for "nothing declared". A blank
|
|
13
|
+
* string is the way a caller opts OUT of a lease explicitly (see `@avantf/dsh-mission`'s tool layer),
|
|
14
|
+
* and it is the same value a missing field loads as, so the two cannot diverge.
|
|
15
|
+
*
|
|
16
|
+
* ## Why the lease key is normalized
|
|
17
|
+
*
|
|
18
|
+
* `unit` is model-authored free text, and the exclusion it buys is an exact string comparison. Two
|
|
19
|
+
* executors told to change the same directory will not necessarily spell it the same way — `a/b`,
|
|
20
|
+
* `a/b/`, `./a/b` and `a\b` are one scope to a human and four different lease keys to the engine, so
|
|
21
|
+
* the guarantee ("two parallel lanes never touch one file") held only when the two agents happened to
|
|
22
|
+
* type byte-identical text.
|
|
23
|
+
*
|
|
24
|
+
* The canonical form, chosen conservatively:
|
|
25
|
+
*
|
|
26
|
+
* - case: ASCII lower-cased, so `Mission/x` and `mission/x` are one lease. The tool text promises
|
|
27
|
+
* this ("大小写、分隔符、结尾斜杠不同都算同一个范围") and Windows paths are case-insensitive; it is
|
|
28
|
+
* deliberate that a case-SENSITIVE filesystem now serializes two spellings that would not actually
|
|
29
|
+
* collide there — the safe direction is to keep the scope busy, not to hand it out twice.
|
|
30
|
+
* - separators: `\` reads as `/` (a Windows-authored path and a POSIX one name the same scope). At
|
|
31
|
+
* most one leading `/` survives, so `//srv/x` (a UNC spelling) and `/srv/x` still differ by the
|
|
32
|
+
* leading slash and are NOT conflated.
|
|
33
|
+
* - redundant separators collapse (`a//b` → `a/b`).
|
|
34
|
+
* - `.` segments are dropped and trailing separators are trimmed (`./a/b/` → `a/b`).
|
|
35
|
+
* - `..` segments are resolved LEXICALLY (`a/b/../c` → `a/c`), but a `..` that would escape the front
|
|
36
|
+
* of the path is PRESERVED (`../a` stays `../a`), so distinctly-named scopes stay distinct. This is
|
|
37
|
+
* textual, not filesystem-aware: no `realpath`, no cwd, no symlink resolution — resolving against
|
|
38
|
+
* the real filesystem would make the lease depend on which process asked, and the core has no cwd.
|
|
39
|
+
*
|
|
40
|
+
* **What this still does not cover (v1, declared in `mission/docs/design/2026-10-02-unit-lease.md`):
|
|
41
|
+
* containment.** `a` and `a/b` remain two different keys, so a parent directory and a file inside it
|
|
42
|
+
* are not mutually exclusive. Only the identical-scope case is guaranteed; a caller that wants
|
|
43
|
+
* isolation must name the exact same scope on both nodes (the tool text recommends a path relative
|
|
44
|
+
* to the repository root, e.g. `mission/packages/core`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function normalizeUnit(raw: string | null | undefined): string | null;
|
|
47
|
+
/**
|
|
48
|
+
* The unit a decomposed child runs under. `undefined` — the spec said nothing — INHERITS the
|
|
49
|
+
* parent's unit, which is the safe default: siblings of one decomposition then cannot run at the
|
|
50
|
+
* same time. An explicit value (including a blank string = "no lease") overrides that.
|
|
51
|
+
*/
|
|
52
|
+
export declare function resolveChildUnit(parentUnit: string | null, declared: string | null | undefined): string | null;
|
|
53
|
+
/**
|
|
54
|
+
* A child's weight. `undefined` — the spec said nothing — does NOT inherit the parent's estimate:
|
|
55
|
+
* a parent that needs 8 cores may be split into children that need 1 each, and inheriting would
|
|
56
|
+
* re-serialize work the decomposition just made parallel. Anything declared is clamped by
|
|
57
|
+
* {@link normalizeWeight}, the same normalizer every other entry point uses.
|
|
58
|
+
*/
|
|
59
|
+
export declare function resolveChildWeight(declared: number | undefined): number;
|
|
60
|
+
/** Every unit currently held by a `running` node, ACROSS every tree: a collision is cross-root by
|
|
61
|
+
* nature (two owners' missions that touch one file collide exactly like two siblings), so a
|
|
62
|
+
* per-tree answer would not describe the resource. */
|
|
63
|
+
export declare function heldUnits(scopes: Iterable<DispatchScope>): ReadonlySet<string>;
|
|
64
|
+
/**
|
|
65
|
+
* The `running` node holding `unit`, if any, excluding `exceptId` (the node asking). `undefined`
|
|
66
|
+
* means the unit is free. The scan deliberately spans every tree, including closed ones: a running
|
|
67
|
+
* node holds its lease no matter which tree it belongs to, and the safe direction is to keep the
|
|
68
|
+
* unit busy rather than to hand it out twice.
|
|
69
|
+
*/
|
|
70
|
+
export declare function unitHolder(scopes: Iterable<DispatchScope>, unit: string, exceptId?: string): NodeRecord | undefined;
|
|
71
|
+
/**
|
|
72
|
+
* The live admission arithmetic the capacity gate needs. Supplied by the engine (it owns the aging
|
|
73
|
+
* clock and the per-node "deferred since" memory); absent means the gate is OPEN, which is exactly
|
|
74
|
+
* how the engine behaved before capacity existed and keeps `MissionTree.nextDispatchable()`'s
|
|
75
|
+
* low-level contract unchanged.
|
|
76
|
+
*/
|
|
77
|
+
export interface CapacityPolicy {
|
|
78
|
+
/** Master gate, in the same unit as `weight`. */
|
|
79
|
+
readonly capacity: number;
|
|
80
|
+
/** Backstop on the NUMBER of running units (`maxConcurrent`). */
|
|
81
|
+
readonly maxConcurrent: number;
|
|
82
|
+
readonly runningCount: number;
|
|
83
|
+
readonly runningWeight: number;
|
|
84
|
+
/** Node id → the time the capacity gate first deferred it. Drives aging; the engine owns it so a
|
|
85
|
+
* test can move the injected clock instead of waiting. */
|
|
86
|
+
readonly deferredSince: ReadonlyMap<string, number>;
|
|
87
|
+
/** Wait past which a deferred node RESERVES the machine (no new admissions until it fits). */
|
|
88
|
+
readonly agingMs: number;
|
|
89
|
+
readonly now: number;
|
|
90
|
+
/** A machine-wide gate (the free-memory floor) that blocks EVERY dispatch while set. */
|
|
91
|
+
readonly globalBlock?: WaitingFor;
|
|
92
|
+
}
|
|
93
|
+
/** One candidate the plan did not select, with the reason it is waiting. */
|
|
94
|
+
export interface DeferredCandidate {
|
|
95
|
+
readonly node: NodeRecord;
|
|
96
|
+
readonly waitingFor: WaitingFor;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* The machine-side half of the admission arithmetic: what the gate compares a candidate against.
|
|
100
|
+
* Deliberately split from {@link CapacityPolicy}, which adds the caller's SNAPSHOT of the load, the
|
|
101
|
+
* aging clock and the machine-wide block. The lock-held recheck takes these numbers as given but
|
|
102
|
+
* FILLS THEM FROM LIVE STATE (`MissionTree.runningLoad()`), which is exactly why the shared judgement
|
|
103
|
+
* cannot read the policy's snapshot itself.
|
|
104
|
+
*/
|
|
105
|
+
export interface CapacityLoad {
|
|
106
|
+
/** Master gate, in the same unit as `weight`. */
|
|
107
|
+
readonly capacity: number;
|
|
108
|
+
/** Backstop on the NUMBER of running units (`maxConcurrent`). */
|
|
109
|
+
readonly maxConcurrent: number;
|
|
110
|
+
readonly runningCount: number;
|
|
111
|
+
readonly runningWeight: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The ONE capacity judgement, shared by {@link planDispatch} and by the tree's lock-held recheck so
|
|
115
|
+
* the two can never drift: `undefined` when `node` may be admitted right now, otherwise the
|
|
116
|
+
* structured reason it must wait.
|
|
117
|
+
*
|
|
118
|
+
* The rule: a candidate heavier than the whole machine (`weight > capacity`) is EXCLUSIVE and fits
|
|
119
|
+
* only when nothing else is running — the arithmetic form of "it is the reservation that waited the
|
|
120
|
+
* longest"; otherwise `Σ running.weight + weight ≤ capacity`. In both cases the `maxConcurrent` slot
|
|
121
|
+
* ceiling must have room. The reason keeps the projection's long-standing precedence: capacity
|
|
122
|
+
* (the master gate) before the slot ceiling.
|
|
123
|
+
*/
|
|
124
|
+
export declare function capacityWaitingFor(node: NodeRecord, load: CapacityLoad): WaitingFor | undefined;
|
|
125
|
+
/** What one admission scan resolved to. */
|
|
126
|
+
export interface DispatchPlan {
|
|
127
|
+
readonly selected: NodeRecord | undefined;
|
|
128
|
+
/** Every candidate examined and left behind, in FIFO order, with its live waiting reason. */
|
|
129
|
+
readonly deferred: readonly DeferredCandidate[];
|
|
130
|
+
/**
|
|
131
|
+
* True when an aged node has RESERVED the machine and does not fit yet: the caller must not admit
|
|
132
|
+
* anything else this pass. `selected` is then `undefined` by construction.
|
|
133
|
+
*/
|
|
134
|
+
readonly reserved: boolean;
|
|
135
|
+
}
|
|
136
|
+
/** What {@link selectNextDispatchable} needs to know beyond the node states themselves. */
|
|
137
|
+
export interface DispatchPolicy {
|
|
138
|
+
/** Node ids already spoken for in this pass; they must not be selected twice. */
|
|
139
|
+
readonly exclude?: ReadonlySet<string>;
|
|
140
|
+
readonly now: number;
|
|
141
|
+
readonly isAgentLive: (sessionId: string) => boolean;
|
|
142
|
+
/** Capacity/slot admission arithmetic; absent means "no capacity gate" (the legacy contract). */
|
|
143
|
+
readonly capacity?: CapacityPolicy;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* The next node to dispatch, or `undefined`. Kept as the simple projection of {@link planDispatch}
|
|
147
|
+
* for callers that only need the answer (and for the pre-capacity contract).
|
|
148
|
+
*/
|
|
149
|
+
export declare function selectNextDispatchable(scopes: Iterable<DispatchScope>, policy: DispatchPolicy): NodeRecord | undefined;
|
|
150
|
+
/**
|
|
151
|
+
* The full admission scan: pick at most one node to dispatch AND describe every candidate left
|
|
152
|
+
* behind. Selection rules, in order:
|
|
153
|
+
*
|
|
154
|
+
* 1. Collect the eligible candidates exactly as the pre-capacity engine did (open tree, live owner,
|
|
155
|
+
* not excluded, dispatchable, no live binding, no parked session, no held unit, not in spawn
|
|
156
|
+
* backoff), FIFO by {@link byCreatedAtThenId}.
|
|
157
|
+
* 2. A machine-wide block (the free-memory floor) defers everything, nothing ages, no reservation.
|
|
158
|
+
* 3. The slot ceiling, if reached, defers everything (capacity may still have room).
|
|
159
|
+
* 4. An AGED candidate (waiting for capacity at least `agingMs`) that is not unit-blocked takes the
|
|
160
|
+
* machine: if it fits — or it is heavier than the whole capacity and nothing is running — it is
|
|
161
|
+
* selected; otherwise `reserved` is true and nothing else is admitted.
|
|
162
|
+
* 5. Otherwise the first FITTING candidate in FIFO order is selected, skipping heavier ones, so
|
|
163
|
+
* spare capacity is filled (work-conserving).
|
|
164
|
+
*/
|
|
165
|
+
export declare function planDispatch(scopes: Iterable<DispatchScope>, policy: DispatchPolicy): DispatchPlan;
|