@vincemakes/kiso-runtime 0.1.28 → 0.1.30

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 CHANGED
@@ -51,14 +51,14 @@ export interface AgentDefinition {
51
51
  readonly compaction?: {
52
52
  readonly thresholdTokens: number;
53
53
  };
54
- /** C 区: microcompact threshold — passed through to every session. */
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 (既有先行), their approvals join the
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 组: the CLI closes on exit). */
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
- * 合并轮 B: the adapter factory the CLI uses for /model switches — the
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 懒连接): an extension's tools array is LIVE — the
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 组: the CLI closes on exit). */
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
- * 合并轮 B: the adapter factory the CLI uses for /model switches — the
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
- // 七: the runtime imports ONLY the provider package — its high-level
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
- * 手感批 B4 (pure move) — the E1/E2 composition helpers, moved verbatim
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 (既有先行), and the loop's microcompact config lookup.
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 (既有先行 — the existing
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
- * 手感批 B4 (pure move) — the E1/E2 composition helpers, moved verbatim
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 (既有先行), and the loop's microcompact config lookup.
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 (既有先行 — the existing
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
- // 复审 E1-P2: the pipe + veto short-circuit — each handler sees the
53
- // message as the PREVIOUS one left it (既有先行), and a null (veto)
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
@@ -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
- * 发现#8 (P1): dispose every extension's external resources — each call
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.
@@ -61,7 +61,7 @@ export async function loadExtensions(dir) {
61
61
  return out;
62
62
  }
63
63
  /**
64
- * 发现#8 (P1): dispose every extension's external resources — each call
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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 手感批 B4 (pure move) — the recovery support pieces, moved verbatim from
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
- * 手感批 B4 (pure move) — the recovery support pieces, moved verbatim from
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 手感批 B4 (pure move) — the Run class (a single turn: write-ahead
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
  */
package/dist/run.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * 手感批 B4 (pure move) — the Run class (a single turn: write-ahead
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
- // 第四轮: health is re-checked when the iterator ACTUALLY starts —
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
- // 第四轮(对抗): the abort paths consult these so a verdict
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 组: recovery is PER-RUN, keyed by StoreRecord.runId ──
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
- // 四: a session with an open run REFUSES new runs at the
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
- // 第五轮(P1-5): flush verdicts the consumer submitted before the
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
- // 四: paired by events NEWER than the request — a historical
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
- // 第五轮(P1-6): a verdict given in the same instant as the
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
- // 第四轮(对抗): a verdict given in the same instant as the
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. 四: pairing is by executionId — a same-callId result
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
- // 八: the repaired result reproduces the normal path
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 crash window: a resolution was persisted but its tool_result
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. 四: keyed by executionId, and the fill
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
- // 裁决 #12 修正一: the honest note rides the recovered failure too —
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
- // 五: live tags survive the resumed path too.
443
+ // round 5: live tags survive the resumed path too.
444
444
  ...(result.tags !== undefined ? { tags: result.tags } : {}),
445
445
  executionId,
446
446
  });
447
- // 裁决 #12 (ADR-0038): the failed-receipt uncertain PAUSE is REMOVED
448
- // here too (it mirrored the live loop's C pause) — a complete
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
- * 手感批 B4 (pure move): the Run class lives in run.ts, the recovery
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
- * (一/第四轮): the in-memory log no longer matches the disk — whatever
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
- * 合并轮 B (/model): replace the adapter for SUBSEQUENT runs. The
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 组: a request whose RUN has terminated is DEAD — it is neither
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 ): "rerun" (the human says the side effect did NOT happen — the
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
- /** 第四轮(对抗): a verdict the human already gave for a live decision. */
151
+ /** round 4 (adversarial): a verdict the human already gave for a live decision. */
152
152
  approvalVerdict(decisionId: string): boolean | undefined;
153
- /** 第四轮(对抗): a verdict the human already gave for a live execution. */
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
- * 第五轮(P1-5): flush every verdict submitted to a live resolver that is
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 区: microcompact threshold — passed through to the loop verbatim. */
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
- * 手感批 B4 (pure move): the Run class lives in run.ts, the recovery
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
- /** 七: verdicts already passed to a live resolver — the resolution event
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
- /** 第四轮(对抗): verdicts the human GAVE, recorded when passed to a live
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
- /** 第五轮(P1-5): verdicts submitted to a LIVE resolver but not yet known
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
- * (一/第四轮): the in-memory log no longer matches the disk — whatever
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
- // 第四轮: ANY rejected write poisons — not only the typed
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
- * 合并轮 B (/model): replace the adapter for SUBSEQUENT runs. The
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 组: a request whose RUN has terminated is DEAD — it is neither
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
- // 第四轮: a poisoned session may not mutate the log — checked before
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 组: a late approve() on a TERMINATED run writes nothing and
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
- // 第四轮(对抗): recorded so an abort racing the verdict cannot
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
- // 第五轮(P1-5): the verdict is SUBMITTED — the Run's finally
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 ): "rerun" (the human says the side effect did NOT happen — the
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
- // 第四轮: a poisoned session may not mutate the log.
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
- // 七: a verdict already passed to a live resolver is FINAL — the
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
- // 七: with a LIVE resolver, the active loop / recovery generator
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. 第四轮(对抗): the verdict is recorded so an
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
- // 第五轮(P1-5): submitted — flushed to disk by the Run's finally
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
- // 七: OFFLINE verdict — no live resolver: persist directly.
346
- // 四: the verdict is attributed to the ORIGINAL run of the execution
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
- // 四: the fill is keyed by THIS execution — a tool_result belonging to
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
- // (对抗): the fill also carries the tags from the durable RECEIPT —
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
- /** 第四轮(对抗): a verdict the human already gave for a live decision. */
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
- /** 第四轮(对抗): a verdict the human already gave for a live execution. */
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
- * 第五轮(P1-5): flush every verdict submitted to a live resolver that is
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 (第四轮) is an EXCLUSIVE KERNEL flock on `<id>.lock`, held by a
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). 第五轮(P1-4): this guard
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 round 1): a partial final line is the only
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 (第四轮): an EXCLUSIVE kernel flock held
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 (第五轮 P1-4): the documented upgrade contract is quarantine —
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
- /** 第五轮(P1-2): true only while the helper process is alive. */
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 (第四轮) is an EXCLUSIVE KERNEL flock on `<id>.lock`, held by a
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). 第五轮(P1-4): this guard
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 round 1): a partial final line is the only
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
- /** 第四轮(对抗): serialize concurrent acquireLock calls ON this instance —
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
- /** 第五轮(P1-1): serialize the WHOLE append critical section per session on
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 (第四轮): an EXCLUSIVE kernel flock held
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 (第五轮 P1-4): the documented upgrade contract is quarantine —
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
- // 第五轮(P1-2): the lock is held only while the helper PROCESS is
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
- /** 第五轮(P1-2): true only while the helper process is alive. */
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
- // (第四轮: the file is advisory; the flock is the authority).
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
- // 第五轮(P1-2): the helper's death removes the entry — the
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
- // 第四轮(对抗): the helper could not start (python3 missing) —
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 (第四轮: never a
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
- // 第五轮(P1-3): a close() that landed while we waited ends the
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
- // 第四轮(对抗): the identity is cleared BEFORE the helper dies — a
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
- // 第五轮(P1-1): the WHOLE critical section is serialized per session
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
- // 第五轮(P1-3): close() may have returned while we waited — the
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 ): a stale preloaded handle cannot write a duplicate seq.
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
- // 第五轮(P1-3): a close() that landed during the write must not
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
- // 二: a line WITHOUT a trailing newline is NOT committed — whether
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 (第四轮). Formats:
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
- // 第五轮(P2-1): a spawn failure (python3 missing, exec denied) is
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.
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The /compact summary layer (ADR-0044) — the MODEL-GENERATED half of
3
- * context economy. 归位式抽取 (0.1.26 gate 裁决): this OFF-LOOP
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
@@ -50,6 +50,14 @@ export declare function lastSummaryPoint(events: readonly Event[]): number;
50
50
  * a turn boundary by construction, so the projection's skip never splits
