@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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/cordis.patch.yml +13 -0
  4. package/lib/client.js +225 -0
  5. package/lib/dsh-build.json +16 -0
  6. package/lib/envinit-bootstrap.js +334 -0
  7. package/lib/index.js +7021 -0
  8. package/lib/interface-version.json +4 -0
  9. package/lib/mission-core/capacity.d.ts +100 -0
  10. package/lib/mission-core/continuation.d.ts +106 -0
  11. package/lib/mission-core/dispatch.d.ts +165 -0
  12. package/lib/mission-core/engine.d.ts +331 -0
  13. package/lib/mission-core/index.d.ts +25 -0
  14. package/lib/mission-core/liveness.d.ts +99 -0
  15. package/lib/mission-core/prompt.d.ts +112 -0
  16. package/lib/mission-core/resources.d.ts +57 -0
  17. package/lib/mission-core/timing.d.ts +37 -0
  18. package/lib/mission-core/tree.d.ts +447 -0
  19. package/lib/mission-core/trouble.d.ts +40 -0
  20. package/lib/mission-core/types.d.ts +409 -0
  21. package/lib/mission-core/wellformed.d.ts +98 -0
  22. package/lib/types/claims.d.ts +3 -0
  23. package/lib/types/claims.d.ts.map +1 -0
  24. package/lib/types/client/MissionTreeView.d.ts +247 -0
  25. package/lib/types/client/MissionTreeView.d.ts.map +1 -0
  26. package/lib/types/client/api.d.ts +184 -0
  27. package/lib/types/client/api.d.ts.map +1 -0
  28. package/lib/types/client/contract.d.ts +209 -0
  29. package/lib/types/client/contract.d.ts.map +1 -0
  30. package/lib/types/client/index.d.ts +48 -0
  31. package/lib/types/client/index.d.ts.map +1 -0
  32. package/lib/types/client/seat.d.ts +26 -0
  33. package/lib/types/client/seat.d.ts.map +1 -0
  34. package/lib/types/client/styles.d.ts +6 -0
  35. package/lib/types/client/styles.d.ts.map +1 -0
  36. package/lib/types/coldResume.d.ts +123 -0
  37. package/lib/types/coldResume.d.ts.map +1 -0
  38. package/lib/types/domain.d.ts +93 -0
  39. package/lib/types/domain.d.ts.map +1 -0
  40. package/lib/types/envinit.d.ts +126 -0
  41. package/lib/types/envinit.d.ts.map +1 -0
  42. package/lib/types/executorSession.d.ts +129 -0
  43. package/lib/types/executorSession.d.ts.map +1 -0
  44. package/lib/types/faces.d.ts +30 -0
  45. package/lib/types/faces.d.ts.map +1 -0
  46. package/lib/types/host.d.ts +752 -0
  47. package/lib/types/host.d.ts.map +1 -0
  48. package/lib/types/index.d.ts +48 -0
  49. package/lib/types/index.d.ts.map +1 -0
  50. package/lib/types/interface_gate.d.ts +51 -0
  51. package/lib/types/interface_gate.d.ts.map +1 -0
  52. package/lib/types/log.d.ts +18 -0
  53. package/lib/types/log.d.ts.map +1 -0
  54. package/lib/types/projectionCache.d.ts +22 -0
  55. package/lib/types/projectionCache.d.ts.map +1 -0
  56. package/lib/types/prompt.d.ts +106 -0
  57. package/lib/types/prompt.d.ts.map +1 -0
  58. package/lib/types/source.d.ts +22 -0
  59. package/lib/types/source.d.ts.map +1 -0
  60. package/lib/types/store.d.ts +20 -0
  61. package/lib/types/store.d.ts.map +1 -0
  62. package/lib/types/timeFormat.d.ts +56 -0
  63. package/lib/types/timeFormat.d.ts.map +1 -0
  64. package/lib/types/tools.d.ts +34 -0
  65. package/lib/types/tools.d.ts.map +1 -0
  66. package/lib/types/wellformed.d.ts +43 -0
  67. package/lib/types/wellformed.d.ts.map +1 -0
  68. package/lib/types/wire.d.ts +178 -0
  69. package/lib/types/wire.d.ts.map +1 -0
  70. package/lib/types/workerEvents.d.ts +36 -0
  71. package/lib/types/workerEvents.d.ts.map +1 -0
  72. package/lib/types/workerSessions.d.ts +256 -0
  73. package/lib/types/workerSessions.d.ts.map +1 -0
  74. package/package.json +145 -0
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Work-tree vocabulary: node states, capacities, durable record shapes. The tree is the only
3
+ * authoritative state holder — every execution reads what it needs from the node it was
4
+ * dispatched for, never from an agent's conversation.
5
+ *
6
+ * @module @avantf/mission-core/types
7
+ */
8
+ export type NodeStatus =
9
+ /** Has non-terminal children; not dispatchable. */
10
+ 'blocked'
11
+ /** The only spawnable status: no children yet, or all terminal. */
12
+ | 'ready'
13
+ /** Dispatched; `claimedBy` holds the worker. */
14
+ | 'running'
15
+ /** Worker vanished without submitting; dispatchable again. */
16
+ | 'interrupted'
17
+ /** Terminal: a result was submitted. */
18
+ | 'done'
19
+ /** Terminal: attempts exhausted, or rejected by a capacity check. */
20
+ | 'failed';
21
+ /** Terminal statuses never change again. */
22
+ export declare const TERMINAL: ReadonlySet<NodeStatus>;
23
+ export declare const DISPATCHABLE: ReadonlySet<NodeStatus>;
24
+ /** Capacities: exported so the tools and the engine validate against ONE source. */
25
+ export declare const CAPACITY: {
26
+ /** Root is depth 1, so depth 8 allows at most seven consecutive decompositions. */
27
+ readonly maxDepth: 8;
28
+ readonly maxChildrenPerDecompose: 6;
29
+ /** Backstop ceiling on one tree's size, checked before a `decompose` commits: depth (8) ×
30
+ * children (6) leaves the count bounded but enormous, and the depth limit does not fire until
31
+ * the eighth level. */
32
+ readonly maxNodesPerTree: 200;
33
+ /** Execution-failure ceiling: a node reclaimed (vanished or stalled) this many times fails;
34
+ * successful rounds do not count. The same ceiling bounds consecutive failed STARTS
35
+ * (`spawnFailures`), tracked separately so an outage is not charged against the mission. */
36
+ readonly maxAttempts: 5;
37
+ /** A node reclaimed for silence this many times gets a heads-up, not a decision request — the
38
+ * engine recovers on its own, which is why it waits for a repeat. */
39
+ readonly maxStallsBeforeReport: 2;
40
+ /** A node reclaimed as `hung` (alive but unproductive, or past its round cap) this many times IN A
41
+ * ROW gets the same one-message heads-up, through the same durable marker. One higher than
42
+ * {@link maxStallsBeforeReport} because a hung round is already a whole stale/round window long, so
43
+ * three in a row is unambiguous rather than jitter — and because a single long-but-legitimate step
44
+ * must not page the owner. This is the ceiling the old behaviour lacked: hung still charges no
45
+ * budget, but it can no longer repeat forever without anyone being told. */
46
+ readonly maxHungsBeforeReport: 3;
47
+ /** Longer results are spilled to the store; the node keeps the summary. */
48
+ readonly maxInlineResultChars: 2000;
49
+ };
50
+ export interface TreeRecord {
51
+ /** Root node id; also the tree's id. */
52
+ readonly rootId: string;
53
+ /** The agent that created the root: no other agent sees the tree, and the tree is destroyed
54
+ * when this session stops existing. */
55
+ readonly ownerSessionId: string;
56
+ readonly createdAt: number;
57
+ /** When the owner closed the tree out (`finish_mission`); `null` while open. Closing is archival —
58
+ * nodes and results stay, but the tree leaves the guidance and the wake decision. */
59
+ readonly closedAt: number | null;
60
+ /** When the owner was woken about this tree reaching a terminal state. Durable on purpose: an
61
+ * in-memory guard re-reports on every dispatch pass after a restart. */
62
+ readonly reportedAt: number | null;
63
+ }
64
+ /**
65
+ * What one dispatch's prompt showed its session, frozen so a LATER wake can subtract it: "what has
66
+ * changed since this session last read this mission?" It is a snapshot of the node's ACCUMULATING
67
+ * channels, not of its execution state — the execution state is the binding's business.
68
+ *
69
+ * Every component exists for one consumer; none is decoration:
70
+ *
71
+ * - `corrections` — the second half of "corrections this session has NOT seen" (the delivery
72
+ * watermark is the first). Both are needed: a FRESH spawn renders every correction but does not
73
+ * advance the watermark, so a watermark-only reading would call those corrections unseen forever
74
+ * and route every corrected mission to a fresh executor instead of continuing it.
75
+ * - `notes` — how many analysis entries existed when the prompt was built; entries beyond it are the
76
+ * ones appended while this session held the node, which is what the delta reports.
77
+ * - `terminalChildren` — for the delta's wording only ("N 子任务达到终态"). Deliberately NOT part of
78
+ * the material-change judgement, because for a PARKED session "all children terminal" is the
79
+ * trigger of the wake itself (see `isMaterialChange`).
80
+ * - `fingerprint` — title+description at dispatch. A changed headline means this session would be
81
+ * resuming a mission it was never handed.
82
+ * - `attempts` — the dispatch generation this prompt was built under, compared against
83
+ * `analysisAttempt` to tell whether the node's latest note belongs to THIS dispatch.
84
+ * - `holder` — the session this prompt was delivered to. The generation number alone cannot answer
85
+ * "is this note mine?": a session's own note survives into a LATER dispatch of the same node
86
+ * (the parent that decomposed and is woken again is the common case), so `attempts` would call
87
+ * its own judgement somebody else's. The wake compares this holder against the note's author
88
+ * instead (`analysisAuthor`), and falls back to the generation comparison only when this is
89
+ * `null` (a baseline written before the field existed).
90
+ *
91
+ * Missing on a record written before the field existed: that reads as "no baseline", which a wake
92
+ * must treat as UNKNOWN rather than as "nothing changed" (see `computeContinuationDelta`).
93
+ */
94
+ export interface DispatchBaseline {
95
+ readonly corrections: number;
96
+ readonly notes: number;
97
+ readonly terminalChildren: number;
98
+ readonly fingerprint: string;
99
+ readonly attempts: number;
100
+ /** The session holding the node when this prompt was built; `null` on a pre-`holder` record, which
101
+ * the delta reads as "author unknown" and judges by the generation comparison. */
102
+ readonly holder: string | null;
103
+ }
104
+ /**
105
+ * Why a `ready`/`interrupted` node has not been dispatched yet — computed live by the engine from
106
+ * the current admission state, never persisted. `null` (or a missing field on an older payload)
107
+ * means "nothing is holding it back".
108
+ *
109
+ * The four reasons are the four gates that can defer a dispatch:
110
+ *
111
+ * - `capacity` — the master gate: `Σ running.weight + weight > capacity`. `resource` distinguishes
112
+ * the machine-derived compute capacity (`cpu`) from the host's free-memory floor (`memory`, whose
113
+ * `needed`/`available` are BYTES, not cores). Only the `cpu` flavour can age into a reservation.
114
+ * - `unit` — another `running` node holds the same declared scope (the unit lease).
115
+ * - `slot` — the `maxConcurrent` ceiling on the NUMBER of units; capacity itself has room.
116
+ * - `aging` — an aged node has RESERVED the machine (past the aging window: no new admission until
117
+ * it fits), so every OTHER queued candidate is held back even though `capacity` alone might have
118
+ * room. Reported instead of `slot` because "capacity is free, the slots are just full" would be
119
+ * false, and this is the one moment the anti-starvation mechanism is supposed to be visible.
120
+ *
121
+ * `needed`/`available` are filled only where a number is meaningful; a memory deferral whose signal
122
+ * is `null` ("cannot tell") carries no numbers at all, because there is nothing honest to show.
123
+ */
124
+ export interface WaitingFor {
125
+ readonly reason: 'capacity' | 'unit' | 'slot' | 'aging';
126
+ readonly resource?: 'cpu' | 'memory';
127
+ readonly needed?: number;
128
+ readonly available?: number;
129
+ /** The unit another node holds, for `reason: 'unit'`. */
130
+ readonly unit?: string;
131
+ }
132
+ export interface NodeRecord {
133
+ readonly id: string;
134
+ readonly rootId: string;
135
+ /** The parent that created this node; `null` for the root. Provenance only: dedup reuse can list
136
+ * a node under several parents' `children` while it keeps the `parentId` it was born under. */
137
+ readonly parentId: string | null;
138
+ readonly title: string;
139
+ readonly description: string;
140
+ /** How many capacity units (cores-equivalent, the same unit as `EngineOptions.capacity`) this
141
+ * mission is expected to occupy while it runs. Declared by `create_mission` for a root and per
142
+ * child by `decompose_mission`; a child that declares nothing uses the DEFAULT 1 and deliberately
143
+ * does NOT inherit the parent's estimate — a parent's appetite says nothing about one child's.
144
+ * Persisted, and a record written before the field existed loads as 1; `DOMAIN_VERSION` stays 1. */
145
+ readonly weight: number;
146
+ /** The SCOPE this mission will modify — a directory or a file — and therefore the key of its
147
+ * engine-enforced lease: no two `running` nodes may carry the same non-null `unit` (see
148
+ * `@avantf/mission-core/dispatch`). Declared by `create_mission` for a root and per child by
149
+ * `decompose_mission`; a child that declares nothing INHERITS its parent's unit, so same-scope
150
+ * siblings cannot run at the same time.
151
+ *
152
+ * `null` means "no scope declared", which is also what a record written before this field existed
153
+ * loads as: such a node takes no part in the lease and behaves exactly as it did before the field
154
+ * existed. Persisted, `DOMAIN_VERSION` stays 1. */
155
+ readonly unit: string | null;
156
+ /** Per-mission RELAXATION of the engine's `roundMs` ceiling — how long ONE round of this mission
157
+ * may run before it is reclaimed as `hung`. Declared by `create_mission` for a root and per child by
158
+ * `decompose_mission`, exactly like `weight`; a child's declaration is its own and is deliberately
159
+ * NOT inherited (a parent's long build says nothing about one child). Relaxation ONLY: the engine
160
+ * compares `max(configured roundMs, this)`, so a declaration can never shorten the backstop that
161
+ * catches a transport retrying forever, and `normalizeRoundMs` caps it at 24 h so it cannot opt out
162
+ * of the backstop either. `null` means "use the engine's configured cap", which is also what a
163
+ * record written before the field existed loads as. Persisted, `DOMAIN_VERSION` stays 1. */
164
+ readonly roundMs: number | null;
165
+ /** Background facts: why this mission exists (written by the decomposer), or the owner's initial
166
+ * analysis for a root — the only channel carrying the vertical "why" down the mission chain. */
167
+ readonly context: readonly string[];
168
+ /** The owner's corrections for this mission, newest last. The name is load-bearing because the
169
+ * field is persisted: renaming it would make every earlier correction load as absent. Separate
170
+ * from `context` on purpose — different author and lifetime. */
171
+ readonly corrections: readonly string[];
172
+ /** Delivery watermark for {@link corrections}: how many LEADING entries are confirmed delivered
173
+ * to the executor that was holding this node. What it decides is the WAKE message — a cold resume
174
+ * renders only `corrections.slice(correctionsDeliveredUpTo)`, because the corrections already
175
+ * handed to that very session must not be argued to it a second time. A FRESH executor ignores
176
+ * the watermark entirely and sees every correction: it has read none of them.
177
+ *
178
+ * `0` means "nothing is confirmed delivered", which is also what a record written before this
179
+ * field existed loads as — the conservative reading, since a silently skipped correction is a
180
+ * direction the owner gave that nobody ever reads. Monotone: a raced, older report can never pull
181
+ * it back. Kept as a NUMBER beside the text array rather than turning `corrections` into objects,
182
+ * because changing that array's shape would make every historical correction load as absent. */
183
+ readonly correctionsDeliveredUpTo: number;
184
+ /** What this mission's executors recorded with `note_mission`, oldest first. The durable channel
185
+ * across sessions: the round that judges the mission is a fresh session reading only this node's
186
+ * prompt, and notes are appended, never replaced. */
187
+ readonly analysisNotes: readonly string[];
188
+ /** The `attempts` value of the dispatch that wrote the LAST entry; `0` when none. `decompose`
189
+ * compares it against the current `attempts`, so an executor cannot inherit a justification
190
+ * written by an earlier round. */
191
+ readonly analysisAttempt: number;
192
+ /** The session that wrote the LAST entry of {@link analysisNotes}; `null` when none, or on a record
193
+ * written before the field existed. The cold wake compares it against {@link DispatchBaseline}'s
194
+ * `holder` so a session's OWN note is not mistaken for a different dispatch's — which the
195
+ * generation number alone cannot tell once the same session's note outlives its dispatch. Persisted,
196
+ * `DOMAIN_VERSION` stays 1. */
197
+ readonly analysisAuthor: string | null;
198
+ readonly status: NodeStatus;
199
+ /** When this node was ACCEPTED into the tree (entered `ready`). The scheduling semantics are the
200
+ * reason this is not "when it started": capacity is a DISPATCH gate, not an admission gate (see
201
+ * `@avantf/mission-core/dispatch`), so a node can sit here for a long time waiting for room or for
202
+ * its unit. {@link dispatchedAt} minus this is the queue time. */
203
+ readonly createdAt: number;
204
+ /** When this node was dispatched for the FIRST time — the moment an executor was bound to it (the
205
+ * first transition into `running`, via `dispatch`/`adoptParked`/`adoptContinuation`). `null` means
206
+ * "never its turn yet": still queued for capacity or for a unit. Immutable after the first write, so
207
+ * a reclaim, a retry or a re-dispatch never rewrites it; a record written before this field existed
208
+ * loads as `null` (= never dispatched, the honest reading). Persisted, `DOMAIN_VERSION` stays 1. */
209
+ readonly dispatchedAt: number | null;
210
+ /** When this node entered a TERMINAL status (`done`/`failed`). `null` means "still in play" — ready,
211
+ * blocked, running or interrupted. Terminal is final, so this is written exactly once, by whichever
212
+ * path ends the node (`submitResult`, `failExhausted`, `cancelSubworks`, `cancelTree`); a node that
213
+ * is cancelled before ever running carries an `endedAt` with a `null` `dispatchedAt`. A record
214
+ * written before this field existed loads as `null` (= still in play). Persisted, `DOMAIN_VERSION`
215
+ * stays 1. */
216
+ readonly endedAt: number | null;
217
+ readonly depth: number;
218
+ /** Durable binding to the worker session id, persisted so a hot reload can tell "someone is
219
+ * still on this"; the agent's existence is always resolved live, never asserted from here. */
220
+ readonly claimedBy: string | null;
221
+ readonly claimedAt: number;
222
+ readonly attempts: number;
223
+ /** Execution-failure budget: how many times this node's worker was reclaimed (vanished or
224
+ * stalled) without producing a result. Separate from `attempts`, which counts EVERY dispatch —
225
+ * including successful aggregate/convergence rounds — so succeeding never burns this budget. */
226
+ readonly failures: number;
227
+ /** How many times in a row a dispatch failed to even START a worker (the runtime refused, or the
228
+ * tool filter could not be applied). An infrastructure failure, so it is budgeted separately
229
+ * from `failures` and pushes the next dispatch back by a cooldown; a successful start resets it. */
230
+ readonly spawnFailures: number;
231
+ /** The worker session that PARKED this node by decomposing it — a wake-up ADDRESS, not a claim:
232
+ * `claimedBy` is cleared exactly as usual, and the address is consumed by the next dispatch
233
+ * (adopted, or replaced by a fresh session). `nextDispatchable` excludes a parked node, because
234
+ * `decompose_mission` is followed by a synchronous `pump()` that would otherwise pre-empt the wake. */
235
+ readonly parkedWorker: string | null;
236
+ /** The session id of the worker most recently bound to this node, kept when the binding is
237
+ * dropped by an INTERRUPTION rather than by a clean hand-off — i.e. `reconcileOnOpen` demoting a
238
+ * `running` node whose worker is not materialized in this process (a restart or a crash), which
239
+ * is exactly when `claimedBy` alone loses the address for good. The next dispatch of this node
240
+ * first tries to CONTINUE that session (cold wake) instead of starting a fresh executor; a
241
+ * delivery the runtime refuses falls back to a brand-new session with no budget charged.
242
+ *
243
+ * Deliberately NOT the same thing as `parkedWorker`, and neither may overwrite the other:
244
+ * a parked worker is an ALIVE, actively waiting continuation (it decomposed and expects to be
245
+ * woken in the same run); `lastWorkerId` is a handle to a session that may no longer exist, and
246
+ * the cold resume that reaches it is allowed to fail. Only `reconcileOnOpen` writes it, and only
247
+ * `adoptContinuation` consumes it — a same-process reclaim does not, so ordinary re-dispatch
248
+ * behaviour is untouched. `null` for a node that was never dispatched or whose handle was spent,
249
+ * which is also the value a record written before this field existed loads as. */
250
+ readonly lastWorkerId: string | null;
251
+ /** The session id of the LAST executor this node was dispatched to — the display address behind
252
+ * the panel's "click the node id to open the session that ran it". Written at every transition
253
+ * INTO `running` (`dispatch` / `adoptParked` / `adoptContinuation`) and deliberately KEPT when the
254
+ * node leaves `running`: a finished mission must still be able to name who executed it, which is
255
+ * exactly what `claimedBy` (cleared on every exit) cannot do.
256
+ *
257
+ * A display address, NOT a continuation handle: nothing consumes it, and it is deliberately not
258
+ * `lastWorkerId` — that one is a one-shot cold-wake address that only `reconcileOnOpen` writes and
259
+ * only `adoptContinuation` spends, so overloading it would change ordinary re-dispatch behaviour (a
260
+ * same-process reclaim would start cold-resuming). A node dispatched several times keeps only the
261
+ * LAST attempt, because the projection carries one string and never a history array. `null` for a
262
+ * node that was never dispatched, which is also the value a record written before this field
263
+ * existed loads as. Persisted, `DOMAIN_VERSION` stays 1. */
264
+ readonly executorSessionId: string | null;
265
+ /** What the prompt of the LAST dispatch showed its session, stamped by the host the moment that
266
+ * prompt was ACCEPTED (not when the node was bound: a dispatch whose prompt was never built or
267
+ * never delivered must not leave a baseline claiming the session read something). The cold wake
268
+ * subtracts it to get the delta it renders, and the same delta decides whether continuing is honest
269
+ * at all; a FRESH spawn ignores it entirely, because a new executor has read nothing.
270
+ *
271
+ * Deliberately NOT the same thing as `analysisAttempt` (that is `note_mission`'s generation gate)
272
+ * and NOT derived from `correctionsDeliveredUpTo` (that is one half of the correction story, and
273
+ * the weaker half — see {@link DispatchBaseline}). `null` for a node never dispatched by a host
274
+ * that stamps baselines, which is also the value a record written before this field existed loads
275
+ * as; that direction is the safe one, because "unknown" renders an honest caveat and never a
276
+ * fabricated "nothing changed". */
277
+ readonly dispatchBaseline: DispatchBaseline | null;
278
+ /** When this node's worker last PRODUCED something: the model's committed output, a tool it asked
279
+ * for, or a tool that finished. This is what the stale check compares against — `progressAt` is
280
+ * about output, deliberately NOT about "an event arrived" (see {@link activityAt}); a long but
281
+ * productive run stays safe, while a provider that only retries stops refreshing it. `0` means
282
+ * "nothing produced yet" and falls back to `claimedAt`. */
283
+ readonly progressAt: number;
284
+ /** When this node's worker was last HEARD FROM at all — any durable session event, output or
285
+ * transport-layer noise (retry attempts, per-request route snapshots). Kept apart from
286
+ * `progressAt` so the engine can tell the two ways a live worker stops being useful: nothing at
287
+ * all (both go stale → `stalled`, which charges the failure budget) versus events with no output
288
+ * (this stays fresh while `progressAt` goes stale → `hung`, which charges nothing). `0` means
289
+ * never observed; the readers fall back to `claimedAt`, and a record written before this field
290
+ * existed loads as `0` — "cannot tell output from noise", the conservative direction that leaves
291
+ * the round cap to reclaim it rather than inventing a stall. Persisted, `DOMAIN_VERSION` stays 1. */
292
+ readonly activityAt: number;
293
+ /** Times this node was reclaimed because its worker went silent past the stale window; a worker
294
+ * that merely vanished is not the node's fault and does not count here. A `hung` reclaim is not
295
+ * counted either — see `MissionTree.reclaim` and {@link hungCount}. */
296
+ readonly stalls: number;
297
+ /** CONSECUTIVE `hung` reclaims since this node last PRODUCED something (real output, i.e. the
298
+ * `MissionTree.touchProgress` clock). Hung rounds charge no budget by design, so without this
299
+ * counter a worker that hangs every round re-runs forever and nothing ever reaches the owner. It is
300
+ * a STREAK, not a history: any real output clears it to 0, and a re-dispatch does NOT (the round
301
+ * after a hung reclaim is exactly the next link in the streak). At
302
+ * `CAPACITY.maxHungsBeforeReport` it trips `isTroubledNode`, which both flags the mission as
303
+ * 「反复出过问题」 and routes it to the owner through the same one-message-per-node trouble channel
304
+ * the stall and failed-start heads-ups use. `0` is also what a record written before the field
305
+ * existed loads as. Persisted, `DOMAIN_VERSION` stays 1. */
306
+ readonly hungCount: number;
307
+ /** When the owner was told this node keeps stalling; `null` until then. Durable for the same
308
+ * reason as `TreeRecord.reportedAt`: an in-memory memo is empty after a restart. */
309
+ readonly stalledNotifiedAt: number | null;
310
+ /** Inline result (the summary when the full text was spilled). */
311
+ readonly result: string | null;
312
+ /** Distinguishes "no result written" from "an empty result was written". */
313
+ readonly hasResult: boolean;
314
+ readonly resultReadAt: number | null;
315
+ readonly resultRef: string | null;
316
+ /** The backend's retrieval guidance for `resultRef`, persisted because a locator without its
317
+ * hint leaves the reader unable to fetch the full text. `null` when nothing was spilled. */
318
+ readonly resultHint: string | null;
319
+ /** Child ids in decomposition order. */
320
+ readonly children: readonly string[];
321
+ readonly updatedAt: number;
322
+ }
323
+ export interface ChildSpec {
324
+ readonly title: string;
325
+ readonly description: string;
326
+ readonly context: readonly string[];
327
+ /** This child's scope. `undefined` (the spec said nothing) inherits the parent's unit — the safe
328
+ * default; a non-blank string is the child's own scope; a blank string or `null` declares that
329
+ * this child takes no lease at all. Resolved by `resolveChildUnit`. */
330
+ readonly unit?: string | null;
331
+ /** This child's declared capacity weight, in cores-equivalent. `undefined` reads as the default 1
332
+ * and is deliberately NOT inherited from the parent — the parent's estimate is not the child's. */
333
+ readonly weight?: number;
334
+ /** This child's declared round-cap relaxation (see {@link NodeRecord.roundMs}). `undefined` reads
335
+ * as `null` — the engine's configured cap — and is deliberately NOT inherited from the parent. */
336
+ readonly roundMs?: number | null;
337
+ }
338
+ /** A dispatch candidate: the node plus its resolved mission chain and, exactly when the node is a
339
+ * ready aggregate, the current children results. */
340
+ export interface DispatchView {
341
+ readonly node: NodeRecord;
342
+ /** Ancestors root → parent, each reduced to title + basic facts. */
343
+ readonly chain: readonly NodeRecord[];
344
+ /** Terminal children of an aggregate, in decomposition order. */
345
+ readonly children: readonly NodeRecord[];
346
+ }
347
+ export interface DecomposeOutcome {
348
+ readonly created: readonly string[];
349
+ /** Children whose equivalent already existed in the subtree and were reused. */
350
+ readonly reused: readonly string[];
351
+ }
352
+ /** Why a mutation was refused. Returned instead of thrown so tools can report it. */
353
+ export type RefusalCode = 'not-found' | 'not-owner' | 'terminal'
354
+ /** The node still has non-terminal children, so the mutation it refused must wait. */
355
+ | 'has-children' | 'not-dispatchable' | 'depth-exceeded' | 'too-many-children' | 'no-children'
356
+ /** Nothing below the node is unfinished, so there is nothing to cancel. */
357
+ | 'nothing-to-cancel' | 'unread-result'
358
+ /** The addressed node is not a root, and only a root takes the mutation. */
359
+ | 'not-root' | 'node-limit'
360
+ /** The node is not terminal, so it is not the owner's to delete. */
361
+ | 'not-deletable' | 'closed'
362
+ /** The caller is not a top-level session, so it has no authority to own a tree. */
363
+ | 'no-authority'
364
+ /** The tool was reached with no caller agent bound at all, so there is nobody to check authority
365
+ * against. Produced by the tool layer — the one place a call can arrive callerless — and listed
366
+ * here so a consumer that exhausts this union does not silently drop it. */
367
+ | 'no-caller'
368
+ /** `note_mission` was given text with no non-blank line, so there is nothing to record. */
369
+ | 'no-analysis'
370
+ /** A required text argument reached the engine containing nothing but whitespace. The tool layer's
371
+ * non-empty check only sees the EMPTY string, so without this a blank title, result or correction
372
+ * was accepted and persisted — then rendered back into every later dispatch. */
373
+ | 'blank-text'
374
+ /** `decompose_mission` was called by a dispatch that has not written its own analysis: a split
375
+ * must be argued for by the round performing it, not inherited from an earlier one. */
376
+ | 'analysis-missing'
377
+ /** The node's declared `unit` is held by another `running` node, so dispatching it would put two
378
+ * executors on the same scope. Refused WITHOUT charging the node: the holder always leaves
379
+ * `running` on its own, and the node is a candidate again then (see
380
+ * `@avantf/mission-core/dispatch`). */
381
+ | 'unit-busy'
382
+ /** The node does not fit the machine RIGHT NOW — its weight would push `Σ running.weight` past
383
+ * `capacity`, or the `maxConcurrent` slot ceiling is reached. Produced by the lock-held recheck
384
+ * (`MissionTree.capacityRefusal`), which re-runs the plan's own judgement against live state so two
385
+ * concurrent passes cannot both bind from one stale snapshot. Refused WITHOUT charging the node:
386
+ * capacity is a DISPATCH gate, so this is exactly the plan path's deferral — the node stays
387
+ * `ready`, with no `attempts`/`failures`/`spawnFailures`, no cooldown and no stall. A caller must
388
+ * never report it as a dispatch failure (see `MissionEngine.pass`). */
389
+ | 'capacity-busy';
390
+ /** A refused mutation, with a stable code the caller can branch on. */
391
+ export interface Refusal {
392
+ readonly ok: false;
393
+ readonly code: RefusalCode;
394
+ readonly message: string;
395
+ }
396
+ /** A successful mutation. */
397
+ export interface Accepted<T> {
398
+ readonly ok: true;
399
+ readonly value: T;
400
+ }
401
+ export type MutationResult<T> = Accepted<T> | Refusal;
402
+ /** One tree as stored: identity plus nodes keyed by id. The durable and in-memory shapes are the
403
+ * same value; the store validates on read and writes it whole. */
404
+ export interface TreeDocument {
405
+ tree: TreeRecord;
406
+ nodes: Record<string, NodeRecord>;
407
+ }
408
+ export declare function refuse(code: RefusalCode, message: string): Refusal;
409
+ export declare function accept<T>(value: T): Accepted<T>;
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Well-formed text — the mission tree's LOCAL copy of the family's lone-surrogate repair, and the
3
+ * port the plugin injects the base's canonical implementation through.
4
+ *
5
+ * The defect has exactly one shape: a UTF-16 code unit in `D800–DFFF` with no partner (an unpaired
6
+ * high half, an unpaired low half, or half of an emoji). Such a string is not a valid Unicode scalar
7
+ * sequence, yet `JSON.stringify` emits it verbatim as the escape `"\ud800"`. JavaScript's own
8
+ * `JSON.parse` accepts that, so every test in this repository passes while a strict parser rejects
9
+ * the whole document — measured, not theorized:
10
+ *
11
+ * - `printf '"\\ud800"' | jq .` → `parse error: Invalid \uXXXX\uXXXX surrogate pair escape`;
12
+ * - Python's `json.load` accepts the document but `print` raises
13
+ * `UnicodeEncodeError: surrogates not allowed`;
14
+ * - the same half-code-unit also poisons FTS indexes, embeddings and the UI.
15
+ *
16
+ * The repair is `String.prototype.toWellFormed()` (Node ≥20): it replaces each lone surrogate with
17
+ * U+FFFD (`�`). {@link wellFormedText} uses the engine's own implementation when it exists and an
18
+ * EQUIVALENT `charCodeAt` scan when it does not, so an older Node degrades in behaviour, never in
19
+ * availability.
20
+ *
21
+ * ── this file is the DEGRADATION copy ───────────────────────────────────────────────────────────
22
+ *
23
+ * The CANONICAL implementation lives in the family base kit
24
+ * (`@avantf/dsh-plugin-base` → `src/kit/wellformed.ts`, interface v2, exported as `wellFormedText` /
25
+ * `wellFormedDeep`). The plugin loads that base at runtime and injects the loaded functions into the
26
+ * core through {@link WellFormedSource} (`TreeDeps.wellFormed`, `buildWorkerPrompt`'s third argument,
27
+ * `AvantfMissionHost.wellFormed`); {@link LOCAL_WELL_FORMED} is what runs only when the base is
28
+ * absent or predates v2 and does not carry the two functions. Per the root `AGENTS.md`, fixing the
29
+ * shared repair means one base release; this copy exists so a missing base still repairs instead of
30
+ * mounting without any repair at all. The plugin must NEVER import the base by value — it takes both
31
+ * halves off the module the inlined bootstrap loaded.
32
+ *
33
+ * ── NFC policy: well-formedness ONLY, deliberately no `normalize('NFC')` ─────────────────────────
34
+ *
35
+ * The base kit leaves Unicode normalization to the caller because it is a PERSISTENCE policy, and
36
+ * mission declines to add it. Mission's inbound texts are PROSE (titles, descriptions, analyses,
37
+ * results, corrections) that is rendered straight back to models; there is no index, no identity and
38
+ * no equivalence class that needs canonical spelling. The one consumer that could want it —
39
+ * decomposition dedup — compares trimmed, whitespace-collapsed, lowercased titles, which is already
40
+ * a heuristic and deliberately does NOT equate different descriptions. Applying NFC on the way in
41
+ * would silently rewrite the caller's own bytes, and mission has no caller that needs it; applying
42
+ * it on the way out would rewrite history. So ONE function serves both directions (this symmetry is
43
+ * also what makes the "inbound funnel" and the outbound fallback provably the same repair). A future
44
+ * feature that needs canonical identity must make that a separate, explicit write-side policy.
45
+ *
46
+ * @module @avantf/mission-core/wellformed
47
+ */
48
+ /**
49
+ * Make ONE string well-formed: every lone surrogate becomes U+FFFD, every properly paired surrogate
50
+ * (a complete emoji, a CJK Extension B character) and every other code point is returned unchanged.
51
+ *
52
+ * Uses the engine's `String.prototype.toWellFormed()` when available and {@link scanAndRepair}
53
+ * otherwise; both produce the same result, so a pre-ES2024 Node never throws here. Idempotent and
54
+ * pure: quotes, backslashes, newlines and tabs come back byte-for-byte, and no NFC is applied
55
+ * (see the module note).
56
+ */
57
+ export declare function wellFormedText(text: string): string;
58
+ /**
59
+ * {@link wellFormedText} over a JSON-shaped value, so a whole result can be handed to a strict parser.
60
+ *
61
+ * Recursion rules, deliberately narrow — this member may repair strings but must never change a
62
+ * value's TYPE:
63
+ *
64
+ * - strings are repaired with {@link wellFormedText};
65
+ * - arrays are mapped (a new array; the input is never mutated);
66
+ * - PLAIN records are rebuilt with well-formed keys and values. "Plain" means the prototype is
67
+ * `Object.prototype` (an object literal) or `null` (`Object.create(null)`), and the object carries
68
+ * no `toJSON` custom serialization. Object KEYS are repaired too: `JSON.stringify` emits a lone
69
+ * surrogate in a property name verbatim, which breaks a strict parser exactly the same way;
70
+ * - EVERYTHING ELSE — numbers, booleans, `null`, `undefined`, bigints, symbols, functions, and every
71
+ * object that is not a plain record (`Date`, `RegExp`, `Map`, a class instance, anything with a
72
+ * `toJSON`) — is returned AS-IS, by identity.
73
+ *
74
+ * Pure and type-preserving: the result is structurally identical to the input apart from lone
75
+ * surrogates, and the input value is never mutated.
76
+ */
77
+ export declare function wellFormedDeep<T>(value: T): T;
78
+ /**
79
+ * The pair of repairs every mission boundary needs. The plugin builds this from the base kit it
80
+ * loaded (`{ text: kit.wellFormedText, deep: kit.wellFormedDeep }`) and injects it; the core defaults
81
+ * to {@link LOCAL_WELL_FORMED}.
82
+ *
83
+ * The two members are separate because the boundaries are: a single model-written STRING (an
84
+ * analysis, a result, a correction, a command line) needs `text`, while a record or array that is
85
+ * about to be persisted or serialized (a `createRoot` input, a `children` array, a whole tool
86
+ * result, a dispatch view) needs `deep`. Both are pure.
87
+ */
88
+ export interface WellFormedSource {
89
+ /** Repair one string (see {@link wellFormedText}). */
90
+ readonly text: (value: string) => string;
91
+ /** Repair every string in a JSON-shaped value (see {@link wellFormedDeep}). */
92
+ readonly deep: <T>(value: T) => T;
93
+ }
94
+ /**
95
+ * The degradation source: the local copy above. Used when the loaded base kit has no
96
+ * `wellFormedText` / `wellFormedDeep` (base absent, or older than interface v2).
97
+ */
98
+ export declare const LOCAL_WELL_FORMED: WellFormedSource;
@@ -0,0 +1,3 @@
1
+ export declare function newClaimId(): string;
2
+ export declare function isWorkerClaimId(sessionId: string): boolean;
3
+ //# sourceMappingURL=claims.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claims.d.ts","sourceRoot":"","sources":["../../src/claims.ts"],"names":[],"mappings":"AASA,wBAAgB,UAAU,IAAI,MAAM,CAEnC;AAED,wBAAgB,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAE1D"}