@vincemakes/kiso-runtime 0.1.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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * SessionStore — append-only JSONL durability, identity-safe (A 组).
3
+ *
4
+ * One file per session: `<root>/<id>.jsonl`, lines of
5
+ * `{"runId": string, "ts": number, "event": Event}`. The single-writer
6
+ * lock (第四轮) is an EXCLUSIVE KERNEL flock on `<id>.lock`, held by a
7
+ * dedicated helper process:
8
+ *
9
+ * - the kernel arbitrates every race — a contender can never remove or
10
+ * overwrite a live holder's lock, because there is nothing to remove;
11
+ * the lock simply exists while the helper lives and vanishes with it;
12
+ * - the lock file ALSO carries `{"pid": number, "token": string}` written
13
+ * by the holder, as a best-effort guard for OLD-format writers (whose
14
+ * O_EXCL pidfile scheme does not honor flock). 第五轮(P1-4): this guard
15
+ * is NOT a seamless rolling upgrade — an old writer that created an
16
+ * empty lock file before writing its pid creates a split-brain window
17
+ * that a pidfile read cannot close. The documented upgrade contract is
18
+ * QUARANTINE: stop every old-format process, THEN start the new
19
+ * version. A dead/empty/unreadable legacy lock is otherwise harmless —
20
+ * flock ignores content, and the kernel lock is what matters;
21
+ * - `close()` releases only THIS instance's helper; `closeAll()` every
22
+ * held helper — a foreign close can never release another writer's
23
+ * kernel lock (flock is tied to the helper's open file description).
24
+ *
25
+ * Consistency contract (A 组):
26
+ * - every id is validated BEFORE any file side effect (append, close,
27
+ * load, lock paths);
28
+ * - append runs an expected-last-seq CAS against the file's REAL last
29
+ * committed seq: a stale preloaded handle writing a duplicate seq is
30
+ * refused with StaleWriterError — and the run that fed it terminates,
31
+ * so the in-memory EventLog never continues past a rejected write;
32
+ * - the torn tail is repaired before EVERY append, and committed records
33
+ * (newline-terminated) are never truncated;
34
+ * - load is strict (A 组 round 1): a partial final line is the only
35
+ * tolerated damage; everything else throws StoreCorruptionError.
36
+ */
37
+ import { type Event } from "@vincemakes/kiso-core";
38
+ /** History that does not parse as a contiguous kiso trajectory. */
39
+ export declare class StoreCorruptionError extends Error {
40
+ constructor(message: string);
41
+ }
42
+ /** A write that would duplicate or skip a seq — the handle is stale. */
43
+ export declare class StaleWriterError extends Error {
44
+ constructor(expected: number, got: number);
45
+ }
46
+ /** A durable record: the run that produced it, and the event itself. */
47
+ export interface StoreRecord {
48
+ readonly runId: string;
49
+ readonly ts: number;
50
+ readonly event: Event;
51
+ }
52
+ export interface SessionMeta {
53
+ readonly id: string;
54
+ readonly title: string;
55
+ readonly events: number;
56
+ readonly runs: number;
57
+ readonly createdAt: number;
58
+ readonly updatedAt: number;
59
+ }
60
+ export declare class SessionStore {
61
+ #private;
62
+ readonly root: string;
63
+ constructor(root: string);
64
+ private pathFor;
65
+ private lockPathFor;
66
+ /**
67
+ * Take the single-writer lock (第四轮): an EXCLUSIVE kernel flock held
68
+ * by a dedicated helper process. The KERNEL arbitrates every race —
69
+ * there is no stale lock to delete and no takeover to race: a
70
+ * contender either gets the flock (the previous holder is gone) or it
71
+ * fails. The lock file also carries the holder's identity so an OLD-format
72
+ * writer (which does not honor flock) still sees a live owner and
73
+ * refuses to take over — a best-effort guard, NOT a seamless rolling
74
+ * upgrade (第五轮 P1-4): the documented upgrade contract is quarantine —
75
+ * stop every old-format process, then start the new version.
76
+ * No recursion, no deletion, no window between NEW-format writers.
77
+ */
78
+ private acquireLock;
79
+ /** 第五轮(P1-2): true only while the helper process is alive. */
80
+ private lockHeld;
81
+ /**
82
+ * Release OUR lock only: kill OUR helper. The kernel releases the
83
+ * flock with the helper's death; the identity file is CLEARED so a
84
+ * same-process successor is never mistaken for a live legacy owner —
85
+ * the flock is the authority, the file is advisory (第四轮).
86
+ */
87
+ private releaseLock;
88
+ /** Write-ahead: durable (written + fsynced) before returning. */
89
+ append(sessionId: string, runId: string, event: Event): Promise<void>;
90
+ /**
91
+ * Open (creating if needed) the session file. The torn tail is repaired
92
+ * here AND before every append — an in-process append failure cannot
93
+ * poison the next one. The parent directory is fsynced so the file's
94
+ * existence survives a crash.
95
+ */
96
+ private fd;
97
+ /**
98
+ * Replay a session's log. The ONLY tolerated damage is a partial final
99
+ * line (a crash mid-write): it is dropped and the contiguous prefix is
100
+ * returned. Anything else — mid-file garbage, valid JSON that is not a
101
+ * kiso record, a seq that is not 0..N — throws StoreCorruptionError.
102
+ */
103
+ load(sessionId: string): StoreRecord[];
104
+ has(sessionId: string): boolean;
105
+ list(): SessionMeta[];
106
+ /** Release a session's fd and OUR writer lock. Idempotent. */
107
+ close(sessionId: string): void;
108
+ /** Release every held fd and lock, including locks whose JSONL open failed. */
109
+ closeAll(): void;
110
+ }
111
+ export type { Event };