@vincemakes/kiso-runtime 0.45.2 → 0.46.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/dist/internal.d.ts +2 -0
- package/dist/internal.js +4 -0
- package/dist/profile.js +13 -7
- package/dist/run.d.ts +22 -3
- package/dist/run.js +75 -23
- package/dist/session.d.ts +19 -0
- package/dist/session.js +66 -5
- package/dist/store.js +14 -4
- package/dist/tasks/delivery.d.ts +82 -0
- package/dist/tasks/delivery.js +482 -0
- package/dist/tasks/journal.d.ts +150 -0
- package/dist/tasks/journal.js +124 -0
- package/dist/tasks/manager.d.ts +219 -0
- package/dist/tasks/manager.js +438 -0
- package/package.json +6 -6
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-0058 §6 — the TaskManager: one per session, the only door to a
|
|
3
|
+
* session's tasks.
|
|
4
|
+
*
|
|
5
|
+
* It owns the task directory (`<store root>/<session>.tasks/<id>/`, holding
|
|
6
|
+
* `journal.jsonl` and `output.log`), allocates ids (`t1`, `t2`, … — stable
|
|
7
|
+
* across restarts, read off the directory), writes the kiso-side records
|
|
8
|
+
* durably, and classifies every task from its journal plus a verified
|
|
9
|
+
* runner identity. It never spawns a process itself: a BACKEND does (the
|
|
10
|
+
* process backend lives with the shell, in tools-node), and any number of
|
|
11
|
+
* hosts reach tasks through this one manager — never by searching for a
|
|
12
|
+
* child process themselves.
|
|
13
|
+
*
|
|
14
|
+
* Two ways in: `start` — a runner owns the command (background work, it
|
|
15
|
+
* survives kiso); `adopt` — a foreground shell command promoted past its
|
|
16
|
+
* wait, which the kiso process that spawned it keeps owning. An adopted
|
|
17
|
+
* task's runner IS that kiso process: if kiso crashes the journal reads
|
|
18
|
+
* `unknown`, never ended, never re-run (ADR-0058 3b, D1).
|
|
19
|
+
*
|
|
20
|
+
* What it does not do (later steps): tell the model (3c), or decide whether
|
|
21
|
+
* an idle session wakes (3c).
|
|
22
|
+
*/
|
|
23
|
+
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { appendRecord, fsyncDir, readRecords, runnerOf, verdictOf } from "./journal.js";
|
|
26
|
+
/** The name a task's end (or its runner's vanishing, or a start that never
|
|
27
|
+
* ran) has for the model — as a notice says it, and as a claim records it. */
|
|
28
|
+
export function endTransitionOf(task) {
|
|
29
|
+
const s = task.state;
|
|
30
|
+
if (s.kind === "ended") {
|
|
31
|
+
if (task.stoppedBy !== undefined)
|
|
32
|
+
return "stopped";
|
|
33
|
+
return s.exitCode === 0 && s.error === undefined ? "exited" : "failed";
|
|
34
|
+
}
|
|
35
|
+
if (s.kind === "unknown")
|
|
36
|
+
return "unknown";
|
|
37
|
+
if (s.kind === "not_run")
|
|
38
|
+
return "failed";
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const JOURNAL = "journal.jsonl";
|
|
42
|
+
const OUTPUT = "output.log";
|
|
43
|
+
const ID = /^t(\d+)$/;
|
|
44
|
+
export class TaskManager {
|
|
45
|
+
root;
|
|
46
|
+
#backend;
|
|
47
|
+
#listeners = new Set();
|
|
48
|
+
#pollMs;
|
|
49
|
+
#identifyEveryMs;
|
|
50
|
+
/** id → the last identity verdict for its runner (pid + start time). */
|
|
51
|
+
#identity = new Map();
|
|
52
|
+
#timer = null;
|
|
53
|
+
/** id → the last observed kind ("ready" for a running task that has signalled) */
|
|
54
|
+
#seen = new Map();
|
|
55
|
+
/** id → how to stop an adopted task this process owns */
|
|
56
|
+
#local = new Map();
|
|
57
|
+
/** executionId → a running foreground command that can be detached (3e) */
|
|
58
|
+
#detachable = new Map();
|
|
59
|
+
/** id → how many `awaitSettled` calls wait on it: the watcher skips it */
|
|
60
|
+
#waiting = new Map();
|
|
61
|
+
constructor(options) {
|
|
62
|
+
this.root = options.root;
|
|
63
|
+
this.#backend = options.backend;
|
|
64
|
+
if (options.onTransition !== undefined)
|
|
65
|
+
this.#listeners.add(options.onTransition);
|
|
66
|
+
this.#pollMs = options.pollMs ?? 500;
|
|
67
|
+
this.#identifyEveryMs = options.identifyEveryMs ?? 5_000;
|
|
68
|
+
}
|
|
69
|
+
/** A runner's identity (Windows P6): "gone" is final — that pid and
|
|
70
|
+
* start time never come back; any other verdict is reused for
|
|
71
|
+
* `identifyEveryMs`. `fresh` always checks (a stop must never signal a
|
|
72
|
+
* pid that is someone else's now). */
|
|
73
|
+
#identityOf(id, runner, fresh = false) {
|
|
74
|
+
const key = `${runner.pid}:${runner.startedAt}`;
|
|
75
|
+
const cached = this.#identity.get(id);
|
|
76
|
+
if (cached !== undefined && cached.key === key && (cached.verdict === "gone" || (!fresh && Date.now() - cached.at < this.#identifyEveryMs)))
|
|
77
|
+
return cached.verdict;
|
|
78
|
+
const verdict = this.#backend.identify(runner.pid, runner.startedAt);
|
|
79
|
+
this.#identity.set(id, { key, verdict, at: Date.now() });
|
|
80
|
+
return verdict;
|
|
81
|
+
}
|
|
82
|
+
/** Hear every transition (delivery, the terminal, telemetry — any number
|
|
83
|
+
* of them). Returns the unsubscribe. */
|
|
84
|
+
subscribe(listener) {
|
|
85
|
+
this.#listeners.add(listener);
|
|
86
|
+
return () => void this.#listeners.delete(listener);
|
|
87
|
+
}
|
|
88
|
+
/** A new task directory, durable before anything is recorded in it: the
|
|
89
|
+
* session's task root (and its entry in the store directory), then the
|
|
90
|
+
* task's own directory. */
|
|
91
|
+
#newTaskDir() {
|
|
92
|
+
if (!existsSync(this.root)) {
|
|
93
|
+
mkdirSync(this.root, { recursive: true });
|
|
94
|
+
fsyncDir(dirname(this.root));
|
|
95
|
+
}
|
|
96
|
+
const id = this.#nextId();
|
|
97
|
+
const dir = join(this.root, id);
|
|
98
|
+
mkdirSync(dir);
|
|
99
|
+
fsyncDir(this.root);
|
|
100
|
+
return { id, dir };
|
|
101
|
+
}
|
|
102
|
+
/** Start a task. `planned` is durable before the runner is spawned. */
|
|
103
|
+
async start(options) {
|
|
104
|
+
const { id, dir } = this.#newTaskDir();
|
|
105
|
+
const exec = options.exec?.(dir);
|
|
106
|
+
appendRecord(join(dir, JOURNAL), {
|
|
107
|
+
type: "planned",
|
|
108
|
+
ts: Date.now(),
|
|
109
|
+
taskId: id,
|
|
110
|
+
backend: "process",
|
|
111
|
+
command: options.command,
|
|
112
|
+
cwd: options.cwd,
|
|
113
|
+
profile: options.profile ?? "oneshot",
|
|
114
|
+
...(options.executionId !== undefined ? { executionId: options.executionId } : {}),
|
|
115
|
+
...(options.readyWhen !== undefined ? { readyWhen: options.readyWhen } : {}),
|
|
116
|
+
...(exec !== undefined ? { launch: { kind: "exec", file: exec.file, args: [...exec.args] } } : {}),
|
|
117
|
+
...(options.agent !== undefined ? { agent: options.agent } : {}),
|
|
118
|
+
});
|
|
119
|
+
// the journal's own directory entry: without it, a power loss could
|
|
120
|
+
// erase a `planned` whose runner was spawned — "no planned ⇒ never
|
|
121
|
+
// spawned" holds only once this is durable
|
|
122
|
+
fsyncDir(dir);
|
|
123
|
+
await this.#backend.spawn({ dir, env: options.env ?? process.env });
|
|
124
|
+
this.#seen.set(id, "starting");
|
|
125
|
+
this.observe();
|
|
126
|
+
return this.get(id);
|
|
127
|
+
}
|
|
128
|
+
/** A running foreground command becomes a task this process owns:
|
|
129
|
+
* `planned`, the owner's identity and `command_started` are durable
|
|
130
|
+
* before this returns. The command is already running; nothing is
|
|
131
|
+
* spawned. */
|
|
132
|
+
adopt(options) {
|
|
133
|
+
const { id, dir } = this.#newTaskDir();
|
|
134
|
+
const journal = join(dir, JOURNAL);
|
|
135
|
+
appendRecord(journal, {
|
|
136
|
+
type: "planned",
|
|
137
|
+
ts: Date.now(),
|
|
138
|
+
taskId: id,
|
|
139
|
+
backend: "foreground",
|
|
140
|
+
command: options.command,
|
|
141
|
+
cwd: options.cwd,
|
|
142
|
+
profile: options.readyWhen !== undefined ? "service" : "oneshot",
|
|
143
|
+
...(options.executionId !== undefined ? { executionId: options.executionId } : {}),
|
|
144
|
+
...(options.readyWhen !== undefined ? { readyWhen: options.readyWhen } : {}),
|
|
145
|
+
});
|
|
146
|
+
appendRecord(journal, { type: "runner_started", ts: Date.now(), pid: options.runner.pid, startedAt: options.runner.startedAt });
|
|
147
|
+
appendRecord(journal, { type: "command_started", ts: Date.now() });
|
|
148
|
+
fsyncDir(dir);
|
|
149
|
+
if (options.ready === true && options.readyWhen !== undefined) {
|
|
150
|
+
appendRecord(journal, { type: "ready", ts: Date.now(), match: options.readyWhen });
|
|
151
|
+
// the promotion's own result says "ready —": that call reports it
|
|
152
|
+
if (options.executionId !== undefined)
|
|
153
|
+
appendRecord(journal, { type: "result_claimed", ts: Date.now(), transition: "ready", executionId: options.executionId });
|
|
154
|
+
}
|
|
155
|
+
this.#local.set(id, options.stop);
|
|
156
|
+
this.#seen.set(id, options.ready === true ? "ready" : "running");
|
|
157
|
+
this.observe();
|
|
158
|
+
let readySent = options.ready === true;
|
|
159
|
+
return {
|
|
160
|
+
id,
|
|
161
|
+
outputPath: join(dir, OUTPUT),
|
|
162
|
+
ready: (match) => {
|
|
163
|
+
if (readySent)
|
|
164
|
+
return;
|
|
165
|
+
readySent = true;
|
|
166
|
+
appendRecord(journal, { type: "ready", ts: Date.now(), match });
|
|
167
|
+
},
|
|
168
|
+
ended: (exitCode, signal) => {
|
|
169
|
+
if (!this.#local.delete(id))
|
|
170
|
+
return;
|
|
171
|
+
appendRecord(journal, { type: "terminal", ts: Date.now(), exitCode, signal });
|
|
172
|
+
},
|
|
173
|
+
unconfirmed: (pids) => {
|
|
174
|
+
if (!this.#local.delete(id))
|
|
175
|
+
return;
|
|
176
|
+
appendRecord(journal, { type: "stop_unconfirmed", ts: Date.now(), pids });
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
/** Every task of the session, oldest first. */
|
|
181
|
+
list() {
|
|
182
|
+
return this.#ids().map((id) => this.#info(id));
|
|
183
|
+
}
|
|
184
|
+
get(id) {
|
|
185
|
+
return ID.test(id) && existsSync(join(this.root, id, JOURNAL)) ? this.#info(id) : undefined;
|
|
186
|
+
}
|
|
187
|
+
/** Stop a live task: `stop_requested` is durable first — the runner
|
|
188
|
+
* watches its journal for it — then a VERIFIED runner is also signalled.
|
|
189
|
+
* An unverifiable one is never signalled (its pid may be a stranger's
|
|
190
|
+
* now); the record alone reaches it if it is ours. False when there is
|
|
191
|
+
* nothing that could be stopped. */
|
|
192
|
+
stop(id, by) {
|
|
193
|
+
if (!ID.test(id))
|
|
194
|
+
return false;
|
|
195
|
+
const records = readRecords(join(this.root, id, JOURNAL));
|
|
196
|
+
if (records.some((r) => r.type === "terminal"))
|
|
197
|
+
return false;
|
|
198
|
+
const planned = records.find((r) => r.type === "planned");
|
|
199
|
+
if (planned?.backend === "foreground") {
|
|
200
|
+
// its runner is a kiso process: never signalled — the owner stops
|
|
201
|
+
// the command, and only the owner can
|
|
202
|
+
const stopper = this.#local.get(id);
|
|
203
|
+
if (stopper === undefined)
|
|
204
|
+
return false;
|
|
205
|
+
appendRecord(join(this.root, id, JOURNAL), { type: "stop_requested", ts: Date.now(), by });
|
|
206
|
+
stopper();
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
const runner = runnerOf(records);
|
|
210
|
+
if (runner === undefined)
|
|
211
|
+
return false;
|
|
212
|
+
const identity = this.#identityOf(id, runner, true);
|
|
213
|
+
if (identity === "gone")
|
|
214
|
+
return false;
|
|
215
|
+
appendRecord(join(this.root, id, JOURNAL), { type: "stop_requested", ts: Date.now(), by });
|
|
216
|
+
if (identity === "verified")
|
|
217
|
+
this.#backend.signalStop(runner.pid);
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
/** ADR-0058 Amendment 7: wait up to `ms` for task `id` to reach `until`
|
|
221
|
+
* — "end" (ended, unknown, never run) or "ready" (ready, or any end) —
|
|
222
|
+
* on behalf of a tool call whose result will report it. While it waits
|
|
223
|
+
* the watcher never announces this task. What it observes is CLAIMED
|
|
224
|
+
* for `executionId` — the caller's own execution: `result_claimed` is
|
|
225
|
+
* durable first, then the transition counts as seen, so it is never
|
|
226
|
+
* announced. A timeout, an abort, no execution id, an agent task (its
|
|
227
|
+
* end belongs to its group, 3d) or a claim that cannot be written
|
|
228
|
+
* claim nothing: the transition is announced as before. */
|
|
229
|
+
async awaitSettled(id, until, ms, opts = {}) {
|
|
230
|
+
if (this.get(id) === undefined)
|
|
231
|
+
throw new Error(`no task ${id} in this session`);
|
|
232
|
+
this.#waiting.set(id, (this.#waiting.get(id) ?? 0) + 1);
|
|
233
|
+
let aborted = opts.signal?.aborted === true;
|
|
234
|
+
let wake = () => { };
|
|
235
|
+
const onAbort = () => {
|
|
236
|
+
aborted = true;
|
|
237
|
+
wake();
|
|
238
|
+
};
|
|
239
|
+
opts.signal?.addEventListener("abort", onAbort);
|
|
240
|
+
try {
|
|
241
|
+
const deadline = Date.now() + ms;
|
|
242
|
+
for (;;) {
|
|
243
|
+
const info = this.#info(id);
|
|
244
|
+
const reached = endTransitionOf(info) ?? (until === "ready" && info.state.kind === "running" && info.state.ready ? "ready" : null);
|
|
245
|
+
if (reached !== null)
|
|
246
|
+
return { info, settled: true, claimed: this.#claim(info, reached, opts.executionId) };
|
|
247
|
+
const left = deadline - Date.now();
|
|
248
|
+
if (aborted || left <= 0)
|
|
249
|
+
return { info, settled: false, claimed: false };
|
|
250
|
+
await new Promise((resolve) => {
|
|
251
|
+
const timer = setTimeout(resolve, Math.min(100, left));
|
|
252
|
+
wake = () => {
|
|
253
|
+
clearTimeout(timer);
|
|
254
|
+
resolve();
|
|
255
|
+
};
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
opts.signal?.removeEventListener("abort", onAbort);
|
|
261
|
+
const n = (this.#waiting.get(id) ?? 1) - 1;
|
|
262
|
+
if (n > 0)
|
|
263
|
+
this.#waiting.set(id, n);
|
|
264
|
+
else
|
|
265
|
+
this.#waiting.delete(id);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
/** The frozen order: the claim durable, THEN seen — an append that fails
|
|
269
|
+
* leaves the transition unseen, to be announced (a duplicate is the
|
|
270
|
+
* lesser evil next to a lost transition). */
|
|
271
|
+
#claim(info, transition, executionId) {
|
|
272
|
+
if (executionId === undefined || info.agent !== undefined)
|
|
273
|
+
return false;
|
|
274
|
+
try {
|
|
275
|
+
appendRecord(join(this.root, info.id, JOURNAL), { type: "result_claimed", ts: Date.now(), transition, executionId });
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
280
|
+
this.#seen.set(info.id, this.#kindOf(info));
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
/** 3e: a running foreground command that can become a task now —
|
|
284
|
+
* registered by its owner while it runs; returns the unregister. Every
|
|
285
|
+
* detach goes through here (ADR-0058 §6): the person's key, a steer. */
|
|
286
|
+
registerDetachable(executionId, detachable) {
|
|
287
|
+
this.#detachable.set(executionId, detachable);
|
|
288
|
+
return () => {
|
|
289
|
+
if (this.#detachable.get(executionId) === detachable)
|
|
290
|
+
this.#detachable.delete(executionId);
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
/** Detach one registered execution. It is unregistered BEFORE it is
|
|
294
|
+
* asked, so a second detach — two steers, a key racing a pending
|
|
295
|
+
* auto-detach — is a no-op: one promotion, one task. */
|
|
296
|
+
detach(executionId, by) {
|
|
297
|
+
const d = this.#detachable.get(executionId);
|
|
298
|
+
if (d === undefined)
|
|
299
|
+
return false;
|
|
300
|
+
this.#detachable.delete(executionId);
|
|
301
|
+
d.detach(by);
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
/** Detach every registered execution; the ids that were detached. */
|
|
305
|
+
detachAll(by) {
|
|
306
|
+
return [...this.#detachable.keys()].filter((id) => this.detach(id, by));
|
|
307
|
+
}
|
|
308
|
+
/** What can be detached now, with when each started. */
|
|
309
|
+
detachable() {
|
|
310
|
+
return [...this.#detachable].map(([executionId, d]) => ({ executionId, startedAt: d.startedAt }));
|
|
311
|
+
}
|
|
312
|
+
/** A clean exit: stop every live task — or, `which: "moved"`, only the
|
|
313
|
+
* ones this process owns (promoted foreground commands; a runner's task
|
|
314
|
+
* is left running) — and wait for their terminals, up to `graceMs`.
|
|
315
|
+
* Returns the ids still without a terminal: a stop REQUESTED, never
|
|
316
|
+
* confirmed (3e). */
|
|
317
|
+
async stopAll(by = "exit", graceMs = 8_000, which = "all") {
|
|
318
|
+
// every task not ended is asked; a corrupt journal is skipped here
|
|
319
|
+
// (it fails loudly where it is read on purpose) — an exit still exits
|
|
320
|
+
const kindOf = (id) => {
|
|
321
|
+
try {
|
|
322
|
+
return this.#info(id).state.kind;
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return "corrupt";
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
const moved = (id) => {
|
|
329
|
+
try {
|
|
330
|
+
return this.#info(id).backend === "foreground";
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
return false;
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
const live = this.#ids().filter((id) => ["running", "starting", "unknown"].includes(kindOf(id)) && (which === "all" || moved(id)));
|
|
337
|
+
const asked = live.filter((id) => this.stop(id, by));
|
|
338
|
+
const deadline = Date.now() + graceMs;
|
|
339
|
+
let left = asked;
|
|
340
|
+
while (left.length > 0 && Date.now() < deadline) {
|
|
341
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
342
|
+
left = left.filter((id) => kindOf(id) !== "ended");
|
|
343
|
+
}
|
|
344
|
+
return left;
|
|
345
|
+
}
|
|
346
|
+
/** Start watching: every task is classified now (no transition for what
|
|
347
|
+
* it already is), and live ones are re-read until they settle. */
|
|
348
|
+
observe() {
|
|
349
|
+
for (const id of this.#ids()) {
|
|
350
|
+
if (this.#seen.has(id))
|
|
351
|
+
continue;
|
|
352
|
+
try {
|
|
353
|
+
this.#seen.set(id, this.#kindOf(this.#info(id)));
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
// a corrupt journal: left unclassified, and read loudly on purpose
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (this.#timer !== null)
|
|
360
|
+
return;
|
|
361
|
+
this.#timer = setInterval(() => this.#poll(), this.#pollMs);
|
|
362
|
+
this.#timer.unref?.();
|
|
363
|
+
}
|
|
364
|
+
/** Stop watching. Tasks keep running. */
|
|
365
|
+
close() {
|
|
366
|
+
if (this.#timer !== null)
|
|
367
|
+
clearInterval(this.#timer);
|
|
368
|
+
this.#timer = null;
|
|
369
|
+
}
|
|
370
|
+
#poll() {
|
|
371
|
+
for (const id of this.#ids()) {
|
|
372
|
+
// a tool call is waiting on it: what it sees, its result reports
|
|
373
|
+
if (this.#waiting.has(id))
|
|
374
|
+
continue;
|
|
375
|
+
const before = this.#seen.get(id);
|
|
376
|
+
if (before === "ended" || before === "unknown" || before === "not_run")
|
|
377
|
+
continue;
|
|
378
|
+
let info;
|
|
379
|
+
try {
|
|
380
|
+
info = this.#info(id);
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
continue; // a corrupt journal: reported where it is read, never here in the background
|
|
384
|
+
}
|
|
385
|
+
const now = this.#kindOf(info);
|
|
386
|
+
if (now === before)
|
|
387
|
+
continue;
|
|
388
|
+
this.#seen.set(id, now);
|
|
389
|
+
const transition = now === "ready" ? "ready" : now === "ended" ? "ended" : now === "unknown" ? "unknown" : now === "not_run" ? "not_run" : null;
|
|
390
|
+
if (transition !== null)
|
|
391
|
+
for (const listener of this.#listeners)
|
|
392
|
+
listener(info, transition);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
#kindOf(info) {
|
|
396
|
+
return info.state.kind === "running" && info.state.ready ? "ready" : info.state.kind;
|
|
397
|
+
}
|
|
398
|
+
#info(id) {
|
|
399
|
+
const records = readRecords(join(this.root, id, JOURNAL));
|
|
400
|
+
const planned = records.find((r) => r.type === "planned");
|
|
401
|
+
const runner = runnerOf(records);
|
|
402
|
+
const terminal = records.find((r) => r.type === "terminal");
|
|
403
|
+
// an adopted task this very process still owns needs no identity
|
|
404
|
+
// check: the owner is here, holding the child; and an ended task's
|
|
405
|
+
// verdict is its terminal — identity cannot change it (Windows P6)
|
|
406
|
+
const verified = this.#local.has(id) || (terminal === undefined && runner !== undefined && this.#identityOf(id, runner) === "verified");
|
|
407
|
+
const stop = records.find((r) => r.type === "stop_requested");
|
|
408
|
+
const claims = records
|
|
409
|
+
.filter((r) => r.type === "result_claimed")
|
|
410
|
+
.map((r) => ({ transition: r.transition, executionId: r.executionId }));
|
|
411
|
+
return {
|
|
412
|
+
id,
|
|
413
|
+
command: planned?.command ?? "",
|
|
414
|
+
profile: planned?.profile ?? "oneshot",
|
|
415
|
+
backend: planned?.backend ?? "process",
|
|
416
|
+
...(planned?.executionId !== undefined ? { executionId: planned.executionId } : {}),
|
|
417
|
+
...(stop !== undefined ? { stoppedBy: stop.by } : {}),
|
|
418
|
+
...(planned?.agent !== undefined ? { agent: planned.agent } : {}),
|
|
419
|
+
state: verdictOf(records, verified),
|
|
420
|
+
outputPath: join(this.root, id, OUTPUT),
|
|
421
|
+
startedAt: planned?.ts ?? 0,
|
|
422
|
+
...(terminal !== undefined ? { endedAt: terminal.ts } : {}),
|
|
423
|
+
...(claims.length > 0 ? { claims } : {}),
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
#ids() {
|
|
427
|
+
if (!existsSync(this.root))
|
|
428
|
+
return [];
|
|
429
|
+
return readdirSync(this.root)
|
|
430
|
+
.filter((name) => ID.test(name))
|
|
431
|
+
.sort((a, b) => Number(a.slice(1)) - Number(b.slice(1)));
|
|
432
|
+
}
|
|
433
|
+
#nextId() {
|
|
434
|
+
const ids = this.#ids();
|
|
435
|
+
const last = ids.length === 0 ? 0 : Number(ids.at(-1).slice(1));
|
|
436
|
+
return `t${last + 1}`;
|
|
437
|
+
}
|
|
438
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.0",
|
|
4
4
|
"description": "kiso runtime \u2014 durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"test": "vitest run"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@vincemakes/kiso-core": "0.
|
|
28
|
+
"@vincemakes/kiso-core": "0.46.0"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.
|
|
33
|
-
"@vincemakes/kiso-provider-openai-responses": "0.
|
|
31
|
+
"@vincemakes/kiso-provider-anthropic": "0.46.0",
|
|
32
|
+
"@vincemakes/kiso-provider-openai": "0.46.0",
|
|
33
|
+
"@vincemakes/kiso-provider-openai-responses": "0.46.0"
|
|
34
34
|
},
|
|
35
35
|
"peerDependenciesMeta": {
|
|
36
36
|
"@vincemakes/kiso-provider-anthropic": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
}
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@vincemakes/kiso-evals": "0.
|
|
47
|
+
"@vincemakes/kiso-evals": "0.46.0",
|
|
48
48
|
"@types/node": "^26.1.2",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vitest": "^3.0.0"
|