51
51
  * a message. Returns undefined when fewer than keepRounds+1 uncovered
52
52
  * rounds exist (nothing worth covering yet).
53
+ *
54
+ * ⑥ (todo round): a tool result tagged do-not-compact is DURABLE work
55
+ * memory (the todo_set echo) — the summary must never cover its round,
56
+ * or the model loses the current list. When the base boundary would
57
+ * cover such a result, the boundary pulls back to just before the round
58
+ * containing the LATEST one (still a turn boundary). A protected round
59
+ * as the FIRST uncovered round leaves nothing before it to cover →
60
+ * undefined (an honest "nothing to compact").
53
61
  */
54
62
  export declare function summaryBoundarySeq(events: readonly Event[], keepRounds?: number): number | undefined;
55
63
  /**
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. 归位式抽取 (0.1.26 gate 裁决): this OFF-LOOP
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
@@ -13,7 +13,7 @@
13
13
  * unchanged ("nothing happened"). Only the generated `summarized` event
14
14
  * lands on disk; the original events stay there forever.
15
15
  */
16
- import { estimateTokens } from "@vincemakes/kiso-core";
16
+ import { estimateTokens, DO_NOT_COMPACT } from "@vincemakes/kiso-core";
17
17
  /** K (ADR-0044): the recent ROUNDS kept intact by /compact — a constant,
18
18
  * not a knob. The covered range ends just before the K-th most recent
19
19
  * round, so the model still reasons over the recent conversation. */
