@lmzhen/dsh-evolution-core 0.26.26 → 0.27.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/lib/composition/index.js +3 -0
- package/lib/composition-D8Y3-ROd.js +604 -0
- package/lib/ensure-MEYMj9jn.js +222 -0
- package/lib/errors-Br2-uUSK.js +57 -0
- package/lib/http-routes-BP-S3KV-.js +149 -0
- package/lib/http-routes.js +2 -0
- package/lib/index.js +66 -850
- package/lib/restrictor.js +37 -0
- package/lib/subagent/index.js +186 -0
- package/lib/types/citations.js +192 -0
- package/lib/types/{preset-composition.d.ts → composition/composer.d.ts} +1 -1
- package/lib/types/composition/composer.js +681 -0
- package/lib/types/composition/ensure.d.ts +10 -0
- package/lib/types/composition/ensure.js +95 -0
- package/lib/types/composition/index.d.ts +5 -0
- package/lib/types/composition/index.js +7 -0
- package/lib/types/composition/preset-ids.d.ts +17 -0
- package/lib/types/composition/preset-ids.js +21 -0
- package/lib/types/composition/restricted-preset.d.ts +36 -0
- package/lib/types/composition/restricted-preset.js +42 -0
- package/lib/types/constants.js +223 -0
- package/lib/types/cost.js +67 -0
- package/lib/types/curator.js +346 -0
- package/lib/types/diagnostics.d.ts +26 -0
- package/lib/types/diagnostics.js +23 -0
- package/lib/types/document-body.js +91 -0
- package/lib/types/drift-signals.js +319 -0
- package/lib/types/env.js +25 -0
- package/lib/types/errors.d.ts +4 -1
- package/lib/types/errors.js +59 -0
- package/lib/types/events.js +16 -0
- package/lib/types/evidence.js +65 -0
- package/lib/types/evolution-events.js +479 -0
- package/lib/types/fold-untrusted.js +44 -0
- package/lib/types/frontmatter.js +623 -0
- package/lib/types/fuzzy-match.js +135 -0
- package/lib/types/gates.js +51 -0
- package/lib/types/guards.js +27 -0
- package/lib/types/health-baseline.js +63 -0
- package/lib/types/http-routes.d.ts +67 -35
- package/lib/types/http-routes.js +135 -0
- package/lib/types/index.d.ts +7 -2
- package/lib/types/index.js +101 -0
- package/lib/types/instance-scope.js +69 -0
- package/lib/types/io.js +675 -0
- package/lib/types/learn-prompt.js +42 -0
- package/lib/types/limits.js +63 -0
- package/lib/types/live.js +107 -0
- package/lib/types/long-runs.js +96 -0
- package/lib/types/memory-store.d.ts +0 -16
- package/lib/types/memory-store.js +606 -0
- package/lib/types/mutations.js +95 -0
- package/lib/types/namespace-migration.js +156 -0
- package/lib/types/numeric.js +47 -0
- package/lib/types/opt-in.js +101 -0
- package/lib/types/package-identity.js +47 -0
- package/lib/types/params.js +328 -0
- package/lib/types/payload.js +36 -0
- package/lib/types/platform-line.generated.d.ts +10 -0
- package/lib/types/platform-line.generated.js +12 -0
- package/lib/types/platform-services.d.ts +2 -2
- package/lib/types/platform-services.js +52 -0
- package/lib/types/probe.js +122 -0
- package/lib/types/prompts.js +409 -0
- package/lib/types/quality.js +308 -0
- package/lib/types/reachability.js +122 -0
- package/lib/types/redact.js +257 -0
- package/lib/types/reference-rewrite.js +144 -0
- package/lib/types/reports.js +110 -0
- package/lib/types/restrictor.d.ts +11 -0
- package/lib/types/restrictor.js +48 -0
- package/lib/types/review-channel.js +59 -0
- package/lib/types/run-registry.js +345 -0
- package/lib/types/scope.js +7 -0
- package/lib/types/serial.js +17 -0
- package/lib/types/session-fold.d.ts +15 -0
- package/lib/types/session-fold.js +28 -0
- package/lib/types/session-projection.js +295 -0
- package/lib/types/signals.js +209 -0
- package/lib/types/skill-discovery.js +66 -0
- package/lib/types/skill-health.js +134 -0
- package/lib/types/skill-history.js +745 -0
- package/lib/types/skill-lint.js +112 -0
- package/lib/types/skill-reads.js +67 -0
- package/lib/types/skill-row-cells.js +123 -0
- package/lib/types/skill-store.js +3528 -0
- package/lib/types/state-store.js +60 -0
- package/lib/types/store-ledger.js +117 -0
- package/lib/types/subagent/channel.d.ts +4 -0
- package/lib/types/subagent/channel.js +131 -0
- package/lib/types/subagent/index.d.ts +4 -0
- package/lib/types/subagent/index.js +5 -0
- package/lib/types/subagent/readiness.d.ts +6 -0
- package/lib/types/subagent/readiness.js +35 -0
- package/lib/types/subagent/types.d.ts +58 -0
- package/lib/types/subagent/types.js +2 -0
- package/lib/types/threats.js +338 -0
- package/lib/types/tool-dispatch.d.ts +5 -0
- package/lib/types/tool-dispatch.js +535 -0
- package/lib/types/tree-write.js +40 -0
- package/lib/types/unit-members.js +121 -0
- package/lib/types/usage.js +474 -0
- package/lib/types/write-authority.js +23 -0
- package/lib/types/write-gates.js +344 -0
- package/lib/types/write-inventory.js +158 -0
- package/package.json +39 -11
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.19.0 (S2) / 0.19.3 (restructure): the family's run-lifecycle owner.
|
|
3
|
+
*
|
|
4
|
+
* A "run" is work that can outlive one interaction — today the maintenance scan.
|
|
5
|
+
* Before this module the only record of such work was the requesting invocation:
|
|
6
|
+
* `lastMaintainAt` / `lastMaintainRunId` / `maintainInFlightSince` were
|
|
7
|
+
* module-scope globals in evolution-commands, and the plan text existed ONLY in
|
|
8
|
+
* the command's return string. An interaction that ended (measured: the desktop
|
|
9
|
+
* shell hangs up at ~305 s) therefore erased every trace of a still-running scan.
|
|
10
|
+
*
|
|
11
|
+
* This registry is the single owner of (a) what is in flight, (b) how a run
|
|
12
|
+
* ended, and (c) WHERE its result lives (`resultRef` is a path, never the result
|
|
13
|
+
* itself — the report file is the one result home).
|
|
14
|
+
*
|
|
15
|
+
* Three disciplines are structural, not stylistic:
|
|
16
|
+
*
|
|
17
|
+
* - **One writer per row** (0.19.3). A run's row is authoritative in the memory of
|
|
18
|
+
* the process that STARTED it (`owned`) and in the shared index for everyone else
|
|
19
|
+
* (`foreign`, rebuilt from the file on every `load()`). The only write a non-owner
|
|
20
|
+
* may make is the orphan verdict for a row whose owner is gone, judged inside the
|
|
21
|
+
* very transaction that read the row. The two cross-plane bugs this family shipped
|
|
22
|
+
* were projections of that one missing rule: 0.19.0 froze a foreign run as
|
|
23
|
+
* `running` forever (a "known id" was never re-read), and 0.19.1 wrote a settled
|
|
24
|
+
* foreign row back as `running` (memory overwrote every row it knew).
|
|
25
|
+
* - **No timer.** A run whose process died is converged at READ time: the live rows
|
|
26
|
+
* are in memory, so a `running` row in the file whose owner is not alive is an
|
|
27
|
+
* orphan — the same "converge when read" posture the approval window uses.
|
|
28
|
+
* - **One terminal state, and it is monotone.** `settle()` refuses to overwrite a row
|
|
29
|
+
* that is already terminal (so a cancel that raced a failure cannot flip the answer
|
|
30
|
+
* the operator already read), and a row that reached a terminal state never reads as
|
|
31
|
+
* `running` again: only the owner that started it could make it running and
|
|
32
|
+
* `settle()` is terminal-once, so a `running` claim for a settled row can only be a
|
|
33
|
+
* stale writer's — refused on read, repaired on the next write.
|
|
34
|
+
*
|
|
35
|
+
* The store is PER INSTANCE (a factory, not module scope): one registry per
|
|
36
|
+
* mounted row, disposed with its fiber. There is no module-scope mutable state
|
|
37
|
+
* here to register under N12.
|
|
38
|
+
* @module
|
|
39
|
+
*/
|
|
40
|
+
import { randomUUID } from 'node:crypto';
|
|
41
|
+
import { join } from 'node:path';
|
|
42
|
+
import { clampedNumber } from "./numeric.js";
|
|
43
|
+
import { isProcessAlive, transactIo } from "./io.js";
|
|
44
|
+
/** Current index schema. Bump only for a structural change. */
|
|
45
|
+
export const RUNS_SCHEMA_VERSION = 1;
|
|
46
|
+
/** Retained run records, newest first. */
|
|
47
|
+
export const DEFAULT_RUN_RECORDS = 50;
|
|
48
|
+
/** Index path for a home. */
|
|
49
|
+
export function runsFile(home) {
|
|
50
|
+
return join(home, 'runs.json');
|
|
51
|
+
}
|
|
52
|
+
const TERMINAL = new Set(['succeeded', 'failed', 'cancelled']);
|
|
53
|
+
/**
|
|
54
|
+
* Parse an index body into records.
|
|
55
|
+
*
|
|
56
|
+
* `ok: false` means the body could not be read as an index at all — the caller
|
|
57
|
+
* decides what to say (`load()` reports it; `persist()` merges with what it can).
|
|
58
|
+
* @param raw - the file's text, or null when it does not exist.
|
|
59
|
+
* @returns the parsed records and whether the body was an index.
|
|
60
|
+
*/
|
|
61
|
+
function parseIndex(raw) {
|
|
62
|
+
if (raw === null)
|
|
63
|
+
return { ok: true, records: [], foreign: [] };
|
|
64
|
+
let parsed;
|
|
65
|
+
try {
|
|
66
|
+
parsed = JSON.parse(raw);
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { ok: false, records: [], foreign: [] };
|
|
70
|
+
}
|
|
71
|
+
const rows = parsed.runs;
|
|
72
|
+
if (!Array.isArray(rows))
|
|
73
|
+
return { ok: false, records: [], foreign: [] };
|
|
74
|
+
const notes = [];
|
|
75
|
+
const version = parsed.schemaVersion;
|
|
76
|
+
if (typeof version === 'number' && version !== RUNS_SCHEMA_VERSION) {
|
|
77
|
+
notes.push(`the index declares schemaVersion ${version} while this build writes ${RUNS_SCHEMA_VERSION}`);
|
|
78
|
+
}
|
|
79
|
+
const records = [];
|
|
80
|
+
const foreign = [];
|
|
81
|
+
for (const row of rows) {
|
|
82
|
+
const record = row;
|
|
83
|
+
if (typeof record.id !== 'string' || typeof record.startedAt !== 'number') {
|
|
84
|
+
foreign.push(row);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (typeof record.kind !== 'string' || typeof record.state !== 'string') {
|
|
88
|
+
foreign.push(row);
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (record.state !== 'running' && !TERMINAL.has(record.state)) {
|
|
92
|
+
// A state this build does not know (a newer writer, or a hand-edited file):
|
|
93
|
+
// it is KEPT as it is — dropping it would silently erase another writer's record.
|
|
94
|
+
foreign.push(row);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
records.push({
|
|
98
|
+
id: record.id,
|
|
99
|
+
kind: record.kind,
|
|
100
|
+
state: record.state,
|
|
101
|
+
startedAt: record.startedAt,
|
|
102
|
+
...(typeof record.pid === 'number' ? { pid: record.pid } : {}),
|
|
103
|
+
...(typeof record.endedAt === 'number' ? { endedAt: record.endedAt } : {}),
|
|
104
|
+
...(typeof record.resultRef === 'string' ? { resultRef: record.resultRef } : {}),
|
|
105
|
+
...(typeof record.failure === 'string' ? { failure: record.failure } : {}),
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return { ok: true, records, foreign, ...(notes.length > 0 ? { note: notes.join('; ') } : {}) };
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Create the run registry for one mounted row.
|
|
112
|
+
* @param options - io seam, evolution home, warn channel, clock and record cap.
|
|
113
|
+
* @returns the registry; call {@link RunRegistry.load} once before answering reads.
|
|
114
|
+
*/
|
|
115
|
+
export function newRunRegistry(options) {
|
|
116
|
+
const now = options.now ?? (() => Date.now());
|
|
117
|
+
const maxRecords = clampedNumber(options.maxRecords ?? DEFAULT_RUN_RECORDS, DEFAULT_RUN_RECORDS, { min: 1 });
|
|
118
|
+
const warn = options.warn ?? (() => { });
|
|
119
|
+
const path = runsFile(options.home);
|
|
120
|
+
/** Rows THIS process started: its memory is the only truth for them. */
|
|
121
|
+
const owned = new Map();
|
|
122
|
+
/** Rows read from the shared index: the file is the only truth for them. */
|
|
123
|
+
const foreign = new Map();
|
|
124
|
+
/** Cancellation handles — "can this still be stopped", NOT ownership (`settle` drops one). */
|
|
125
|
+
const controllers = new Map();
|
|
126
|
+
/** The ONE wording of the orphan verdict. */
|
|
127
|
+
const ORPHAN_FAILURE = 'no live owner process — the run was interrupted before it settled (orphan)';
|
|
128
|
+
/** A row whose owner cannot be reached: `pid` absent (an older build) or not alive. */
|
|
129
|
+
const ownerIsGone = (row) => row.pid === undefined || !isProcessAlive(row.pid);
|
|
130
|
+
/** The verdict is materialized ONCE, so its `endedAt` (and the duration a reader sees) is stable. */
|
|
131
|
+
const orphanOf = (row) => ({ ...row, state: 'failed', endedAt: now(), failure: ORPHAN_FAILURE });
|
|
132
|
+
/** Every row this process knows, newest first. ALL reads go through here. */
|
|
133
|
+
const view = () => [...owned.values(), ...foreign.values()].sort((a, b) => b.startedAt - a.startedAt);
|
|
134
|
+
/** Retained history: every running row plus the newest terminal ones (one cap, both planes). */
|
|
135
|
+
const capped = (rows) => {
|
|
136
|
+
const live = rows.filter(row => row.state === 'running');
|
|
137
|
+
const done = rows
|
|
138
|
+
.filter(row => TERMINAL.has(row.state))
|
|
139
|
+
.sort((a, b) => (b.endedAt ?? b.startedAt) - (a.endedAt ?? a.startedAt))
|
|
140
|
+
.slice(0, maxRecords);
|
|
141
|
+
return [...live, ...done].sort((a, b) => b.startedAt - a.startedAt);
|
|
142
|
+
};
|
|
143
|
+
/**
|
|
144
|
+
* Update a surviving foreign row in place (identity stable), or add it.
|
|
145
|
+
*
|
|
146
|
+
* Monotone states (0.19.3): a row already known TERMINAL is never shown as `running`
|
|
147
|
+
* again. Only the owner that started a run could make it running, and `settle()` is
|
|
148
|
+
* terminal-once — so a `running` claim for a settled row can only come from a stale
|
|
149
|
+
* writer (an older plane holding an outdated view), and adopting it would propagate the
|
|
150
|
+
* resurrection to every other plane.
|
|
151
|
+
* @returns true when the row was refused as exactly that regression.
|
|
152
|
+
*/
|
|
153
|
+
const adopt = (row) => {
|
|
154
|
+
const existing = foreign.get(row.id);
|
|
155
|
+
if (existing === undefined) {
|
|
156
|
+
foreign.set(row.id, row);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
if (TERMINAL.has(existing.state) && !TERMINAL.has(row.state))
|
|
160
|
+
return true;
|
|
161
|
+
// Clear what the new row dropped, so nothing is inherited from the previous view.
|
|
162
|
+
delete existing.endedAt;
|
|
163
|
+
delete existing.resultRef;
|
|
164
|
+
delete existing.failure;
|
|
165
|
+
Object.assign(existing, row);
|
|
166
|
+
return false;
|
|
167
|
+
};
|
|
168
|
+
const persist = async () => {
|
|
169
|
+
try {
|
|
170
|
+
// Read-modify-write under the io backend's cross-process lock. The file is the
|
|
171
|
+
// BASE and only THIS process's rows are laid over it ("one writer per row"); the
|
|
172
|
+
// single write a non-owner may make is the orphan verdict for a row whose owner is
|
|
173
|
+
// gone. The row read inside this transaction IS the compare-and-swap: if its owner
|
|
174
|
+
// settled it in between, it no longer reads as `running` and is left alone.
|
|
175
|
+
await transactIo(options.io, path, (current) => {
|
|
176
|
+
const onDisk = parseIndex(current);
|
|
177
|
+
const merged = new Map();
|
|
178
|
+
for (const row of onDisk.records)
|
|
179
|
+
merged.set(row.id, row);
|
|
180
|
+
for (const [id, row] of owned)
|
|
181
|
+
merged.set(id, row);
|
|
182
|
+
// Repair a stale writer's regression: this process READ the owner's terminal
|
|
183
|
+
// verdict, so re-asserting it is not authoring state — it refuses to propagate a
|
|
184
|
+
// stale overwrite. Without this, a fresh plane would see the resurrected `running`
|
|
185
|
+
// row and hold its in-flight guard (the 0.19.0 symptom, one plane later).
|
|
186
|
+
for (const [id, row] of foreign) {
|
|
187
|
+
const onDiskRow = merged.get(id);
|
|
188
|
+
if (TERMINAL.has(row.state) && onDiskRow !== undefined && !TERMINAL.has(onDiskRow.state))
|
|
189
|
+
merged.set(id, row);
|
|
190
|
+
}
|
|
191
|
+
for (const [id, row] of merged) {
|
|
192
|
+
if (row.state === 'running' && !owned.has(id) && ownerIsGone(row))
|
|
193
|
+
merged.set(id, orphanOf(row));
|
|
194
|
+
}
|
|
195
|
+
const kept = capped([...merged.values()]);
|
|
196
|
+
const live = kept.filter(row => row.state === 'running');
|
|
197
|
+
const done = kept.filter(row => TERMINAL.has(row.state));
|
|
198
|
+
// Rows this build does not understand ride along untouched (P2-5): it cannot
|
|
199
|
+
// judge them, and dropping them would erase another writer's record.
|
|
200
|
+
return JSON.stringify({ schemaVersion: RUNS_SCHEMA_VERSION, runs: [...live, ...done, ...onDisk.foreign] }, null, 2);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
warn(`evolution-core: could not persist the run index (${error instanceof Error ? error.message : String(error)})`);
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
|
+
return {
|
|
208
|
+
async load() {
|
|
209
|
+
let raw;
|
|
210
|
+
try {
|
|
211
|
+
raw = await options.io.readText(path);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
// A read FAILURE is not "no runs": say so instead of presenting an empty
|
|
215
|
+
// registry as a clean history (the same three-state rule the probe files use).
|
|
216
|
+
return { ok: false, note: `run index unreadable: ${error instanceof Error ? error.message : String(error)}` };
|
|
217
|
+
}
|
|
218
|
+
const parsed = parseIndex(raw);
|
|
219
|
+
if (!parsed.ok) {
|
|
220
|
+
return { ok: false, note: '运行索引读不出来(不是 JSON,或没有 runs 数组)—— 上一次的运行历史未知(报告不受影响)' };
|
|
221
|
+
}
|
|
222
|
+
// The foreign view is REBUILT wholesale: a row this process does not own is whatever
|
|
223
|
+
// the index says it is, every time. (0.19.0 kept "known" ids frozen, which is how a
|
|
224
|
+
// settled foreign run answered `running` until the observing process restarted.)
|
|
225
|
+
// Surviving rows are updated in place so a reader that held one across the load
|
|
226
|
+
// keeps a live view; rows that vanished from the index are dropped.
|
|
227
|
+
const wasRunning = new Set([...foreign.values()].filter(row => row.state === 'running').map(row => row.id));
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
let converged = 0;
|
|
230
|
+
let liveElsewhere = 0;
|
|
231
|
+
let followed = 0;
|
|
232
|
+
let regressed = 0;
|
|
233
|
+
for (const row of parsed.records) {
|
|
234
|
+
if (owned.has(row.id))
|
|
235
|
+
continue;
|
|
236
|
+
seen.add(row.id);
|
|
237
|
+
if (row.state !== 'running') {
|
|
238
|
+
if (wasRunning.has(row.id))
|
|
239
|
+
followed++;
|
|
240
|
+
if (adopt(row))
|
|
241
|
+
regressed++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
// A running row whose OWNER is alive belongs to another process sharing this home:
|
|
245
|
+
// genuinely in flight, never rewritten here (review P1-1). A row that cannot name
|
|
246
|
+
// its owner is treated as gone — the conservative side, and what a row written by
|
|
247
|
+
// an older build looks like.
|
|
248
|
+
if (ownerIsGone(row)) {
|
|
249
|
+
converged++;
|
|
250
|
+
adopt(orphanOf(row));
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
liveElsewhere++;
|
|
254
|
+
if (adopt(row))
|
|
255
|
+
regressed++;
|
|
256
|
+
}
|
|
257
|
+
for (const id of [...foreign.keys()]) {
|
|
258
|
+
if (!seen.has(id))
|
|
259
|
+
foreign.delete(id);
|
|
260
|
+
}
|
|
261
|
+
if (converged > 0 || regressed > 0)
|
|
262
|
+
await persist();
|
|
263
|
+
const notes = [
|
|
264
|
+
parsed.note,
|
|
265
|
+
converged > 0 ? `${converged} run(s) with no live owner were recorded as failed(orphan)` : undefined,
|
|
266
|
+
liveElsewhere > 0 ? `${liveElsewhere} run(s) are in flight in another process sharing this home` : undefined,
|
|
267
|
+
followed > 0 ? `${followed} run(s) settled by their owner were re-read from the index` : undefined,
|
|
268
|
+
regressed > 0 ? `${regressed} settled run(s) stayed settled although the index claimed they were running` : undefined,
|
|
269
|
+
].filter((note) => note !== undefined);
|
|
270
|
+
return { ok: true, ...(notes.length > 0 ? { note: notes.join('; ') } : {}) };
|
|
271
|
+
},
|
|
272
|
+
async begin(kind) {
|
|
273
|
+
const id = randomUUID();
|
|
274
|
+
const startedAt = now();
|
|
275
|
+
const controller = new AbortController();
|
|
276
|
+
controllers.set(id, controller);
|
|
277
|
+
// The pid is what lets ANOTHER process tell this live run from an orphan.
|
|
278
|
+
owned.set(id, { id, kind, state: 'running', startedAt, pid: process.pid });
|
|
279
|
+
// Write-through (0.19.1, review P2): answering "started" promises a run another
|
|
280
|
+
// plane can read, so the row reaches the index before this resolves. `persist()`
|
|
281
|
+
// warns instead of throwing, so an unwritable home cannot withhold the handle.
|
|
282
|
+
await persist();
|
|
283
|
+
return { id, kind, signal: controller.signal, startedAt };
|
|
284
|
+
},
|
|
285
|
+
async settle(id, outcome) {
|
|
286
|
+
const row = owned.get(id);
|
|
287
|
+
// Terminal-once: a cancel that raced the run's own failure must not flip the
|
|
288
|
+
// answer the operator already read.
|
|
289
|
+
if (row === undefined || TERMINAL.has(row.state))
|
|
290
|
+
return;
|
|
291
|
+
row.state = outcome.state;
|
|
292
|
+
row.endedAt = now();
|
|
293
|
+
if (outcome.resultRef !== undefined)
|
|
294
|
+
row.resultRef = outcome.resultRef;
|
|
295
|
+
if (outcome.failure !== undefined)
|
|
296
|
+
row.failure = outcome.failure;
|
|
297
|
+
controllers.delete(id);
|
|
298
|
+
await persist();
|
|
299
|
+
},
|
|
300
|
+
// The registry tracks ONE kind today (`RunKind` = 'maintain'), so these reads take no
|
|
301
|
+
// kind filter — a comparison the type system already knows is true. Re-add the
|
|
302
|
+
// parameter (and the filter) together with the second `RunKind` member.
|
|
303
|
+
inFlight() {
|
|
304
|
+
return view().find(row => row.state === 'running');
|
|
305
|
+
},
|
|
306
|
+
lastSettledAt() {
|
|
307
|
+
return view().find(row => TERMINAL.has(row.state))?.endedAt;
|
|
308
|
+
},
|
|
309
|
+
runs() {
|
|
310
|
+
return capped(view());
|
|
311
|
+
},
|
|
312
|
+
find(id) {
|
|
313
|
+
return owned.get(id) ?? foreign.get(id);
|
|
314
|
+
},
|
|
315
|
+
cancel(id) {
|
|
316
|
+
// Only work THIS process started can be stopped. A running row loaded from the
|
|
317
|
+
// index belongs to another process sharing this home (review P1-1), and marking it
|
|
318
|
+
// cancelled here would fabricate a terminal state for work that is still running
|
|
319
|
+
// there — so an unowned id is refused, not "cancelled". (`controllers` is the
|
|
320
|
+
// right test here precisely because it is about stopping, not about authority.)
|
|
321
|
+
const controller = controllers.get(id);
|
|
322
|
+
if (controller === undefined)
|
|
323
|
+
return false;
|
|
324
|
+
const row = owned.get(id);
|
|
325
|
+
if (row === undefined || row.state !== 'running')
|
|
326
|
+
return false;
|
|
327
|
+
controller.abort();
|
|
328
|
+
controllers.delete(id);
|
|
329
|
+
row.state = 'cancelled';
|
|
330
|
+
row.endedAt = now();
|
|
331
|
+
row.failure = 'cancelled by operator';
|
|
332
|
+
void persist();
|
|
333
|
+
return true;
|
|
334
|
+
},
|
|
335
|
+
cancelAll() {
|
|
336
|
+
let count = 0;
|
|
337
|
+
for (const id of [...controllers.keys()]) {
|
|
338
|
+
if (this.cancel(id))
|
|
339
|
+
count++;
|
|
340
|
+
}
|
|
341
|
+
return count;
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
//# sourceMappingURL=run-registry.js.map
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A process-local serial task queue: each task starts only after the previous
|
|
3
|
+
* one settles (success or failure), so read-modify-write sequences that share
|
|
4
|
+
* one file never interleave inside this process. The durable cross-process
|
|
5
|
+
* serialization layer is the IO backend's transact lock; this chain is the
|
|
6
|
+
* second layer (0.3.17 S2.8, T-1: the shape was duplicated in state-json and
|
|
7
|
+
* memory-files — one factory now).
|
|
8
|
+
*/
|
|
9
|
+
export function makeSerialQueue() {
|
|
10
|
+
let chain = Promise.resolve();
|
|
11
|
+
return (task) => {
|
|
12
|
+
const run = chain.then(task, task);
|
|
13
|
+
chain = run.then(() => undefined, () => undefined);
|
|
14
|
+
return run;
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=serial.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export type SessionEventClass = 'model' | 'plugin-record' | 'unknown';
|
|
2
|
+
/** Frames the model authored/saw (the only frames evidence, reads and usage accounting may consume). */
|
|
3
|
+
import { MODEL_SESSION_EVENT_TYPES } from './tool-dispatch.ts';
|
|
4
|
+
export { MODEL_SESSION_EVENT_TYPES as MODEL_SESSION_EVENTS };
|
|
5
|
+
/** Classify one session event WITHOUT interpreting it (pure). */
|
|
6
|
+
export declare function classifySessionEvent(event: {
|
|
7
|
+
readonly type?: unknown;
|
|
8
|
+
readonly ignorable?: unknown;
|
|
9
|
+
}): SessionEventClass;
|
|
10
|
+
/** True when the event must be skipped by evidence/usage readers (tolerant, never fatal). */
|
|
11
|
+
export declare function isIgnorableEvent(event: {
|
|
12
|
+
readonly type?: unknown;
|
|
13
|
+
readonly ignorable?: unknown;
|
|
14
|
+
}): boolean;
|
|
15
|
+
//# sourceMappingURL=session-fold.d.ts.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Session-event classification for EVERY family reader (G6-S1; C-03/C-05).
|
|
2
|
+
//
|
|
3
|
+
// Platform contract: native session events are the model-visible content frames; a plugin record is
|
|
4
|
+
// an `ignorable: true` event whose type is `plugin:<name>` (session/src/index.ts pluginRecordOf).
|
|
5
|
+
// Family readers must classify BEFORE they count or index: unknown frames are tolerated (never
|
|
6
|
+
// interpreted, never written), plugin records are tolerated read-only. The family never appends
|
|
7
|
+
// plugin records.
|
|
8
|
+
// The platform rule is `pluginRecordOf(event) = ignorable && type startsWith('plugin:')`
|
|
9
|
+
// (session/src/index.ts). Spelled structurally here: the preflight's machine-state resolution can
|
|
10
|
+
// point at an older session closure than the platform line this build targets, so importing the
|
|
11
|
+
// helper would make the contract depend on the bed rather than on the rule.
|
|
12
|
+
/** Frames the model authored/saw (the only frames evidence, reads and usage accounting may consume). */
|
|
13
|
+
import { MODEL_SESSION_EVENT_TYPES } from "./tool-dispatch.js";
|
|
14
|
+
export { MODEL_SESSION_EVENT_TYPES as MODEL_SESSION_EVENTS };
|
|
15
|
+
const MODEL_EVENT_TYPES = new Set(MODEL_SESSION_EVENT_TYPES);
|
|
16
|
+
/** Classify one session event WITHOUT interpreting it (pure). */
|
|
17
|
+
export function classifySessionEvent(event) {
|
|
18
|
+
const type = typeof event.type === 'string' ? event.type : '';
|
|
19
|
+
if (type.startsWith('plugin:')) {
|
|
20
|
+
return event.ignorable === true ? 'plugin-record' : 'unknown';
|
|
21
|
+
}
|
|
22
|
+
return MODEL_EVENT_TYPES.has(type) ? 'model' : 'unknown';
|
|
23
|
+
}
|
|
24
|
+
/** True when the event must be skipped by evidence/usage readers (tolerant, never fatal). */
|
|
25
|
+
export function isIgnorableEvent(event) {
|
|
26
|
+
return classifySessionEvent(event) !== 'model';
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=session-fold.js.map
|