@vincemakes/kiso-runtime 0.1.29 → 0.1.31
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/agent.d.ts +4 -4
- package/dist/agent.js +4 -4
- package/dist/compose.d.ts +3 -3
- package/dist/compose.js +5 -5
- package/dist/extensions.d.ts +1 -1
- package/dist/extensions.js +1 -1
- package/dist/recovery.d.ts +2 -2
- package/dist/recovery.js +2 -2
- package/dist/run.d.ts +1 -1
- package/dist/run.js +19 -19
- package/dist/session.d.ts +12 -12
- package/dist/session.js +27 -27
- package/dist/store.d.ts +9 -9
- package/dist/store.js +26 -26
- package/dist/summarize.d.ts +1 -1
- package/dist/summarize.js +1 -1
- package/dist/trust.d.ts +1 -1
- package/dist/trust.js +3 -3
- package/package.json +5 -5
package/dist/agent.d.ts
CHANGED
|
@@ -51,14 +51,14 @@ export interface AgentDefinition {
|
|
|
51
51
|
readonly compaction?: {
|
|
52
52
|
readonly thresholdTokens: number;
|
|
53
53
|
};
|
|
54
|
-
/** C
|
|
54
|
+
/** C area: microcompact threshold — passed through to every session. */
|
|
55
55
|
readonly microcompact?: {
|
|
56
56
|
readonly thresholdTokens: number;
|
|
57
57
|
};
|
|
58
58
|
readonly maxRetries?: number;
|
|
59
59
|
/** E1: loaded extensions — their tools merge into the registry (a name
|
|
60
60
|
* collision with a built-in is a loud startup error), their hooks
|
|
61
|
-
* compose after the agent's own (
|
|
61
|
+
* compose after the agent's own (the existing come first), their approvals join the
|
|
62
62
|
* loop's policy chain. */
|
|
63
63
|
readonly extensions?: readonly KisoExtension[];
|
|
64
64
|
}
|
|
@@ -68,7 +68,7 @@ export declare class AgentRuntime {
|
|
|
68
68
|
sessionIds(): string[];
|
|
69
69
|
/** Session metadata for listings (`kiso sessions`). */
|
|
70
70
|
sessions(): import("./store.js").SessionMeta[];
|
|
71
|
-
/** Release every held fd and writer lock (E
|
|
71
|
+
/** Release every held fd and writer lock (E group: the CLI closes on exit). */
|
|
72
72
|
close(): void;
|
|
73
73
|
/** Load an existing session from disk, or create a fresh one. */
|
|
74
74
|
session(options: {
|
|
@@ -78,7 +78,7 @@ export declare class AgentRuntime {
|
|
|
78
78
|
/** The one-liner the README promises. */
|
|
79
79
|
export declare function createAgent(definition: AgentDefinition): AgentRuntime;
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
81
|
+
* merge round B: the adapter factory the CLI uses for /model switches — the
|
|
82
82
|
* same lazy provider resolution as createAgent's (the CLI never imports
|
|
83
83
|
* provider SDKs directly; the runtime owns them here). Returns a NEW
|
|
84
84
|
* adapter each call; the caller (session.setAdapter) decides when it
|
package/dist/agent.js
CHANGED
|
@@ -27,7 +27,7 @@ export class AgentRuntime {
|
|
|
27
27
|
for (const ext of definition.extensions ?? []) {
|
|
28
28
|
for (const tool of ext.tools ?? [])
|
|
29
29
|
this.#registry.register(tool);
|
|
30
|
-
// 0.1.26 (MCP
|
|
30
|
+
// 0.1.26 (MCP lazy connection): an extension's tools array is LIVE — the
|
|
31
31
|
// registry consults it on every lookup, so tools registered by a
|
|
32
32
|
// background connect (the MCP bridge's servers) are callable the
|
|
33
33
|
// moment they land, without a session rebuild.
|
|
@@ -42,7 +42,7 @@ export class AgentRuntime {
|
|
|
42
42
|
sessions() {
|
|
43
43
|
return this.#definition.store.list();
|
|
44
44
|
}
|
|
45
|
-
/** Release every held fd and writer lock (E
|
|
45
|
+
/** Release every held fd and writer lock (E group: the CLI closes on exit). */
|
|
46
46
|
close() {
|
|
47
47
|
this.#definition.store.closeAll();
|
|
48
48
|
}
|
|
@@ -104,7 +104,7 @@ function policyHooks(policy) {
|
|
|
104
104
|
};
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
*
|
|
107
|
+
* merge round B: the adapter factory the CLI uses for /model switches — the
|
|
108
108
|
* same lazy provider resolution as createAgent's (the CLI never imports
|
|
109
109
|
* provider SDKs directly; the runtime owns them here). Returns a NEW
|
|
110
110
|
* adapter each call; the caller (session.setAdapter) decides when it
|
|
@@ -119,7 +119,7 @@ async function resolveAdapter(definition) {
|
|
|
119
119
|
if (definition.adapter)
|
|
120
120
|
return definition.adapter;
|
|
121
121
|
switch (definition.provider) {
|
|
122
|
-
//
|
|
122
|
+
// round 7: the runtime imports ONLY the provider package — its high-level
|
|
123
123
|
// factory owns the SDK and builds the adapter from config. The SDKs
|
|
124
124
|
// are private dependencies of the provider packages, so a nested
|
|
125
125
|
// consumer install resolves them next to the provider, never through
|
package/dist/compose.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* the ergonomics batch B4 (pure move) — the E1/E2 composition helpers, moved verbatim
|
|
3
3
|
* from session.ts: the extension system-prompt appends, the extension
|
|
4
|
-
* hook composition (
|
|
4
|
+
* hook composition (the existing come first), and the loop's microcompact config lookup.
|
|
5
5
|
*/
|
|
6
6
|
import type { HookHost, KisoExtension } from "@vincemakes/kiso-core";
|
|
7
7
|
import type { SessionConfig } from "./session.js";
|
|
@@ -12,7 +12,7 @@ import type { SessionConfig } from "./session.js";
|
|
|
12
12
|
*/
|
|
13
13
|
export declare function composeSystemPrompt(base: string | undefined, extensions: readonly KisoExtension[]): string | undefined;
|
|
14
14
|
/**
|
|
15
|
-
* E1: extension hooks compose AFTER the agent's own (
|
|
15
|
+
* E1: extension hooks compose AFTER the agent's own (the existing come first — the existing
|
|
16
16
|
* hook sees every event first). Observers all run, in order; onUserMessage
|
|
17
17
|
* and onPreTool — the FIRST decisive answer wins (the existing hook
|
|
18
18
|
* outranks extensions; defers fall through); onPostTool folds — each
|
package/dist/compose.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* the ergonomics batch B4 (pure move) — the E1/E2 composition helpers, moved verbatim
|
|
3
3
|
* from session.ts: the extension system-prompt appends, the extension
|
|
4
|
-
* hook composition (
|
|
4
|
+
* hook composition (the existing come first), and the loop's microcompact config lookup.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* E2: the session's systemPrompt plus every extension's append, in LOAD
|
|
@@ -15,7 +15,7 @@ export function composeSystemPrompt(base, extensions) {
|
|
|
15
15
|
return base === undefined ? appends.join("\n\n") : `${base}\n\n${appends.join("\n\n")}`;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* E1: extension hooks compose AFTER the agent's own (
|
|
18
|
+
* E1: extension hooks compose AFTER the agent's own (the existing come first — the existing
|
|
19
19
|
* hook sees every event first). Observers all run, in order; onUserMessage
|
|
20
20
|
* and onPreTool — the FIRST decisive answer wins (the existing hook
|
|
21
21
|
* outranks extensions; defers fall through); onPostTool folds — each
|
|
@@ -49,8 +49,8 @@ export function composeHooks(existing, extensions) {
|
|
|
49
49
|
out.onUserMessage = messageHandlers[0]; // length 1 guarantees the element
|
|
50
50
|
}
|
|
51
51
|
else if (messageHandlers.length > 1) {
|
|
52
|
-
//
|
|
53
|
-
// message as the PREVIOUS one left it (
|
|
52
|
+
// re-review E1-P2: the pipe + veto short-circuit — each handler sees the
|
|
53
|
+
// message as the PREVIOUS one left it (the existing come first), and a null (veto)
|
|
54
54
|
// anywhere ends the chain immediately: never "no opinion" for the
|
|
55
55
|
// next handler to outvote. Adding an extension can therefore never
|
|
56
56
|
// make the chain MORE permissive (the approval chain's deny>ask>allow
|
package/dist/extensions.d.ts
CHANGED
|
@@ -21,7 +21,7 @@ export type { KisoExtension };
|
|
|
21
21
|
export declare function loadProjectExtensions(dir: string, existing?: readonly KisoExtension[]): Promise<KisoExtension[]>;
|
|
22
22
|
export declare function loadExtensions(dir: string): Promise<KisoExtension[]>;
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
24
|
+
* finding #8 (P1): dispose every extension's external resources — each call
|
|
25
25
|
* guarded (one failure never blocks the rest), each capped at 5s (a
|
|
26
26
|
* timeout is abandoned and recorded — Promise.allSettled semantics).
|
|
27
27
|
* Whoever LOADS extensions is responsible for disposing them.
|
package/dist/extensions.js
CHANGED
|
@@ -61,7 +61,7 @@ export async function loadExtensions(dir) {
|
|
|
61
61
|
return out;
|
|
62
62
|
}
|
|
63
63
|
/**
|
|
64
|
-
*
|
|
64
|
+
* finding #8 (P1): dispose every extension's external resources — each call
|
|
65
65
|
* guarded (one failure never blocks the rest), each capped at 5s (a
|
|
66
66
|
* timeout is abandoned and recorded — Promise.allSettled semantics).
|
|
67
67
|
* Whoever LOADS extensions is responsible for disposing them.
|
package/dist/recovery.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* the ergonomics batch B4 (pure move) — the recovery support pieces, moved verbatim from
|
|
3
3
|
* session.ts: the open-run gate, the abort sentinel/race wrapper, and the
|
|
4
4
|
* merged abort signal.
|
|
5
5
|
*/
|
|
@@ -8,7 +8,7 @@ import type { StoreRecord } from "./store.js";
|
|
|
8
8
|
/**
|
|
9
9
|
* The most recent run WITHOUT a terminal, or undefined when every recorded
|
|
10
10
|
* run terminated. Recovery can only drive ONE run to its terminal, so an
|
|
11
|
-
* open run must be the exclusive reason a session refuses new runs (
|
|
11
|
+
* open run must be the exclusive reason a session refuses new runs (round 4).
|
|
12
12
|
*/
|
|
13
13
|
export declare function openRunId(records: readonly StoreRecord[]): string | undefined;
|
|
14
14
|
/** Sentinel: the signal aborted while the recovery awaited a decision. */
|
package/dist/recovery.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* the ergonomics batch B4 (pure move) — the recovery support pieces, moved verbatim from
|
|
3
3
|
* session.ts: the open-run gate, the abort sentinel/race wrapper, and the
|
|
4
4
|
* merged abort signal.
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* The most recent run WITHOUT a terminal, or undefined when every recorded
|
|
8
8
|
* run terminated. Recovery can only drive ONE run to its terminal, so an
|
|
9
|
-
* open run must be the exclusive reason a session refuses new runs (
|
|
9
|
+
* open run must be the exclusive reason a session refuses new runs (round 4).
|
|
10
10
|
*/
|
|
11
11
|
export function openRunId(records) {
|
|
12
12
|
const terminated = new Set(records.filter((r) => r.event.type === "terminal").map((r) => r.runId));
|
package/dist/run.d.ts
CHANGED
package/dist/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* the ergonomics batch B4 (pure move) — the Run class (a single turn: write-ahead
|
|
3
3
|
* persistence, the loop drive, the durable recovery state machine), moved
|
|
4
4
|
* verbatim from session.ts.
|
|
5
5
|
*/
|
|
@@ -46,7 +46,7 @@ export class Run {
|
|
|
46
46
|
// run at ANY yield (even the user_input one) must release the
|
|
47
47
|
// session's single-run slot and its approval resolvers.
|
|
48
48
|
try {
|
|
49
|
-
//
|
|
49
|
+
// round 4: health is re-checked when the iterator ACTUALLY starts —
|
|
50
50
|
// a run constructed before the session was poisoned must fail
|
|
51
51
|
// here, before any log or disk mutation.
|
|
52
52
|
this.#session.ensureHealthy();
|
|
@@ -80,7 +80,7 @@ export class Run {
|
|
|
80
80
|
this.#decisionIds.push(decisionId);
|
|
81
81
|
this.#session.registerResolver(decisionId, resolve);
|
|
82
82
|
}),
|
|
83
|
-
//
|
|
83
|
+
// round 4 (adversarial): the abort paths consult these so a verdict
|
|
84
84
|
// the human gave in the same instant as the abort is
|
|
85
85
|
// recorded, exactly once.
|
|
86
86
|
approvalVerdict: (decisionId) => this.#session.approvalVerdict(decisionId),
|
|
@@ -98,7 +98,7 @@ export class Run {
|
|
|
98
98
|
}
|
|
99
99
|
};
|
|
100
100
|
if (this.#resume) {
|
|
101
|
-
// ── B
|
|
101
|
+
// ── B group: recovery is PER-RUN, keyed by StoreRecord.runId ──
|
|
102
102
|
// Rebuild run boundaries; only the LAST unterminated run is
|
|
103
103
|
// recovered. Earlier runs that DID terminate have their
|
|
104
104
|
// dangling approvals closed (permission_expired) — a dead
|
|
@@ -167,14 +167,14 @@ export class Run {
|
|
|
167
167
|
}
|
|
168
168
|
// 2. Continuation: drive the LAST OPEN run to its terminal.
|
|
169
169
|
// The guard is scoped to that run — an earlier run's
|
|
170
|
-
// terminal must not suppress it (B
|
|
170
|
+
// terminal must not suppress it (B group).
|
|
171
171
|
if (!lastOpen.events.some((e) => e.type === "terminal")) {
|
|
172
172
|
for await (const ev of runLoop())
|
|
173
173
|
yield ev;
|
|
174
174
|
}
|
|
175
175
|
return;
|
|
176
176
|
}
|
|
177
|
-
//
|
|
177
|
+
// round 4: a session with an open run REFUSES new runs at the
|
|
178
178
|
// persistence layer — a second open run would be permanently
|
|
179
179
|
// orphaned (recovery only ever recovers the last one). The
|
|
180
180
|
// open run is continued via resume(), never by starting another.
|
|
@@ -196,7 +196,7 @@ export class Run {
|
|
|
196
196
|
yield ev;
|
|
197
197
|
}
|
|
198
198
|
finally {
|
|
199
|
-
//
|
|
199
|
+
// round 5(P1-5): flush verdicts the consumer submitted before the
|
|
200
200
|
// generator was abandoned — an approve()/resolveUncertain() whose
|
|
201
201
|
// durable event the loop never got to persist must STILL land on
|
|
202
202
|
// disk, exactly once.
|
|
@@ -232,7 +232,7 @@ export class Run {
|
|
|
232
232
|
const requests = scope.filter((e) => e.type === "permission_requested");
|
|
233
233
|
for (const pending of requests) {
|
|
234
234
|
const decided = log.all.find((e) => e.type === "permission_decided" && e.decisionId === pending.decisionId);
|
|
235
|
-
//
|
|
235
|
+
// round 4: paired by events NEWER than the request — a historical
|
|
236
236
|
// same-callId execution from an earlier run must not count as THIS
|
|
237
237
|
// request's execution (the provider callId may repeat across runs).
|
|
238
238
|
const hasExecution = log.all.some((e) => e.type === "tool_execution_started" && e.callId === pending.callId && e.seq > pending.seq);
|
|
@@ -247,7 +247,7 @@ export class Run {
|
|
|
247
247
|
// Area 4: an abort during the resumed approval wait ends the
|
|
248
248
|
// run; the request stays durable and pending.
|
|
249
249
|
if (signal.aborted) {
|
|
250
|
-
//
|
|
250
|
+
// round 5(P1-6): a verdict given in the same instant as the
|
|
251
251
|
// abort is still recorded — the abort must not bypass the
|
|
252
252
|
// durable fallback (aligned with the loop's abort path).
|
|
253
253
|
const verdict = this.#session.approvalVerdict(pending.decisionId);
|
|
@@ -264,7 +264,7 @@ export class Run {
|
|
|
264
264
|
}
|
|
265
265
|
const final = await abortable(pendingDecision, signal);
|
|
266
266
|
if (final === ABORTED) {
|
|
267
|
-
//
|
|
267
|
+
// round 4 (adversarial): a verdict given in the same instant as the
|
|
268
268
|
// abort is recorded (exactly once), never lost.
|
|
269
269
|
const verdict = this.#session.approvalVerdict(pending.decisionId);
|
|
270
270
|
if (verdict !== undefined) {
|
|
@@ -282,7 +282,7 @@ export class Run {
|
|
|
282
282
|
yield log.append({
|
|
283
283
|
type: "permission_decided",
|
|
284
284
|
decisionId: pending.decisionId,
|
|
285
|
-
callId: pending.callId, // binds the decision to the invocation (B
|
|
285
|
+
callId: pending.callId, // binds the decision to the invocation (B group)
|
|
286
286
|
decision: final.action === "allow" ? "approved" : "denied",
|
|
287
287
|
...(final.action === "deny" && final.reason !== undefined ? { reason: final.reason } : {}),
|
|
288
288
|
});
|
|
@@ -311,7 +311,7 @@ export class Run {
|
|
|
311
311
|
// whose model-facing result never landed is completed FROM THE
|
|
312
312
|
// RECEIPT — never re-executed. Snapshot the scope first: this phase
|
|
313
313
|
// appends the repaired results, and iterating a growing array would
|
|
314
|
-
// re-visit them.
|
|
314
|
+
// re-visit them. round 4: pairing is by executionId — a same-callId result
|
|
315
315
|
// from a different execution never suppresses the repair.
|
|
316
316
|
for (const ev of [...scope]) {
|
|
317
317
|
if (ev.type !== "tool_execution_succeeded" && ev.type !== "tool_execution_failed")
|
|
@@ -325,7 +325,7 @@ export class Run {
|
|
|
325
325
|
callId: ev.callId,
|
|
326
326
|
content: ev.result.content,
|
|
327
327
|
isError: false,
|
|
328
|
-
//
|
|
328
|
+
// round 8: the repaired result reproduces the normal path
|
|
329
329
|
// losslessly — the tags ride on the durable receipt.
|
|
330
330
|
...(ev.tags !== undefined ? { tags: ev.tags } : {}),
|
|
331
331
|
executionId: ev.executionId,
|
|
@@ -340,9 +340,9 @@ export class Run {
|
|
|
340
340
|
executionId: ev.executionId,
|
|
341
341
|
});
|
|
342
342
|
}
|
|
343
|
-
// B
|
|
343
|
+
// B group crash window: a resolution was persisted but its tool_result
|
|
344
344
|
// fill never landed — complete it so the model is never left staring
|
|
345
|
-
// at a dangling tool_use.
|
|
345
|
+
// at a dangling tool_use. round 4: keyed by executionId, and the fill
|
|
346
346
|
// carries it, so a same-callId result from another execution is never
|
|
347
347
|
// confused with this one.
|
|
348
348
|
for (const ev of [...scope]) {
|
|
@@ -403,7 +403,7 @@ export class Run {
|
|
|
403
403
|
result = await this.#config.hooks.onPostTool({ callId, name, input }, result, { sessionId: this.#session.id });
|
|
404
404
|
}
|
|
405
405
|
}
|
|
406
|
-
//
|
|
406
|
+
// ruling #12 correction one: the honest note rides the recovered failure too —
|
|
407
407
|
// the receipt and the repaired tool_result reproduce the live path
|
|
408
408
|
// losslessly.
|
|
409
409
|
if (result.isError && tool?.idempotent !== true) {
|
|
@@ -440,12 +440,12 @@ export class Run {
|
|
|
440
440
|
isError: result.isError,
|
|
441
441
|
// P1-9: errorKind only exists on errors — runtime-guarded too.
|
|
442
442
|
...(result.isError && result.errorKind !== undefined ? { errorKind: result.errorKind } : {}),
|
|
443
|
-
//
|
|
443
|
+
// round 5: live tags survive the resumed path too.
|
|
444
444
|
...(result.tags !== undefined ? { tags: result.tags } : {}),
|
|
445
445
|
executionId,
|
|
446
446
|
});
|
|
447
|
-
//
|
|
448
|
-
// here too (it mirrored the live loop's C
|
|
447
|
+
// ruling #12 (ADR-0038): the failed-receipt uncertain PAUSE is REMOVED
|
|
448
|
+
// here too (it mirrored the live loop's C group pause) — a complete
|
|
449
449
|
// receipt IS the outcome; uncertainty belongs to the crash window
|
|
450
450
|
// alone. A retry passes the approval chain again.
|
|
451
451
|
}
|
package/dist/session.d.ts
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Restart recovery is the same code path as a second run: rebuild the log
|
|
24
24
|
* from the JSONL, continue numbering where the file ended.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* the ergonomics batch B4 (pure move): the Run class lives in run.ts, the recovery
|
|
27
27
|
* support in recovery.ts, the E1/E2 composition helpers in compose.ts —
|
|
28
28
|
* same package, same exports (index.ts re-exports all four).
|
|
29
29
|
*/
|
|
@@ -32,7 +32,7 @@ import { type SessionStore } from "./store.js";
|
|
|
32
32
|
import { Run } from "./run.js";
|
|
33
33
|
/** A session whose disk write was rejected (stale handle) is PERMANENTLY
|
|
34
34
|
* poisoned: its in-memory log no longer matches the disk, so no further
|
|
35
|
-
* run may proceed — reload the session (
|
|
35
|
+
* run may proceed — reload the session (round 1). */
|
|
36
36
|
export declare class PoisonedSessionError extends Error {
|
|
37
37
|
constructor(reason: string);
|
|
38
38
|
}
|
|
@@ -65,12 +65,12 @@ export declare class AgentSession {
|
|
|
65
65
|
#private;
|
|
66
66
|
readonly id: string;
|
|
67
67
|
readonly log: EventLog;
|
|
68
|
-
/** Permanently invalidate the session after a rejected disk write (
|
|
68
|
+
/** Permanently invalidate the session after a rejected disk write (round 1). */
|
|
69
69
|
poison(reason: string): void;
|
|
70
70
|
ensureHealthy(): void;
|
|
71
71
|
constructor(id: string, log: EventLog, store: SessionStore, adapter: Adapter, config: SessionConfig);
|
|
72
72
|
/** Write-ahead through the store; a rejected write POISONS the session
|
|
73
|
-
* (
|
|
73
|
+
* (round 1/round 4): the in-memory log no longer matches the disk — whatever
|
|
74
74
|
* the cause (stale handle, corruption, a live external writer, an I/O
|
|
75
75
|
* fault) — so no further run, resume, or log mutation may proceed.
|
|
76
76
|
* The health check runs BEFORE every write, on every path. */
|
|
@@ -80,7 +80,7 @@ export declare class AgentSession {
|
|
|
80
80
|
/** The conversation so far, as the model sees it. */
|
|
81
81
|
projected(): readonly Message[];
|
|
82
82
|
/**
|
|
83
|
-
*
|
|
83
|
+
* merge round B (/model): replace the adapter for SUBSEQUENT runs. The
|
|
84
84
|
* kernel reads the adapter through the loop-config closure at each
|
|
85
85
|
* turn, so the swap takes effect at the next turn — a run already in
|
|
86
86
|
* flight keeps the adapter it started with. The CLI calls this between
|
|
@@ -115,7 +115,7 @@ export declare class AgentSession {
|
|
|
115
115
|
}): Promise<SummarizeResult | null>;
|
|
116
116
|
/**
|
|
117
117
|
* Pauses that still await a human decision (durable, survives restart).
|
|
118
|
-
* B
|
|
118
|
+
* B group: a request whose RUN has terminated is DEAD — it is neither
|
|
119
119
|
* re-presented here nor recoverable; expired requests are excluded too.
|
|
120
120
|
*/
|
|
121
121
|
pendingApprovals(): ApprovalRequest[];
|
|
@@ -134,7 +134,7 @@ export declare class AgentSession {
|
|
|
134
134
|
uncertainExecutions(): import("@vincemakes/kiso-core").ExecutionRecord[];
|
|
135
135
|
/**
|
|
136
136
|
* The human's verdict on an interrupted execution, keyed by EXECUTION ID
|
|
137
|
-
* (B
|
|
137
|
+
* (B group): "rerun" (the human says the side effect did NOT happen — the
|
|
138
138
|
* attempt is completed with a recorded failure so the model may re-issue
|
|
139
139
|
* it as a new logical call) or "abandoned" (treated as failed forever).
|
|
140
140
|
* Only uncertain → rerun/abandoned is legal; a resolved or successful
|
|
@@ -148,12 +148,12 @@ export declare class AgentSession {
|
|
|
148
148
|
registerUncertaintyResolver(executionId: string, resolve: (resolution: "rerun" | "abandoned") => void): void;
|
|
149
149
|
dropUncertaintyResolver(executionId: string): void;
|
|
150
150
|
registerResolver(decisionId: string, resolve: (decision: PermissionDecision) => void): void;
|
|
151
|
-
/**
|
|
151
|
+
/** round 4 (adversarial): a verdict the human already gave for a live decision. */
|
|
152
152
|
approvalVerdict(decisionId: string): boolean | undefined;
|
|
153
|
-
/**
|
|
153
|
+
/** round 4 (adversarial): a verdict the human already gave for a live execution. */
|
|
154
154
|
uncertaintyVerdict(executionId: string): "rerun" | "abandoned" | undefined;
|
|
155
155
|
/**
|
|
156
|
-
*
|
|
156
|
+
* round 5(P1-5): flush every verdict submitted to a live resolver that is
|
|
157
157
|
* not yet durable. Called from the Run iterator's FINALLY — whether the
|
|
158
158
|
* run completed, aborted, or was abandoned by the consumer. An event the
|
|
159
159
|
* loop already appended is left alone (its persist precedes its yield);
|
|
@@ -178,7 +178,7 @@ export interface SessionConfig {
|
|
|
178
178
|
readonly compaction?: {
|
|
179
179
|
readonly thresholdTokens: number;
|
|
180
180
|
};
|
|
181
|
-
/** C
|
|
181
|
+
/** C area: microcompact threshold — passed through to the loop verbatim. */
|
|
182
182
|
readonly microcompact?: {
|
|
183
183
|
readonly thresholdTokens: number;
|
|
184
184
|
};
|
|
@@ -186,7 +186,7 @@ export interface SessionConfig {
|
|
|
186
186
|
/**
|
|
187
187
|
* E1: loaded extensions — their tools join the registry (idempotently;
|
|
188
188
|
* a collision with a built-in name was already rejected at agent
|
|
189
|
-
* creation), their hooks compose AFTER the existing ones (
|
|
189
|
+
* creation), their hooks compose AFTER the existing ones (the existing come first),
|
|
190
190
|
* their approval policies enter the loop's policy chain.
|
|
191
191
|
*/
|
|
192
192
|
readonly extensions?: readonly KisoExtension[];
|
package/dist/session.js
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* Restart recovery is the same code path as a second run: rebuild the log
|
|
24
24
|
* from the JSONL, continue numbering where the file ended.
|
|
25
25
|
*
|
|
26
|
-
*
|
|
26
|
+
* the ergonomics batch B4 (pure move): the Run class lives in run.ts, the recovery
|
|
27
27
|
* support in recovery.ts, the E1/E2 composition helpers in compose.ts —
|
|
28
28
|
* same package, same exports (index.ts re-exports all four).
|
|
29
29
|
*/
|
|
@@ -35,7 +35,7 @@ import { composeHooks } from "./compose.js";
|
|
|
35
35
|
import { Run } from "./run.js";
|
|
36
36
|
/** A session whose disk write was rejected (stale handle) is PERMANENTLY
|
|
37
37
|
* poisoned: its in-memory log no longer matches the disk, so no further
|
|
38
|
-
* run may proceed — reload the session (
|
|
38
|
+
* run may proceed — reload the session (round 1). */
|
|
39
39
|
export class PoisonedSessionError extends Error {
|
|
40
40
|
constructor(reason) {
|
|
41
41
|
super(`session is poisoned: ${reason} — reload it; the in-memory log no longer matches the disk`);
|
|
@@ -63,16 +63,16 @@ export class AgentSession {
|
|
|
63
63
|
#pendingResolvers = new Map();
|
|
64
64
|
#uncertaintyResolvers = new Map();
|
|
65
65
|
#answered = new Set();
|
|
66
|
-
/**
|
|
66
|
+
/** round 7: verdicts already passed to a live resolver — the resolution event
|
|
67
67
|
* lands in the log asynchronously (the loop owns it), so the ledger
|
|
68
68
|
* alone cannot make resolveUncertain idempotent across the same tick. */
|
|
69
69
|
#uncertaintyAnswered = new Set();
|
|
70
|
-
/**
|
|
70
|
+
/** round 4 (adversarial): verdicts the human GAVE, recorded when passed to a live
|
|
71
71
|
* resolver. If an abort races the verdict, the loop / recovery queries
|
|
72
72
|
* these and records the decision (exactly once) instead of losing it. */
|
|
73
73
|
#approvalVerdicts = new Map();
|
|
74
74
|
#uncertaintyVerdicts = new Map();
|
|
75
|
-
/**
|
|
75
|
+
/** round 5(P1-5): verdicts submitted to a LIVE resolver but not yet known
|
|
76
76
|
* durable. An async generator only advances on next(), so approve()/
|
|
77
77
|
* resolveUncertain() CANNOT wait for the loop to persist — that would
|
|
78
78
|
* deadlock (the consumer waits while the generator needs a next()).
|
|
@@ -82,7 +82,7 @@ export class AgentSession {
|
|
|
82
82
|
#pendingDurableApprovals = new Map();
|
|
83
83
|
#pendingDurableUncertainties = new Map();
|
|
84
84
|
#poisoned = null;
|
|
85
|
-
/** Permanently invalidate the session after a rejected disk write (
|
|
85
|
+
/** Permanently invalidate the session after a rejected disk write (round 1). */
|
|
86
86
|
poison(reason) {
|
|
87
87
|
if (this.#poisoned === null)
|
|
88
88
|
this.#poisoned = reason;
|
|
@@ -110,7 +110,7 @@ export class AgentSession {
|
|
|
110
110
|
this.#config = composedHooks === undefined ? config : { ...config, hooks: composedHooks };
|
|
111
111
|
}
|
|
112
112
|
/** Write-ahead through the store; a rejected write POISONS the session
|
|
113
|
-
* (
|
|
113
|
+
* (round 1/round 4): the in-memory log no longer matches the disk — whatever
|
|
114
114
|
* the cause (stale handle, corruption, a live external writer, an I/O
|
|
115
115
|
* fault) — so no further run, resume, or log mutation may proceed.
|
|
116
116
|
* The health check runs BEFORE every write, on every path. */
|
|
@@ -120,7 +120,7 @@ export class AgentSession {
|
|
|
120
120
|
await this.#store.append(this.id, runId, event);
|
|
121
121
|
}
|
|
122
122
|
catch (err) {
|
|
123
|
-
//
|
|
123
|
+
// round 4: ANY rejected write poisons — not only the typed
|
|
124
124
|
// stale/corruption errors. A live external writer's lock error
|
|
125
125
|
// is the realistic case; the in-memory log is ahead of the disk
|
|
126
126
|
// in all of them.
|
|
@@ -143,7 +143,7 @@ export class AgentSession {
|
|
|
143
143
|
return projectMessages(this.log.all);
|
|
144
144
|
}
|
|
145
145
|
/**
|
|
146
|
-
*
|
|
146
|
+
* merge round B (/model): replace the adapter for SUBSEQUENT runs. The
|
|
147
147
|
* kernel reads the adapter through the loop-config closure at each
|
|
148
148
|
* turn, so the swap takes effect at the next turn — a run already in
|
|
149
149
|
* flight keeps the adapter it started with. The CLI calls this between
|
|
@@ -206,7 +206,7 @@ export class AgentSession {
|
|
|
206
206
|
// ── Phase D: approvals ───────────────────────────────────────────────
|
|
207
207
|
/**
|
|
208
208
|
* Pauses that still await a human decision (durable, survives restart).
|
|
209
|
-
* B
|
|
209
|
+
* B group: a request whose RUN has terminated is DEAD — it is neither
|
|
210
210
|
* re-presented here nor recoverable; expired requests are excluded too.
|
|
211
211
|
*/
|
|
212
212
|
pendingApprovals() {
|
|
@@ -250,7 +250,7 @@ export class AgentSession {
|
|
|
250
250
|
* lost decision only re-presents the request.
|
|
251
251
|
*/
|
|
252
252
|
async approve(decisionId, allow) {
|
|
253
|
-
//
|
|
253
|
+
// round 4: a poisoned session may not mutate the log — checked before
|
|
254
254
|
// anything is recorded.
|
|
255
255
|
this.ensureHealthy();
|
|
256
256
|
// Idempotent: one decision per request (review finding 7). The
|
|
@@ -263,7 +263,7 @@ export class AgentSession {
|
|
|
263
263
|
this.#answered.add(decisionId);
|
|
264
264
|
if (this.log.all.some((e) => e.type === "permission_decided" && e.decisionId === decisionId))
|
|
265
265
|
return;
|
|
266
|
-
// B
|
|
266
|
+
// B group: a late approve() on a TERMINATED run writes nothing and
|
|
267
267
|
// executes nothing — a dead run's approval cannot resurrect it.
|
|
268
268
|
const records = this.#store.load(this.id);
|
|
269
269
|
const request = records.find((r) => r.event.type === "permission_requested" && r.event.decisionId === decisionId);
|
|
@@ -274,10 +274,10 @@ export class AgentSession {
|
|
|
274
274
|
}
|
|
275
275
|
const resolver = this.#pendingResolvers.get(decisionId);
|
|
276
276
|
if (resolver !== undefined) {
|
|
277
|
-
//
|
|
277
|
+
// round 4 (adversarial): recorded so an abort racing the verdict cannot
|
|
278
278
|
// lose it — the loop's abort path consults approvalVerdict.
|
|
279
279
|
this.#approvalVerdicts.set(decisionId, allow);
|
|
280
|
-
//
|
|
280
|
+
// round 5(P1-5): the verdict is SUBMITTED — the Run's finally
|
|
281
281
|
// flushes it to disk if the generator never gets to persist it.
|
|
282
282
|
// (Waiting here for durability would deadlock: the generator
|
|
283
283
|
// only advances on the consumer's next(), which the consumer
|
|
@@ -304,7 +304,7 @@ export class AgentSession {
|
|
|
304
304
|
}
|
|
305
305
|
/**
|
|
306
306
|
* The human's verdict on an interrupted execution, keyed by EXECUTION ID
|
|
307
|
-
* (B
|
|
307
|
+
* (B group): "rerun" (the human says the side effect did NOT happen — the
|
|
308
308
|
* attempt is completed with a recorded failure so the model may re-issue
|
|
309
309
|
* it as a new logical call) or "abandoned" (treated as failed forever).
|
|
310
310
|
* Only uncertain → rerun/abandoned is legal; a resolved or successful
|
|
@@ -313,37 +313,37 @@ export class AgentSession {
|
|
|
313
313
|
* rejected by real providers (review finding 1).
|
|
314
314
|
*/
|
|
315
315
|
async resolveUncertain(executionId, resolution) {
|
|
316
|
-
//
|
|
316
|
+
// round 4: a poisoned session may not mutate the log.
|
|
317
317
|
this.ensureHealthy();
|
|
318
318
|
const record = executionLedger(this.log.all).get(executionId);
|
|
319
319
|
if (!record)
|
|
320
320
|
throw new Error(`no execution record for ${executionId}`);
|
|
321
321
|
if (record.status !== "uncertain")
|
|
322
322
|
return; // idempotent + irreversible
|
|
323
|
-
//
|
|
323
|
+
// round 7: a verdict already passed to a live resolver is FINAL — the
|
|
324
324
|
// loop's resolution event lands asynchronously, so the ledger alone
|
|
325
325
|
// cannot make this idempotent across the same tick.
|
|
326
326
|
if (this.#uncertaintyAnswered.has(executionId))
|
|
327
327
|
return;
|
|
328
328
|
this.#uncertaintyAnswered.add(executionId);
|
|
329
|
-
//
|
|
329
|
+
// round 7: with a LIVE resolver, the active loop / recovery generator
|
|
330
330
|
// OWNS the resolution event — it appends, yields, and persists it
|
|
331
331
|
// through the Run, so the consumer's stream and the durable log
|
|
332
332
|
// stay identical. We only pass the verdict; a hidden append here
|
|
333
|
-
// would leave a seq gap.
|
|
333
|
+
// would leave a seq gap. round 4 (adversarial): the verdict is recorded so an
|
|
334
334
|
// abort racing it cannot lose it.
|
|
335
335
|
const resolver = this.#uncertaintyResolvers.get(executionId);
|
|
336
336
|
if (resolver !== undefined) {
|
|
337
337
|
this.#uncertaintyVerdicts.set(executionId, resolution);
|
|
338
|
-
//
|
|
338
|
+
// round 5(P1-5): submitted — flushed to disk by the Run's finally
|
|
339
339
|
// if the generator never persists it.
|
|
340
340
|
this.#pendingDurableUncertainties.set(executionId, { resolution, callId: record.callId });
|
|
341
341
|
this.#uncertaintyResolvers.delete(executionId);
|
|
342
342
|
resolver(resolution);
|
|
343
343
|
return;
|
|
344
344
|
}
|
|
345
|
-
//
|
|
346
|
-
//
|
|
345
|
+
// round 7: OFFLINE verdict — no live resolver: persist directly.
|
|
346
|
+
// round 4: the verdict is attributed to the ORIGINAL run of the execution
|
|
347
347
|
// — never the fake runId "resolution".
|
|
348
348
|
const runId = this.runIdFor(executionId);
|
|
349
349
|
const resolved = this.log.append({
|
|
@@ -353,10 +353,10 @@ export class AgentSession {
|
|
|
353
353
|
resolution,
|
|
354
354
|
});
|
|
355
355
|
await this.persist(runId, resolved);
|
|
356
|
-
//
|
|
356
|
+
// round 4: the fill is keyed by THIS execution — a tool_result belonging to
|
|
357
357
|
// a different (same-callId) execution must not suppress the verdict's
|
|
358
358
|
// model-facing result, and the fill itself carries the executionId.
|
|
359
|
-
//
|
|
359
|
+
// round 8 (adversarial): the fill also carries the tags from the durable RECEIPT —
|
|
360
360
|
// the normal live path emits the result with tags before the pause,
|
|
361
361
|
// so a crash-window repair reproduces them.
|
|
362
362
|
if (!this.log.all.some((e) => e.type === "tool_result" && e.executionId === record.executionId)) {
|
|
@@ -399,16 +399,16 @@ export class AgentSession {
|
|
|
399
399
|
registerResolver(decisionId, resolve) {
|
|
400
400
|
this.#pendingResolvers.set(decisionId, resolve);
|
|
401
401
|
}
|
|
402
|
-
/**
|
|
402
|
+
/** round 4 (adversarial): a verdict the human already gave for a live decision. */
|
|
403
403
|
approvalVerdict(decisionId) {
|
|
404
404
|
return this.#approvalVerdicts.get(decisionId);
|
|
405
405
|
}
|
|
406
|
-
/**
|
|
406
|
+
/** round 4 (adversarial): a verdict the human already gave for a live execution. */
|
|
407
407
|
uncertaintyVerdict(executionId) {
|
|
408
408
|
return this.#uncertaintyVerdicts.get(executionId);
|
|
409
409
|
}
|
|
410
410
|
/**
|
|
411
|
-
*
|
|
411
|
+
* round 5(P1-5): flush every verdict submitted to a live resolver that is
|
|
412
412
|
* not yet durable. Called from the Run iterator's FINALLY — whether the
|
|
413
413
|
* run completed, aborted, or was abandoned by the consumer. An event the
|
|
414
414
|
* loop already appended is left alone (its persist precedes its yield);
|
package/dist/store.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SessionStore — append-only JSONL durability, identity-safe (A
|
|
2
|
+
* SessionStore — append-only JSONL durability, identity-safe (A group).
|
|
3
3
|
*
|
|
4
4
|
* One file per session: `<root>/<id>.jsonl`, lines of
|
|
5
5
|
* `{"runId": string, "ts": number, "event": Event}`. The single-writer
|
|
6
|
-
* lock (
|
|
6
|
+
* lock (round 4) is an EXCLUSIVE KERNEL flock on `<id>.lock`, held by a
|
|
7
7
|
* dedicated helper process:
|
|
8
8
|
*
|
|
9
9
|
* - the kernel arbitrates every race — a contender can never remove or
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* the lock simply exists while the helper lives and vanishes with it;
|
|
12
12
|
* - the lock file ALSO carries `{"pid": number, "token": string}` written
|
|
13
13
|
* by the holder, as a best-effort guard for OLD-format writers (whose
|
|
14
|
-
* O_EXCL pidfile scheme does not honor flock).
|
|
14
|
+
* O_EXCL pidfile scheme does not honor flock). round 5(P1-4): this guard
|
|
15
15
|
* is NOT a seamless rolling upgrade — an old writer that created an
|
|
16
16
|
* empty lock file before writing its pid creates a split-brain window
|
|
17
17
|
* that a pidfile read cannot close. The documented upgrade contract is
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* held helper — a foreign close can never release another writer's
|
|
23
23
|
* kernel lock (flock is tied to the helper's open file description).
|
|
24
24
|
*
|
|
25
|
-
* Consistency contract (A
|
|
25
|
+
* Consistency contract (A group):
|
|
26
26
|
* - every id is validated BEFORE any file side effect (append, close,
|
|
27
27
|
* load, lock paths);
|
|
28
28
|
* - append runs an expected-last-seq CAS against the file's REAL last
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* so the in-memory EventLog never continues past a rejected write;
|
|
32
32
|
* - the torn tail is repaired before EVERY append, and committed records
|
|
33
33
|
* (newline-terminated) are never truncated;
|
|
34
|
-
* - load is strict (A
|
|
34
|
+
* - load is strict (A group round 1): a partial final line is the only
|
|
35
35
|
* tolerated damage; everything else throws StoreCorruptionError.
|
|
36
36
|
*/
|
|
37
37
|
import { type Event } from "@vincemakes/kiso-core";
|
|
@@ -64,25 +64,25 @@ export declare class SessionStore {
|
|
|
64
64
|
private pathFor;
|
|
65
65
|
private lockPathFor;
|
|
66
66
|
/**
|
|
67
|
-
* Take the single-writer lock (
|
|
67
|
+
* Take the single-writer lock (round 4): an EXCLUSIVE kernel flock held
|
|
68
68
|
* by a dedicated helper process. The KERNEL arbitrates every race —
|
|
69
69
|
* there is no stale lock to delete and no takeover to race: a
|
|
70
70
|
* contender either gets the flock (the previous holder is gone) or it
|
|
71
71
|
* fails. The lock file also carries the holder's identity so an OLD-format
|
|
72
72
|
* writer (which does not honor flock) still sees a live owner and
|
|
73
73
|
* refuses to take over — a best-effort guard, NOT a seamless rolling
|
|
74
|
-
* upgrade (
|
|
74
|
+
* upgrade (round 5 P1-4): the documented upgrade contract is quarantine —
|
|
75
75
|
* stop every old-format process, then start the new version.
|
|
76
76
|
* No recursion, no deletion, no window between NEW-format writers.
|
|
77
77
|
*/
|
|
78
78
|
private acquireLock;
|
|
79
|
-
/**
|
|
79
|
+
/** round 5(P1-2): true only while the helper process is alive. */
|
|
80
80
|
private lockHeld;
|
|
81
81
|
/**
|
|
82
82
|
* Release OUR lock only: kill OUR helper. The kernel releases the
|
|
83
83
|
* flock with the helper's death; the identity file is CLEARED so a
|
|
84
84
|
* same-process successor is never mistaken for a live legacy owner —
|
|
85
|
-
* the flock is the authority, the file is advisory (
|
|
85
|
+
* the flock is the authority, the file is advisory (round 4).
|
|
86
86
|
*/
|
|
87
87
|
private releaseLock;
|
|
88
88
|
/** Write-ahead: durable (written + fsynced) before returning. */
|
package/dist/store.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* SessionStore — append-only JSONL durability, identity-safe (A
|
|
2
|
+
* SessionStore — append-only JSONL durability, identity-safe (A group).
|
|
3
3
|
*
|
|
4
4
|
* One file per session: `<root>/<id>.jsonl`, lines of
|
|
5
5
|
* `{"runId": string, "ts": number, "event": Event}`. The single-writer
|
|
6
|
-
* lock (
|
|
6
|
+
* lock (round 4) is an EXCLUSIVE KERNEL flock on `<id>.lock`, held by a
|
|
7
7
|
* dedicated helper process:
|
|
8
8
|
*
|
|
9
9
|
* - the kernel arbitrates every race — a contender can never remove or
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* the lock simply exists while the helper lives and vanishes with it;
|
|
12
12
|
* - the lock file ALSO carries `{"pid": number, "token": string}` written
|
|
13
13
|
* by the holder, as a best-effort guard for OLD-format writers (whose
|
|
14
|
-
* O_EXCL pidfile scheme does not honor flock).
|
|
14
|
+
* O_EXCL pidfile scheme does not honor flock). round 5(P1-4): this guard
|
|
15
15
|
* is NOT a seamless rolling upgrade — an old writer that created an
|
|
16
16
|
* empty lock file before writing its pid creates a split-brain window
|
|
17
17
|
* that a pidfile read cannot close. The documented upgrade contract is
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* held helper — a foreign close can never release another writer's
|
|
23
23
|
* kernel lock (flock is tied to the helper's open file description).
|
|
24
24
|
*
|
|
25
|
-
* Consistency contract (A
|
|
25
|
+
* Consistency contract (A group):
|
|
26
26
|
* - every id is validated BEFORE any file side effect (append, close,
|
|
27
27
|
* load, lock paths);
|
|
28
28
|
* - append runs an expected-last-seq CAS against the file's REAL last
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* so the in-memory EventLog never continues past a rejected write;
|
|
32
32
|
* - the torn tail is repaired before EVERY append, and committed records
|
|
33
33
|
* (newline-terminated) are never truncated;
|
|
34
|
-
* - load is strict (A
|
|
34
|
+
* - load is strict (A group round 1): a partial final line is the only
|
|
35
35
|
* tolerated damage; everything else throws StoreCorruptionError.
|
|
36
36
|
*/
|
|
37
37
|
import { spawn } from "node:child_process";
|
|
@@ -59,10 +59,10 @@ export class SessionStore {
|
|
|
59
59
|
#fds = new Map();
|
|
60
60
|
/** sessionId → the lock helper process THIS instance spawned. */
|
|
61
61
|
#lockHelpers = new Map();
|
|
62
|
-
/**
|
|
62
|
+
/** round 4 (adversarial): serialize concurrent acquireLock calls ON this instance —
|
|
63
63
|
* two racing appends must not spawn two helpers and fight each other. */
|
|
64
64
|
#lockAcquiring = new Map();
|
|
65
|
-
/**
|
|
65
|
+
/** round 5(P1-1): serialize the WHOLE append critical section per session on
|
|
66
66
|
* this instance — lock check → CAS → write → fsync. A rejected write
|
|
67
67
|
* propagates to every append queued behind it, so a concurrent write can
|
|
68
68
|
* never land after a stale failure (which would fork memory and disk). */
|
|
@@ -84,19 +84,19 @@ export class SessionStore {
|
|
|
84
84
|
return join(this.root, `${sessionId}.lock`);
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
87
|
-
* Take the single-writer lock (
|
|
87
|
+
* Take the single-writer lock (round 4): an EXCLUSIVE kernel flock held
|
|
88
88
|
* by a dedicated helper process. The KERNEL arbitrates every race —
|
|
89
89
|
* there is no stale lock to delete and no takeover to race: a
|
|
90
90
|
* contender either gets the flock (the previous holder is gone) or it
|
|
91
91
|
* fails. The lock file also carries the holder's identity so an OLD-format
|
|
92
92
|
* writer (which does not honor flock) still sees a live owner and
|
|
93
93
|
* refuses to take over — a best-effort guard, NOT a seamless rolling
|
|
94
|
-
* upgrade (
|
|
94
|
+
* upgrade (round 5 P1-4): the documented upgrade contract is quarantine —
|
|
95
95
|
* stop every old-format process, then start the new version.
|
|
96
96
|
* No recursion, no deletion, no window between NEW-format writers.
|
|
97
97
|
*/
|
|
98
98
|
async acquireLock(sessionId) {
|
|
99
|
-
//
|
|
99
|
+
// round 5(P1-2): the lock is held only while the helper PROCESS is
|
|
100
100
|
// alive — flock is bound to the helper's lifetime. A dead helper's
|
|
101
101
|
// entry must never be trusted as "locked".
|
|
102
102
|
if (this.lockHeld(sessionId))
|
|
@@ -108,7 +108,7 @@ export class SessionStore {
|
|
|
108
108
|
this.#lockAcquiring.set(sessionId, attempt);
|
|
109
109
|
return attempt;
|
|
110
110
|
}
|
|
111
|
-
/**
|
|
111
|
+
/** round 5(P1-2): true only while the helper process is alive. */
|
|
112
112
|
lockHeld(sessionId) {
|
|
113
113
|
const child = this.#lockHelpers.get(sessionId);
|
|
114
114
|
if (child === undefined || child.pid === undefined || child.pid <= 0)
|
|
@@ -128,7 +128,7 @@ export class SessionStore {
|
|
|
128
128
|
// be alive — its lock file names it. Refuse, and release
|
|
129
129
|
// the flock (the helper dies). A MODERN lock (with a token)
|
|
130
130
|
// naming OUR OWN process is a same-process writer's residue
|
|
131
|
-
// (
|
|
131
|
+
// (round 4: the file is advisory; the flock is the authority).
|
|
132
132
|
const legacy = readLockIdentity(lockPath);
|
|
133
133
|
if (legacy?.pid !== undefined && isAlive(legacy.pid) && (legacy.token === undefined || legacy.pid !== process.pid)) {
|
|
134
134
|
child.kill();
|
|
@@ -144,7 +144,7 @@ export class SessionStore {
|
|
|
144
144
|
// the file itself is advisory — the kernel lock holds
|
|
145
145
|
}
|
|
146
146
|
this.#lockHelpers.set(sessionId, child);
|
|
147
|
-
//
|
|
147
|
+
// round 5(P1-2): the helper's death removes the entry — the
|
|
148
148
|
// flock dies with the process; a later append re-acquires
|
|
149
149
|
// (and fails honestly if a rival holds the flock now).
|
|
150
150
|
child.on("exit", () => {
|
|
@@ -156,7 +156,7 @@ export class SessionStore {
|
|
|
156
156
|
}
|
|
157
157
|
child.kill();
|
|
158
158
|
if (verdict === "SPAWN_FAILED") {
|
|
159
|
-
//
|
|
159
|
+
// round 4 (adversarial): the helper could not start (python3 missing) —
|
|
160
160
|
// an HONEST error, never a fake lock conflict.
|
|
161
161
|
throw new Error(`session locking unavailable: the flock helper (python3) failed to start for ${sessionId}`);
|
|
162
162
|
}
|
|
@@ -164,7 +164,7 @@ export class SessionStore {
|
|
|
164
164
|
// exiting (its helper is dying). A FOREIGN live writer's identity
|
|
165
165
|
// is in the file — refuse at once. A MODERN lock (with a token)
|
|
166
166
|
// naming OUR OWN process is a same-process writer — it will
|
|
167
|
-
// release its helper; retry until it does (
|
|
167
|
+
// release its helper; retry until it does (round 4: never a
|
|
168
168
|
// spurious self-conflict). A legacy bare-pid lock naming our own
|
|
169
169
|
// process is still a live foreign owner and is refused.
|
|
170
170
|
const legacy = readLockIdentity(lockPath);
|
|
@@ -174,7 +174,7 @@ export class SessionStore {
|
|
|
174
174
|
if (attempt >= 25) {
|
|
175
175
|
throw new Error(`session ${sessionId} is locked by another writer`);
|
|
176
176
|
}
|
|
177
|
-
//
|
|
177
|
+
// round 5(P1-3): a close() that landed while we waited ends the
|
|
178
178
|
// acquisition immediately — no 500ms wait, no lock at all.
|
|
179
179
|
if (this.#closed.has(sessionId)) {
|
|
180
180
|
throw new Error(`session store is closed for ${sessionId}`);
|
|
@@ -186,14 +186,14 @@ export class SessionStore {
|
|
|
186
186
|
* Release OUR lock only: kill OUR helper. The kernel releases the
|
|
187
187
|
* flock with the helper's death; the identity file is CLEARED so a
|
|
188
188
|
* same-process successor is never mistaken for a live legacy owner —
|
|
189
|
-
* the flock is the authority, the file is advisory (
|
|
189
|
+
* the flock is the authority, the file is advisory (round 4).
|
|
190
190
|
*/
|
|
191
191
|
releaseLock(sessionId) {
|
|
192
192
|
const child = this.#lockHelpers.get(sessionId);
|
|
193
193
|
if (child === undefined)
|
|
194
194
|
return;
|
|
195
195
|
this.#lockHelpers.delete(sessionId);
|
|
196
|
-
//
|
|
196
|
+
// round 4 (adversarial): the identity is cleared BEFORE the helper dies — a
|
|
197
197
|
// contender that acquires the flock in the release gap writes its
|
|
198
198
|
// own identity AFTER our clear, so it is never wiped by us (the
|
|
199
199
|
// file is advisory; the kernel flock is the authority).
|
|
@@ -212,7 +212,7 @@ export class SessionStore {
|
|
|
212
212
|
throw new Error(`session store is closed for ${sessionId}`);
|
|
213
213
|
}
|
|
214
214
|
this.pathFor(sessionId); // id validated before ANY file side effect
|
|
215
|
-
//
|
|
215
|
+
// round 5(P1-1): the WHOLE critical section is serialized per session
|
|
216
216
|
// on this instance — and a rejection PROPAGATES to every append
|
|
217
217
|
// queued behind it: a concurrent write can never land after a
|
|
218
218
|
// stale failure that poisoned the session.
|
|
@@ -229,7 +229,7 @@ export class SessionStore {
|
|
|
229
229
|
}
|
|
230
230
|
}
|
|
231
231
|
async #appendOnce(sessionId, runId, event) {
|
|
232
|
-
//
|
|
232
|
+
// round 5(P1-3): close() may have returned while we waited — the
|
|
233
233
|
// lifecycle barrier is re-checked after the lock acquisition.
|
|
234
234
|
if (this.#closed.has(sessionId)) {
|
|
235
235
|
throw new Error(`session store is closed for ${sessionId}`);
|
|
@@ -247,13 +247,13 @@ export class SessionStore {
|
|
|
247
247
|
}
|
|
248
248
|
catch (err) {
|
|
249
249
|
// The lock was acquired but the JSONL could not be opened:
|
|
250
|
-
// release the lock — it must not leak (A
|
|
250
|
+
// release the lock — it must not leak (A group).
|
|
251
251
|
this.releaseLock(sessionId);
|
|
252
252
|
throw err;
|
|
253
253
|
}
|
|
254
254
|
repairTornTail(fd);
|
|
255
255
|
// Expected-last-seq CAS against the file's REAL last committed seq
|
|
256
|
-
// (A
|
|
256
|
+
// (A group): a stale preloaded handle cannot write a duplicate seq.
|
|
257
257
|
const last = lastCommittedSeq(fd);
|
|
258
258
|
const expected = (last ?? -1) + 1;
|
|
259
259
|
if (event.seq !== expected) {
|
|
@@ -261,7 +261,7 @@ export class SessionStore {
|
|
|
261
261
|
}
|
|
262
262
|
appendFileSync(fd, `${JSON.stringify({ runId, ts: Date.now(), event })}\n`);
|
|
263
263
|
fsyncSync(fd);
|
|
264
|
-
//
|
|
264
|
+
// round 5(P1-3): a close() that landed during the write must not
|
|
265
265
|
// leave our helper behind.
|
|
266
266
|
if (this.#closed.has(sessionId)) {
|
|
267
267
|
this.releaseLock(sessionId);
|
|
@@ -302,7 +302,7 @@ export class SessionStore {
|
|
|
302
302
|
if (lines[i] !== "")
|
|
303
303
|
nonEmpty.push(i);
|
|
304
304
|
}
|
|
305
|
-
//
|
|
305
|
+
// round 2: a line WITHOUT a trailing newline is NOT committed — whether
|
|
306
306
|
// or not it happens to parse. load and append must agree: append's
|
|
307
307
|
// torn-tail repair truncates exactly what load refuses to return.
|
|
308
308
|
const tolerantTail = !raw.endsWith("\n");
|
|
@@ -384,7 +384,7 @@ function isRecord(value) {
|
|
|
384
384
|
return typeof v.runId === "string" && typeof v.ts === "number" && isKisoEvent(v.event);
|
|
385
385
|
}
|
|
386
386
|
/**
|
|
387
|
-
* Read a lock file's holder identity (
|
|
387
|
+
* Read a lock file's holder identity (round 4). Formats:
|
|
388
388
|
* modern: {"pid": 123, "token": "..."}
|
|
389
389
|
* legacy: a bare pid — either the STRING "123" or, because
|
|
390
390
|
* JSON.parse("123") yields the NUMBER 123, the number itself.
|
|
@@ -480,7 +480,7 @@ function helperVerdict(child) {
|
|
|
480
480
|
});
|
|
481
481
|
child.stdout?.on("end", () => done(buf.trim()));
|
|
482
482
|
child.stdout?.on("error", () => done("FAILED"));
|
|
483
|
-
//
|
|
483
|
+
// round 5(P2-1): a spawn failure (python3 missing, exec denied) is
|
|
484
484
|
// DISTINCT from a busy lock — the caller must not report "locked by
|
|
485
485
|
// another writer" for a missing helper. The verdict is SPAWN_FAILED
|
|
486
486
|
// and the acquire path checks exactly that string.
|
package/dist/summarize.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The /compact summary layer (ADR-0044) — the MODEL-GENERATED half of
|
|
3
|
-
* context economy.
|
|
3
|
+
* context economy. the home-relocation extraction (0.1.26 gate ruling): this OFF-LOOP
|
|
4
4
|
* ORCHESTRATION lived in the kernel by a context-round expedience; it
|
|
5
5
|
* calls the ADAPTER to generate the summary, which is the RUNTIME's
|
|
6
6
|
* business — the kernel's duty is the `summarized` EVENT TYPE and the
|
package/dist/summarize.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The /compact summary layer (ADR-0044) — the MODEL-GENERATED half of
|
|
3
|
-
* context economy.
|
|
3
|
+
* context economy. the home-relocation extraction (0.1.26 gate ruling): this OFF-LOOP
|
|
4
4
|
* ORCHESTRATION lived in the kernel by a context-round expedience; it
|
|
5
5
|
* calls the ADAPTER to generate the summary, which is the RUNTIME's
|
|
6
6
|
* business — the kernel's duty is the `summarized` EVENT TYPE and the
|
package/dist/trust.d.ts
CHANGED
|
@@ -53,7 +53,7 @@ export interface ProjectArtifacts {
|
|
|
53
53
|
* no recognized artifacts (an empty .kiso has nothing to gate).
|
|
54
54
|
*/
|
|
55
55
|
export declare function projectArtifacts(cwd: string): Promise<ProjectArtifacts | null>;
|
|
56
|
-
/**
|
|
56
|
+
/** finding #11: KISO_HOME is the ONE root — the store derives from it. */
|
|
57
57
|
export declare function kisoHome(): string;
|
|
58
58
|
/**
|
|
59
59
|
* The last record matching (root, digest) — append-only, last wins. A
|
package/dist/trust.js
CHANGED
|
@@ -44,7 +44,7 @@ export async function projectArtifacts(cwd) {
|
|
|
44
44
|
throw err;
|
|
45
45
|
}
|
|
46
46
|
const root = await realpath(kisoDir);
|
|
47
|
-
//
|
|
47
|
+
// finding #10 (P1): when cwd IS the KISO_HOME parent (the user's home
|
|
48
48
|
// directory), <cwd>/.kiso IS the user-level config directory itself —
|
|
49
49
|
// trusting your own configuration is nonsense, and the mcp merge would
|
|
50
50
|
// mirror the same file onto itself and loudly conflict. The home is
|
|
@@ -83,7 +83,7 @@ export async function projectArtifacts(cwd) {
|
|
|
83
83
|
throw err; // a file named like a dir → ENOTDIR: inert, skip
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
//
|
|
86
|
+
// merge round B: the project's config.json is an artifact of the trust
|
|
87
87
|
// package — trusting the package trusts its config, and a CHANGED
|
|
88
88
|
// config is a changed digest (the trust decision re-evaluates).
|
|
89
89
|
try {
|
|
@@ -134,7 +134,7 @@ async function readdirOrEmpty(dir) {
|
|
|
134
134
|
function isMissing(err) {
|
|
135
135
|
return err.code === "ENOENT" || err.code === "ENOTDIR";
|
|
136
136
|
}
|
|
137
|
-
/**
|
|
137
|
+
/** finding #11: KISO_HOME is the ONE root — the store derives from it. */
|
|
138
138
|
export function kisoHome() {
|
|
139
139
|
return process.env.KISO_HOME ?? join(homedir(), ".kiso");
|
|
140
140
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "kiso runtime — durable multi-turn agent sessions: AgentDefinition, AgentRuntime, AgentSession, Run, append-only JSONL store.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@vincemakes/kiso-core": "0.1.
|
|
24
|
+
"@vincemakes/kiso-core": "0.1.30"
|
|
25
25
|
},
|
|
26
26
|
"peerDependencies": {
|
|
27
|
-
"@vincemakes/kiso-provider-anthropic": "0.1.
|
|
28
|
-
"@vincemakes/kiso-provider-openai": "0.1.
|
|
27
|
+
"@vincemakes/kiso-provider-anthropic": "0.1.31",
|
|
28
|
+
"@vincemakes/kiso-provider-openai": "0.1.31"
|
|
29
29
|
},
|
|
30
30
|
"peerDependenciesMeta": {
|
|
31
31
|
"@vincemakes/kiso-provider-anthropic": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
}
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
|
-
"@vincemakes/kiso-evals": "0.1.
|
|
39
|
+
"@vincemakes/kiso-evals": "0.1.31",
|
|
40
40
|
"@types/node": "^26.1.2",
|
|
41
41
|
"typescript": "^5.7.2",
|
|
42
42
|
"vitest": "^3.0.0"
|