@@ -84,6 +84,14 @@ export function lastSummaryPoint(events) {
84
84
  * a turn boundary by construction, so the projection's skip never splits
85
85
  * a message. Returns undefined when fewer than keepRounds+1 uncovered
86
86
  * rounds exist (nothing worth covering yet).
87
+ *
88
+ * ⑥ (todo round): a tool result tagged do-not-compact is DURABLE work
89
+ * memory (the todo_set echo) — the summary must never cover its round,
90
+ * or the model loses the current list. When the base boundary would
91
+ * cover such a result, the boundary pulls back to just before the round
92
+ * containing the LATEST one (still a turn boundary). A protected round
93
+ * as the FIRST uncovered round leaves nothing before it to cover →
94
+ * undefined (an honest "nothing to compact").
87
95
  */
88
96
  export function summaryBoundarySeq(events, keepRounds = KEEP_RECENT_ROUNDS) {
89
97
  const prevPoint = lastSummaryPoint(events);
@@ -94,9 +102,48 @@ export function summaryBoundarySeq(events, keepRounds = KEEP_RECENT_ROUNDS) {
94
102
  }
95
103
  if (uncoveredInputs.length <= keepRounds)
96
104
  return undefined;
105
+ const base = uncoveredInputs[uncoveredInputs.length - keepRounds] - 1;
106
+ const protectedRound = latestProtectedBoundary(events, prevPoint, base);
107
+ if (protectedRound !== undefined) {
108
+ if (protectedRound <= prevPoint)
109
+ return undefined;
110
+ return protectedRound;
111
+ }
97
112
  // The input at m - keepRounds opens the FIRST KEPT round; everything
98
113
  // before it (m - keepRounds ≥ 1 covered rounds) is summarizable.
99
- return uncoveredInputs[uncoveredInputs.length - keepRounds] - 1;
114
+ return base;
115
+ }
116
+ /**
117
+ * ⑥: the boundary just before the round holding the LATEST do-not-compact
118
+ * tool result inside (prevPoint, base] — that round's opening user_input
119
+ * minus one, or undefined when the range holds no such result. The
120
+ * projection replaces by RANGE, so only the newest echo matters: older
121
+ * tagged echoes are superseded and may be covered.
122
+ */
123
+ function latestProtectedBoundary(events, prevPoint, base) {
124
+ let protectSeq = -1;
125
+ for (const ev of events) {
126
+ if (ev.type === "tool_result" &&
127
+ ev.seq > prevPoint &&
128
+ ev.seq <= base &&
129
+ (ev.tags ?? []).includes(DO_NOT_COMPACT)) {
130
+ protectSeq = ev.seq;
131
+ }
132
+ }
133
+ if (protectSeq < 0)
134
+ return undefined;
135
+ // The round's opening input: the last user_input before the result.
136
+ // The result's whole round is uncovered by construction (the previous
137
+ // compact ended at a turn boundary before its input), so the input is
138
+ // > prevPoint — the guard is the belt.
139
+ let inputSeq = -1;
140
+ for (const ev of events) {
141
+ if (ev.type === "user_input" && ev.seq > prevPoint && ev.seq < protectSeq)
142
+ inputSeq = ev.seq;
143
+ }
144
+ if (inputSeq < 0)
145
+ return undefined;
146
+ return inputSeq - 1;
100
147
  }
101
148
  /**
102
149
  * The NoticeCell's number: estimated tokens of the covered content minus
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
- /** 发现#11: KISO_HOME is the ONE root — the store derives from it. */
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
- // 发现#10 (P1): when cwd IS the KISO_HOME parent (the user's home
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
- // 合并轮 B: the project's config.json is an artifact of the trust
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
- /** 发现#11: KISO_HOME is the ONE root — the store derives from it. */
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.28",
3
+ "version": "0.1.30",
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.28"
24
+ "@vincemakes/kiso-core": "0.1.29"
25
25
  },
26
26
  "peerDependencies": {
27
- "@vincemakes/kiso-provider-anthropic": "0.1.28",
28
- "@vincemakes/kiso-provider-openai": "0.1.28"
27
+ "@vincemakes/kiso-provider-anthropic": "0.1.29",
28
+ "@vincemakes/kiso-provider-openai": "0.1.29"
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.28",
39
+ "@vincemakes/kiso-evals": "0.1.29",
40
40
  "@types/node": "^26.1.2",
41
41
  "typescript": "^5.7.2",
42
42
  "vitest": "^3.0.0"