@vincemakes/kiso-runtime 0.32.1 → 0.33.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/profile.js CHANGED
@@ -57,7 +57,11 @@ export function writeProfile(root, sessionId, profile) {
57
57
  const tmpDir = mkdtempSync(join(root, ".meta-"));
58
58
  const tmp = join(tmpDir, "meta.json");
59
59
  try {
60
- writeFileSync(tmp, `${JSON.stringify({ profile }, null, "\t")}\n`);
60
+ // DF-0322-F1: 0600 on the file kiso CREATES. The mode rides the temp
61
+ // because rename preserves it, and rename is the only path by which
62
+ // this file comes into existence. Existing files are not migrated —
63
+ // R6's choice, kept.
64
+ writeFileSync(tmp, `${JSON.stringify({ profile }, null, "\t")}\n`, { mode: 0o600 });
61
65
  const fd = openSync(tmp, "r");
62
66
  try {
63
67
  fsyncSync(fd);
@@ -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): 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
- throw err;
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
- source.addEventListener("abort", () => {
54
- for (const listener of this.#listeners)
55
- listener();
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
- this.#listeners.add(() => listener.call(this, undefined));
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/session.js CHANGED
@@ -485,8 +485,8 @@ export class AgentSession {
485
485
  try {
486
486
  const canonical = canonicalizeUsageForModel(binding.model, binding.baseUrl, binding.provider ?? "adapter", usage);
487
487
  const line = JSON.stringify({ kind: "summary", canonical }) + "\n";
488
- mkdirSync(join(this.#store.root, "traces"), { recursive: true });
489
- appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), line);
488
+ mkdirSync(join(this.#store.root, "traces"), { recursive: true, mode: 0o700 }); // DF-0322-F1
489
+ appendFileSync(join(this.#store.root, "traces", `${this.id}.jsonl`), line, { mode: 0o600 });
490
490
  }
491
491
  catch (err) {
492
492
  console.error(`[kiso] summary usage ledger degraded (${err instanceof Error ? err.message : String(err)}); the summary call's cost is not recorded`);
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" && typeof e.content === "string")
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
- mkdirSync(root, { recursive: true });
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
- const fd = openSync(path, "a+");
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);
@@ -55,7 +55,7 @@ export class TraceWriter {
55
55
  if (this.#degraded)
56
56
  return;
57
57
  try {
58
- mkdirSync(join(this.#root, "traces"), { recursive: true });
58
+ mkdirSync(join(this.#root, "traces"), { recursive: true, mode: 0o700 }); // DF-0322-F1
59
59
  const fresh = !existsSync(this.#path);
60
60
  if (this.#crashDetect()) {
61
61
  this.#writeSync([
@@ -129,7 +129,11 @@ export class TraceWriter {
129
129
  if (lines.length === 0)
130
130
  return;
131
131
  try {
132
- appendFileSync(this.#path, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`);
132
+ // DF-0322-F1: the mode applies when THIS call creates the file, and
133
+ // it is the only call that ever does — nothing truncates the ledger
134
+ // into existence first (the R6b trap, where an earlier writeFileSync
135
+ // made the append's mode a no-op).
136
+ appendFileSync(this.#path, `${lines.map((l) => JSON.stringify(l)).join("\n")}\n`, { mode: 0o600 });
133
137
  }
134
138
  catch (err) {
135
139
  this.#degrade(err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-runtime",
3
- "version": "0.32.1",
3
+ "version": "0.33.0",
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.1"
28
+ "@vincemakes/kiso-core": "0.33.0"
29
29
  },
30
30
  "peerDependencies": {
31
- "@vincemakes/kiso-provider-anthropic": "0.32.1",
32
- "@vincemakes/kiso-provider-openai": "0.32.1",
33
- "@vincemakes/kiso-provider-openai-responses": "0.32.1"
31
+ "@vincemakes/kiso-provider-anthropic": "0.33.0",
32
+ "@vincemakes/kiso-provider-openai": "0.33.0",
33
+ "@vincemakes/kiso-provider-openai-responses": "0.33.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.32.1",
47
+ "@vincemakes/kiso-evals": "0.33.0",
48
48
  "@types/node": "^26.1.2",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^3.0.0"