@vincemakes/kiso-runtime 0.32.1 → 0.32.2
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/recovery.d.ts +3 -1
- package/dist/recovery.js +48 -9
- package/dist/store.d.ts +0 -13
- package/dist/store.js +50 -4
- package/package.json +6 -6
package/dist/recovery.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare class MergedSignal implements AbortSignalStub {
|
|
|
23
23
|
#private;
|
|
24
24
|
constructor(...sources: readonly AbortSignalLike[]);
|
|
25
25
|
get aborted(): boolean;
|
|
26
|
-
addEventListener(_type: string, listener: (this: AbortSignalStub, ev: unknown) => void
|
|
26
|
+
addEventListener(_type: string, listener: (this: AbortSignalStub, ev: unknown) => void, _options?: {
|
|
27
|
+
once?: boolean;
|
|
28
|
+
}): void;
|
|
27
29
|
removeEventListener(_type: string, listener: (this: AbortSignalStub, ev: unknown) => void): void;
|
|
28
30
|
}
|
package/dist/recovery.js
CHANGED
|
@@ -23,7 +23,7 @@ export const ABORTED = Symbol("kiso-resume-aborted");
|
|
|
23
23
|
export async function abortable(promise, signal) {
|
|
24
24
|
if (signal.aborted)
|
|
25
25
|
return ABORTED;
|
|
26
|
-
return new Promise((resolve) => {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
27
|
const onAbort = () => {
|
|
28
28
|
signal.removeEventListener("abort", onAbort);
|
|
29
29
|
resolve(ABORTED);
|
|
@@ -33,8 +33,16 @@ export async function abortable(promise, signal) {
|
|
|
33
33
|
signal.removeEventListener("abort", onAbort);
|
|
34
34
|
resolve(value);
|
|
35
35
|
}, (err) => {
|
|
36
|
+
// R3: REJECT, do not throw. Throwing here creates a new
|
|
37
|
+
// rejected promise that nobody holds, while the executor
|
|
38
|
+
// above never calls resolve or reject — so the outer promise
|
|
39
|
+
// NEVER SETTLES. Three recovery sites await this, so a
|
|
40
|
+
// decision hook that rejects hung the run and left an
|
|
41
|
+
// unhandled rejection behind it. A hook that throws is a host
|
|
42
|
+
// bug: it fails the run, the tool never runs, and there is no
|
|
43
|
+
// silent allow.
|
|
36
44
|
signal.removeEventListener("abort", onAbort);
|
|
37
|
-
|
|
45
|
+
reject(err);
|
|
38
46
|
});
|
|
39
47
|
});
|
|
40
48
|
}
|
|
@@ -44,23 +52,54 @@ export async function abortable(promise, signal) {
|
|
|
44
52
|
*/
|
|
45
53
|
export class MergedSignal {
|
|
46
54
|
#sources;
|
|
55
|
+
/** R5: the ORIGINAL listeners, by identity. The previous Set held
|
|
56
|
+
* WRAPPERS while `removeEventListener` deleted the original, so
|
|
57
|
+
* removal never removed anything and every listener ever added fired
|
|
58
|
+
* for the life of the run. */
|
|
47
59
|
#listeners = new Set();
|
|
60
|
+
/** R5: `aborted` is a TRANSITION, not a stream. Two sources aborting
|
|
61
|
+
* fired every listener twice. */
|
|
62
|
+
#fired = false;
|
|
63
|
+
/** R5: the source subscriptions, released on the transition — they
|
|
64
|
+
* were never released at all. */
|
|
65
|
+
#cleanups = [];
|
|
48
66
|
constructor(...sources) {
|
|
49
67
|
this.#sources = sources;
|
|
50
68
|
for (const source of sources) {
|
|
51
69
|
if (source.aborted)
|
|
52
70
|
continue;
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
71
|
+
const onSource = () => this.#fire();
|
|
72
|
+
source.addEventListener("abort", onSource, { once: true });
|
|
73
|
+
this.#cleanups.push(() => source.removeEventListener("abort", onSource));
|
|
57
74
|
}
|
|
58
75
|
}
|
|
76
|
+
/** One transition: latch, release the sources, then call each listener
|
|
77
|
+
* once. Clearing the set before the calls is what honours `once` for
|
|
78
|
+
* every listener without tracking the flag per listener — after the
|
|
79
|
+
* one transition there is nothing left to fire a second time. */
|
|
80
|
+
#fire() {
|
|
81
|
+
if (this.#fired)
|
|
82
|
+
return;
|
|
83
|
+
this.#fired = true;
|
|
84
|
+
for (const cleanup of this.#cleanups)
|
|
85
|
+
cleanup();
|
|
86
|
+
this.#cleanups = [];
|
|
87
|
+
const listeners = [...this.#listeners];
|
|
88
|
+
this.#listeners.clear();
|
|
89
|
+
for (const listener of listeners)
|
|
90
|
+
listener.call(this, undefined);
|
|
91
|
+
}
|
|
59
92
|
get aborted() {
|
|
60
|
-
return this.#sources.some((s) => s.aborted);
|
|
93
|
+
return this.#fired || this.#sources.some((s) => s.aborted);
|
|
61
94
|
}
|
|
62
|
-
addEventListener(_type, listener) {
|
|
63
|
-
|
|
95
|
+
addEventListener(_type, listener, _options) {
|
|
96
|
+
// A listener added AFTER the transition never fires, which is what a
|
|
97
|
+
// real AbortSignal does — the event is the transition. Callers read
|
|
98
|
+
// `aborted` for the state; `abortable` checks it before it ever
|
|
99
|
+
// subscribes, which is the guard that makes this safe.
|
|
100
|
+
if (this.#fired)
|
|
101
|
+
return;
|
|
102
|
+
this.#listeners.add(listener);
|
|
64
103
|
}
|
|
65
104
|
removeEventListener(_type, listener) {
|
|
66
105
|
this.#listeners.delete(listener);
|
package/dist/store.d.ts
CHANGED
|
@@ -66,19 +66,6 @@ export interface SessionMeta {
|
|
|
66
66
|
readonly createdAt: number;
|
|
67
67
|
readonly updatedAt: number;
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
70
|
-
* The session's title — its first substantive prompt.
|
|
71
|
-
*
|
|
72
|
-
* Exported on the INTERNAL entry (never the public root: this is a
|
|
73
|
-
* projection helper, not an API the runtime wants to support forever —
|
|
74
|
-
* and adding it to the root surface is a ritual the api-surface gate
|
|
75
|
-
* rightly refused). It has two consumers and may only have one
|
|
76
|
-
* definition: `list()` below, which is what `kiso sessions` prints, and
|
|
77
|
-
* the CLI's session-card projection, which is what the resume PICKER
|
|
78
|
-
* shows. They were allowed to differ once — the listing got the title
|
|
79
|
-
* and the picker kept showing bare ids, so the fix reached the surface
|
|
80
|
-
* nobody uses and missed the one everybody does.
|
|
81
|
-
*/
|
|
82
69
|
export declare function sessionTitle(records: readonly StoreRecord[]): string;
|
|
83
70
|
export declare class SessionStore {
|
|
84
71
|
#private;
|
package/dist/store.js
CHANGED
|
@@ -99,11 +99,44 @@ const isOpener = (t) => [...t].length < MIN_TITLE_CHARS || ASCII_OPENER.test(t);
|
|
|
99
99
|
* and the picker kept showing bare ids, so the fix reached the surface
|
|
100
100
|
* nobody uses and missed the one everybody does.
|
|
101
101
|
*/
|
|
102
|
+
/**
|
|
103
|
+
* R7 — what a `user_input` SAYS, as one string.
|
|
104
|
+
*
|
|
105
|
+
* A turn carrying an image is a content ARRAY, and the title only ever
|
|
106
|
+
* read `typeof content === "string"` — so such a turn was filtered out
|
|
107
|
+
* entirely and the session was titled "(no prompt)". Worse where a
|
|
108
|
+
* greeting came first: the opener `isOpener` exists to skip became the
|
|
109
|
+
* title, because the substantive turn was not in the list to be found.
|
|
110
|
+
* Exactly the sessions a human recognises by what they showed it.
|
|
111
|
+
*
|
|
112
|
+
* Projected HERE, in the runtime, with no dependency on the TUI packages.
|
|
113
|
+
* The dependency runs one way; a store that reached into the renderer to
|
|
114
|
+
* name a session would invert it. Unknown block types contribute nothing
|
|
115
|
+
* rather than a guess — a title is a label, and a label that invents is
|
|
116
|
+
* worse than a label that is short.
|
|
117
|
+
*/
|
|
118
|
+
function contentText(content) {
|
|
119
|
+
if (typeof content === "string")
|
|
120
|
+
return content;
|
|
121
|
+
if (!Array.isArray(content))
|
|
122
|
+
return "";
|
|
123
|
+
const parts = [];
|
|
124
|
+
for (const block of content) {
|
|
125
|
+
if (block === null || typeof block !== "object")
|
|
126
|
+
continue;
|
|
127
|
+
const b = block;
|
|
128
|
+
if (b.type === "text" && typeof b.text === "string")
|
|
129
|
+
parts.push(b.text);
|
|
130
|
+
else if (b.type === "image")
|
|
131
|
+
parts.push("(image)");
|
|
132
|
+
}
|
|
133
|
+
return parts.join(" ");
|
|
134
|
+
}
|
|
102
135
|
export function sessionTitle(records) {
|
|
103
136
|
const asked = records
|
|
104
137
|
.map((r) => r.event)
|
|
105
|
-
.filter((e) => e.type === "user_input"
|
|
106
|
-
.map((e) => e.content.trim())
|
|
138
|
+
.filter((e) => e.type === "user_input")
|
|
139
|
+
.map((e) => contentText(e.content).trim())
|
|
107
140
|
.filter((t) => t !== "");
|
|
108
141
|
if (asked.length === 0)
|
|
109
142
|
return "(no prompt)";
|
|
@@ -135,7 +168,18 @@ export class SessionStore {
|
|
|
135
168
|
constructor(root, opts) {
|
|
136
169
|
this.root = root;
|
|
137
170
|
this.#lockAdapter = opts?.lockAdapter ?? nativeLockAdapter;
|
|
138
|
-
|
|
171
|
+
// R6: PRIVATE by default. Under the common umask 022 this came out
|
|
172
|
+
// 0755 and the logs 0644 — a session's full transcript, prompts and
|
|
173
|
+
// tool inputs and the contents of every file the model read, was
|
|
174
|
+
// world-readable on a shared machine. `auth.json` has been 0700/0600
|
|
175
|
+
// since the credential store shipped; the logs are no less sensitive
|
|
176
|
+
// and were never given the same treatment.
|
|
177
|
+
//
|
|
178
|
+
// EXISTING FILES ARE NOT MIGRATED. Changing modes under someone's
|
|
179
|
+
// feet is its own surprise, and a deliberate 0644 is a choice kiso
|
|
180
|
+
// should not silently reverse. A fresh home is private; an old one
|
|
181
|
+
// stays as its owner left it.
|
|
182
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
139
183
|
fsyncDir(root);
|
|
140
184
|
}
|
|
141
185
|
// ── paths and the cross-process lock ─────────────────────────────────
|
|
@@ -282,7 +326,9 @@ export class SessionStore {
|
|
|
282
326
|
if (existing !== undefined)
|
|
283
327
|
return existing;
|
|
284
328
|
const path = this.pathFor(sessionId);
|
|
285
|
-
|
|
329
|
+
// R6: the mode applies at CREATION only — an existing log keeps
|
|
330
|
+
// whatever it has, which is the not-migrated rule above.
|
|
331
|
+
const fd = openSync(path, "a+", 0o600);
|
|
286
332
|
repairTornTail(fd);
|
|
287
333
|
fsyncDir(dirname(path));
|
|
288
334
|
this.#fds.set(sessionId, fd);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-runtime",
|
|
3
|
-
"version": "0.32.
|
|
3
|
+
"version": "0.32.2",
|
|
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",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"test": "vitest run"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@vincemakes/kiso-core": "0.32.
|
|
28
|
+
"@vincemakes/kiso-core": "0.32.2"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@vincemakes/kiso-provider-anthropic": "0.32.
|
|
32
|
-
"@vincemakes/kiso-provider-openai": "0.32.
|
|
33
|
-
"@vincemakes/kiso-provider-openai-responses": "0.32.
|
|
31
|
+
"@vincemakes/kiso-provider-anthropic": "0.32.2",
|
|
32
|
+
"@vincemakes/kiso-provider-openai": "0.32.2",
|
|
33
|
+
"@vincemakes/kiso-provider-openai-responses": "0.32.2"
|
|
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.32.
|
|
47
|
+
"@vincemakes/kiso-evals": "0.32.2",
|
|
48
48
|
"@types/node": "^26.1.2",
|
|
49
49
|
"typescript": "^5.7.2",
|
|
50
50
|
"vitest": "^3.0.0"
|