@vincemakes/kiso-runtime 0.39.2 → 0.40.1
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 +20 -1
- package/dist/agent.js +44 -8
- package/dist/compaction-policy.d.ts +82 -0
- package/dist/compaction-policy.js +197 -0
- package/dist/compose.d.ts +5 -0
- package/dist/compose.js +12 -1
- package/dist/context-anchor.d.ts +25 -0
- package/dist/context-anchor.js +46 -0
- package/dist/internal.d.ts +3 -0
- package/dist/internal.js +4 -0
- package/dist/profile.d.ts +25 -6
- package/dist/profile.js +68 -7
- package/dist/run.d.ts +1 -1
- package/dist/run.js +35 -9
- package/dist/session-summary.d.ts +87 -0
- package/dist/session-summary.js +166 -0
- package/dist/session.d.ts +77 -2
- package/dist/session.js +258 -22
- package/dist/store.d.ts +8 -0
- package/dist/store.js +27 -2
- package/dist/summarize.d.ts +65 -7
- package/dist/summarize.js +98 -31
- package/package.json +6 -6
package/dist/profile.js
CHANGED
|
@@ -40,6 +40,7 @@ export function buildProfile(input) {
|
|
|
40
40
|
modelId: input.modelId,
|
|
41
41
|
provider: input.provider,
|
|
42
42
|
profileName: input.profileName ?? null,
|
|
43
|
+
workspace: input.workspace ?? null,
|
|
43
44
|
reasoning: input.reasoning ?? { thinking: "default", effort: "default" },
|
|
44
45
|
systemPromptDigest: sha(input.systemPrompt ?? ""),
|
|
45
46
|
tools,
|
|
@@ -49,10 +50,37 @@ export function buildProfile(input) {
|
|
|
49
50
|
export function profilePath(root, sessionId) {
|
|
50
51
|
return join(root, `${sessionId}.meta.json`);
|
|
51
52
|
}
|
|
53
|
+
/** The sidecar's tenants as they are on disk NOW — the writer of one
|
|
54
|
+
* tenant carries the other's bytes through untouched. An unreadable file
|
|
55
|
+
* yields {}: the writer that follows replaces it (the profile path's own
|
|
56
|
+
* fail-closed read is readProfile, which a writer never bypasses). */
|
|
57
|
+
function currentTenants(root, sessionId) {
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(readFileSync(profilePath(root, sessionId), "utf8"));
|
|
60
|
+
return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
52
66
|
/** Atomic, fail-closed write: a reader sees the previous revision or the
|
|
53
67
|
* new one, never a torn file — and the RENAME itself is made durable by
|
|
54
|
-
* the parent-directory fsync.
|
|
68
|
+
* the parent-directory fsync. 0.40.0 dogfood: the file holds two tenants
|
|
69
|
+
* (`profile`, `summary`); writing one preserves the other. */
|
|
55
70
|
export function writeProfile(root, sessionId, profile) {
|
|
71
|
+
writeTenants(root, sessionId, { ...currentTenants(root, sessionId), profile });
|
|
72
|
+
}
|
|
73
|
+
/** The 0.40.0 dogfood's `summary` tenant — what the session list reads
|
|
74
|
+
* instead of the log. Preserves the `profile` tenant byte for byte. */
|
|
75
|
+
export function writeSummary(root, sessionId, summary) {
|
|
76
|
+
writeTenants(root, sessionId, { ...currentTenants(root, sessionId), summary });
|
|
77
|
+
}
|
|
78
|
+
/** The summary tenant, or null (no sidecar, no summary, or unreadable). */
|
|
79
|
+
export function readSummary(root, sessionId) {
|
|
80
|
+
const t = currentTenants(root, sessionId).summary;
|
|
81
|
+
return t !== undefined && typeof t.updatedAt === "number" ? t : null;
|
|
82
|
+
}
|
|
83
|
+
function writeTenants(root, sessionId, tenants) {
|
|
56
84
|
const path = profilePath(root, sessionId);
|
|
57
85
|
const tmpDir = mkdtempSync(join(root, ".meta-"));
|
|
58
86
|
const tmp = join(tmpDir, "meta.json");
|
|
@@ -61,7 +89,7 @@ export function writeProfile(root, sessionId, profile) {
|
|
|
61
89
|
// because rename preserves it, and rename is the only path by which
|
|
62
90
|
// this file comes into existence. Existing files are not migrated —
|
|
63
91
|
// R6's choice, kept.
|
|
64
|
-
writeFileSync(tmp, `${JSON.stringify(
|
|
92
|
+
writeFileSync(tmp, `${JSON.stringify(tenants, null, "\t")}\n`, { mode: 0o600 });
|
|
65
93
|
const fd = openSync(tmp, "r");
|
|
66
94
|
try {
|
|
67
95
|
fsyncSync(fd);
|
|
@@ -95,6 +123,11 @@ export function readProfile(root, sessionId) {
|
|
|
95
123
|
}
|
|
96
124
|
try {
|
|
97
125
|
const parsed = JSON.parse(raw);
|
|
126
|
+
// 0.40.0 dogfood: a sidecar that holds ONLY the summary tenant (a legacy
|
|
127
|
+
// session the list summarised) has no profile — absent, never corrupt:
|
|
128
|
+
// "corrupt" BLOCKS the session from opening, and nothing is wrong
|
|
129
|
+
if (parsed !== null && typeof parsed === "object" && !("profile" in parsed))
|
|
130
|
+
return { kind: "absent" };
|
|
98
131
|
const p = parsed.profile;
|
|
99
132
|
if (p === undefined ||
|
|
100
133
|
typeof p.revision !== "number" ||
|
|
@@ -105,7 +138,13 @@ export function readProfile(root, sessionId) {
|
|
|
105
138
|
!Array.isArray(p.tools)) {
|
|
106
139
|
return { kind: "corrupt", error: "the profile tenant is missing or malformed" };
|
|
107
140
|
}
|
|
108
|
-
|
|
141
|
+
// 0.40.0: a sidecar written before `workspace` existed (and one whose
|
|
142
|
+
// `profileName` no writer ever filled) reads as null — unknown, never
|
|
143
|
+
// a guess. The reader stays open: no new key is required.
|
|
144
|
+
return {
|
|
145
|
+
kind: "ok",
|
|
146
|
+
profile: { ...p, profileName: typeof p.profileName === "string" ? p.profileName : null, workspace: typeof p.workspace === "string" ? p.workspace : null },
|
|
147
|
+
};
|
|
109
148
|
}
|
|
110
149
|
catch (err) {
|
|
111
150
|
return { kind: "corrupt", error: String(err.message ?? err) };
|
|
@@ -115,8 +154,9 @@ export function readProfile(root, sessionId) {
|
|
|
115
154
|
* never from digest inequality alone: every recorded tool present with
|
|
116
155
|
* identical hashes plus new names = compatible additions (a one-line
|
|
117
156
|
* notice); a missing name, a changed hash, a provider/model divergence,
|
|
118
|
-
* or a system-prompt divergence = MATERIAL (
|
|
119
|
-
*
|
|
157
|
+
* or a system-prompt divergence = MATERIAL (the change is NAMED and
|
|
158
|
+
* recorded as the next revision under the CURRENT configuration — it
|
|
159
|
+
* blocks nothing; a digest mismatch is never presented as restoration). */
|
|
120
160
|
export function assessProfileDrift(recorded, current) {
|
|
121
161
|
const reasons = [];
|
|
122
162
|
const notes = [];
|
|
@@ -125,8 +165,29 @@ export function assessProfileDrift(recorded, current) {
|
|
|
125
165
|
if ((r === null) !== (c === null)) {
|
|
126
166
|
reasons.push(`the recorded binding is ${r === null ? "unscoped" : `${r.providerId}/${r.modelId}`} but the current process serves ${c === null ? "an unscoped adapter" : `${c.providerId}/${c.modelId}`}`);
|
|
127
167
|
}
|
|
128
|
-
else if (r !== null && c !== null
|
|
129
|
-
|
|
168
|
+
else if (r !== null && c !== null) {
|
|
169
|
+
// WHO ANSWERS is an identity, and `providerId` alone is not one:
|
|
170
|
+
// `custom` names a CLASS — two custom endpoints are two different
|
|
171
|
+
// places to spend — so an endpoint (or API flavour) that moved is a
|
|
172
|
+
// changed binding like any other. Found in review of this round.
|
|
173
|
+
if (r.providerId !== c.providerId)
|
|
174
|
+
reasons.push(`the recorded provider is ${r.providerId} but the current process serves ${c.providerId}`);
|
|
175
|
+
if (r.apiId !== c.apiId)
|
|
176
|
+
reasons.push(`the recorded API is ${r.apiId} but the current process serves ${c.apiId}`);
|
|
177
|
+
if ((r.endpoint ?? null) !== (c.endpoint ?? null))
|
|
178
|
+
reasons.push(`the recorded endpoint is ${r.endpoint ?? "(none)"} but the current process serves ${c.endpoint ?? "(none)"}`);
|
|
179
|
+
}
|
|
180
|
+
// THE MODEL TOO — the owner's ruling of 2026-09-21, taken on review: the
|
|
181
|
+
// CONFIGURATION wins, including a model switch inside one provider (`--model`
|
|
182
|
+
// and the config are never silently ignored on a resume). This is compared
|
|
183
|
+
// even when BOTH sides are unscoped, where there is no provider ref to carry
|
|
184
|
+
// it, and it covers the ref's own `modelId` as well (the same fact, stamped
|
|
185
|
+
// by the same builder).
|
|
186
|
+
if (r !== null && c !== null && (r.modelId !== c.modelId || recorded.modelId !== current.modelId)) {
|
|
187
|
+
reasons.push(`the recorded model is ${recorded.modelId} but the current process serves ${current.modelId}`);
|
|
188
|
+
}
|
|
189
|
+
else if (r === null && c === null && recorded.modelId !== current.modelId) {
|
|
190
|
+
reasons.push(`the recorded model is ${recorded.modelId} but the current process serves ${current.modelId} (both bindings are unscoped)`);
|
|
130
191
|
}
|
|
131
192
|
if (recorded.systemPromptDigest !== current.systemPromptDigest) {
|
|
132
193
|
notes.push(`the composed system prompt differs from the recorded one (${recorded.systemPromptDigest.slice(0, 12)}… → ${current.systemPromptDigest.slice(0, 12)}…)`);
|
package/dist/run.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type AgentSession, type SessionConfig } from "./session.js";
|
|
|
13
13
|
export declare class Run implements AsyncIterable<Event> {
|
|
14
14
|
#private;
|
|
15
15
|
runId: string;
|
|
16
|
-
constructor(store: SessionStore, adapter: Adapter, config: SessionConfig, session: AgentSession, input: string | readonly ContentBlock[] | undefined, externalSignal: AbortSignalLike | undefined, resume: boolean, source?: import("@vincemakes/kiso-core").MessageSource);
|
|
16
|
+
constructor(store: SessionStore, adapter: Adapter, config: SessionConfig, session: AgentSession, input: string | readonly ContentBlock[] | undefined, externalSignal: AbortSignalLike | undefined, resume: boolean, source?: import("@vincemakes/kiso-core").MessageSource, via?: import("@vincemakes/kiso-core").UserInputVia);
|
|
17
17
|
/** Cancel the run: propagates to the adapter (SDK) and future executions. */
|
|
18
18
|
abort(): void;
|
|
19
19
|
[Symbol.asyncIterator](): AsyncIterator<Event>;
|
package/dist/run.js
CHANGED
|
@@ -7,7 +7,7 @@ import { denialResult, loop } from "@vincemakes/kiso-core";
|
|
|
7
7
|
import { ABORTED, MergedSignal, abortable, openRunId } from "./recovery.js";
|
|
8
8
|
import { resolveReasoning } from "./provider/metadata.js";
|
|
9
9
|
import { deriveRecoveryPlan, invocationSeqOf } from "./recovery-plan.js";
|
|
10
|
-
import { composeApprovalChain, composeSystemPrompt,
|
|
10
|
+
import { composeApprovalChain, composeSystemPrompt, runBasePrompt } from "./compose.js";
|
|
11
11
|
import { truncationGuard } from "./truncation-guard.js";
|
|
12
12
|
import { DEFAULT_STREAM_IDLE_MS, idleGuard } from "./idle-guard.js";
|
|
13
13
|
import { RequestTracer, traceGuard } from "./trace/guard.js";
|
|
@@ -30,6 +30,7 @@ export class Run {
|
|
|
30
30
|
#input;
|
|
31
31
|
#resume;
|
|
32
32
|
#source;
|
|
33
|
+
#via;
|
|
33
34
|
#abort = new AbortController();
|
|
34
35
|
#externalSignal;
|
|
35
36
|
#decisionIds = [];
|
|
@@ -38,7 +39,10 @@ export class Run {
|
|
|
38
39
|
// TV-1B: durable PROVENANCE for the input (e.g. the verification
|
|
39
40
|
// seed's source:"system") — who produced the line, never a
|
|
40
41
|
// provider-role escalation. Absent = plain user input.
|
|
41
|
-
source
|
|
42
|
+
source,
|
|
43
|
+
// 0.40.0: how a person composed the turn (a skill + the typed line) —
|
|
44
|
+
// recorded for display, never projected into a request.
|
|
45
|
+
via) {
|
|
42
46
|
this.#store = store;
|
|
43
47
|
this.#adapter = adapter;
|
|
44
48
|
this.#config = config;
|
|
@@ -47,6 +51,7 @@ export class Run {
|
|
|
47
51
|
this.#externalSignal = externalSignal;
|
|
48
52
|
this.#resume = resume;
|
|
49
53
|
this.#source = source;
|
|
54
|
+
this.#via = via;
|
|
50
55
|
this.runId = crypto.randomUUID();
|
|
51
56
|
}
|
|
52
57
|
/** Cancel the run: propagates to the adapter (SDK) and future executions. */
|
|
@@ -77,10 +82,7 @@ export class Run {
|
|
|
77
82
|
// session's base prompt and the extension appends: generated
|
|
78
83
|
// machinery never outranks the deliberate extension text (the E2
|
|
79
84
|
// "append lands at the END" contract holds). "" when empty.
|
|
80
|
-
const
|
|
81
|
-
const basePrompt = toolTable === "" ? this.#config.systemPrompt
|
|
82
|
-
: this.#config.systemPrompt === undefined ? toolTable
|
|
83
|
-
: `${this.#config.systemPrompt}\n\n${toolTable}`;
|
|
85
|
+
const basePrompt = runBasePrompt(this.#config.systemPrompt, this.#config.registry);
|
|
84
86
|
// E3 — the ledger's parts: the base as CONFIGURED (what the CLI
|
|
85
87
|
// handed the runtime — the tool table is generated machinery, R3)
|
|
86
88
|
// and the extension appends in load order (R4 attribution). The
|
|
@@ -113,12 +115,30 @@ export class Run {
|
|
|
113
115
|
// extension providing a compaction config supplies it. E6: the
|
|
114
116
|
// contextPolicy override beats the session's own, and its minTurns
|
|
115
117
|
// no-fire guard may omit the config below the floor.
|
|
116
|
-
const microcompact = microcompactFor(this.#config, log.all);
|
|
117
118
|
// E2: the session's own systemPrompt first, then every extension
|
|
118
119
|
// append in LOAD order — deterministic (same extensions → same
|
|
119
120
|
// prompt); no appends → byte-identical to the extension-less run.
|
|
120
121
|
const systemPrompt = composeSystemPrompt(basePrompt, this.#config.extensions ?? []);
|
|
121
122
|
const approvalChain = composeApprovalChain(this.#config.extensions ?? []);
|
|
123
|
+
// ADR-0055 Amendment 1 (A1b): the compaction point the kernel asks
|
|
124
|
+
// before every request — the run's own prefix, so an in-band
|
|
125
|
+
// summary reads it at the cache-hit price.
|
|
126
|
+
// The loop resolves the same setting when it builds each request; a
|
|
127
|
+
// setting that cannot run refuses the run there, before any compaction.
|
|
128
|
+
let wireReasoning;
|
|
129
|
+
try {
|
|
130
|
+
wireReasoning = this.#config.reasoning !== undefined ? resolveReasoningOrThrow(this.#config.model, this.#config.reasoning, this.#config.baseUrl).reasoning : undefined;
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
wireReasoning = undefined;
|
|
134
|
+
}
|
|
135
|
+
const compact = this.#session.compactionPoint({
|
|
136
|
+
...(systemPrompt !== undefined ? { systemPrompt } : {}),
|
|
137
|
+
tools: () => this.#config.registry.snapshot().specs,
|
|
138
|
+
...(wireReasoning !== undefined ? { reasoning: wireReasoning } : {}),
|
|
139
|
+
signal,
|
|
140
|
+
...(this.#config.maxRetries !== undefined ? { maxRetries: this.#config.maxRetries } : {}),
|
|
141
|
+
});
|
|
122
142
|
const loopConfig = () => ({
|
|
123
143
|
// 0.1.40 (R-C item 3): the truncation guard gates the model
|
|
124
144
|
// stream — a truncated turn's tool batch never executes.
|
|
@@ -134,7 +154,7 @@ export class Run {
|
|
|
134
154
|
...(this.#config.maxTurns !== undefined ? { maxTurns: this.#config.maxTurns } : {}),
|
|
135
155
|
...(this.#config.maxTokens !== undefined ? { maxTokens: this.#config.maxTokens } : {}),
|
|
136
156
|
...(this.#config.temperature !== undefined ? { temperature: this.#config.temperature } : {}),
|
|
137
|
-
...(
|
|
157
|
+
...(compact !== undefined ? { compact } : {}),
|
|
138
158
|
...(this.#config.maxRetries !== undefined ? { maxRetries: this.#config.maxRetries } : {}),
|
|
139
159
|
// MG-1 (A5): the kernel stamps committed envelopes with this.
|
|
140
160
|
...(this.#config.continuationScope !== undefined ? { continuationScope: this.#config.continuationScope } : {}),
|
|
@@ -265,8 +285,11 @@ export class Run {
|
|
|
265
285
|
// session. The prompt is also the first event the consumer
|
|
266
286
|
// sees, so what was asked and what happened live in the same
|
|
267
287
|
// stream.
|
|
268
|
-
const inputEvent = log.append({ type: "user_input", content: this.#input, ...(this.#source !== undefined ? { source: this.#source } : {}) });
|
|
288
|
+
const inputEvent = log.append({ type: "user_input", content: this.#input, ...(this.#source !== undefined ? { source: this.#source } : {}), ...(this.#via !== undefined ? { via: this.#via } : {}) });
|
|
269
289
|
await this.#session.persist(this.runId, inputEvent);
|
|
290
|
+
// 0.40.0 dogfood: the list's row says "open" from here — a run
|
|
291
|
+
// killed mid-way reads as interrupted, which is the truth
|
|
292
|
+
this.#session.recordSummary(true);
|
|
270
293
|
yield inputEvent;
|
|
271
294
|
// 2. The loop projects from the session log — multi-turn context
|
|
272
295
|
// is the projection, not a second copy.
|
|
@@ -285,6 +308,9 @@ export class Run {
|
|
|
285
308
|
// the flush itself failed (poisoned session) — the error
|
|
286
309
|
// already poisoned everything; nothing more can be done.
|
|
287
310
|
}
|
|
311
|
+
// 0.40.0 dogfood: the row at the run's end — its terminal, or the
|
|
312
|
+
// pause it stopped at (pending asks counted); best-effort
|
|
313
|
+
this.#session.recordSummary(this.#session.log.all.at(-1)?.type !== "terminal");
|
|
288
314
|
// The run is over (or abandoned): its unanswered approvals must
|
|
289
315
|
// fall back to the direct-persist path, so a late approve() is
|
|
290
316
|
// still durable.
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 0.40.0 dogfood (item 2) — a session's SUMMARY, the one thing the
|
|
3
|
+
* session list reads.
|
|
4
|
+
*
|
|
5
|
+
* The list used to read every session's whole event log, three times, to
|
|
6
|
+
* draw one row each: on the owner's machine that is 118 sessions, 584 MB,
|
|
7
|
+
* 3.58M events, about 8.3 s before the picker appears. The sidecar the
|
|
8
|
+
* session already keeps (`<id>.meta.json`) is read in 2 ms for all 118. So
|
|
9
|
+
* the list reads the sidecar ONLY, and the sidecar carries what the row
|
|
10
|
+
* shows, as its own tenant beside the execution profile:
|
|
11
|
+
*
|
|
12
|
+
* title, turns, last activity, state (open or the terminal's outcome),
|
|
13
|
+
* uncertain executions, pending asks, whether the workspace is unknown,
|
|
14
|
+
* and where the summary came from (a run, or the one-time migration).
|
|
15
|
+
*
|
|
16
|
+
* The runtime writes it at a run's start (state open — a run killed there
|
|
17
|
+
* reads "interrupted" in the list, which is the truth) and at its end. It
|
|
18
|
+
* is OBSERVATION: nothing in recovery, projection or a request reads it
|
|
19
|
+
* (the §6 purity rule), and a failed write never fails a run.
|
|
20
|
+
*
|
|
21
|
+
* `sessionSummary` is a pure function of the durable records, shared by the
|
|
22
|
+
* live writes and the one-time migration so the two can never disagree.
|
|
23
|
+
*/
|
|
24
|
+
import type { Event } from "@vincemakes/kiso-core";
|
|
25
|
+
import { type StoreRecord } from "./store.js";
|
|
26
|
+
export interface SessionSummary {
|
|
27
|
+
/** the session's first substantive prompt (`sessionTitle`); null when unknown */
|
|
28
|
+
readonly title: string | null;
|
|
29
|
+
/** user turns; null when unknown (an unreadable log) */
|
|
30
|
+
readonly turns: number | null;
|
|
31
|
+
/** ms since the epoch — the last record's stamp, or the file's mtime */
|
|
32
|
+
readonly updatedAt: number;
|
|
33
|
+
/** "open" while a run has no terminal event; else the terminal's outcome kind */
|
|
34
|
+
readonly state: string | null;
|
|
35
|
+
readonly uncertain: number;
|
|
36
|
+
readonly asks: number;
|
|
37
|
+
/** the profile records no workspace (a session from before 0.40.0) */
|
|
38
|
+
readonly workspaceUnknown: boolean;
|
|
39
|
+
/** "run" — written by a run; "migration" — the one-time pass over a legacy log */
|
|
40
|
+
readonly source: "run" | "migration";
|
|
41
|
+
}
|
|
42
|
+
/** The summary of a session's events — the core both writers share.
|
|
43
|
+
* `open`, `updatedAt` and `asks` come from whoever knows them: a live
|
|
44
|
+
* session knows its own run and its resolvers; the migration derives
|
|
45
|
+
* them from the records. */
|
|
46
|
+
export declare function summarizeEvents(events: readonly Event[], opts: {
|
|
47
|
+
open: boolean;
|
|
48
|
+
updatedAt: number;
|
|
49
|
+
asks: number;
|
|
50
|
+
workspaceUnknown: boolean;
|
|
51
|
+
source: "run" | "migration";
|
|
52
|
+
}): SessionSummary;
|
|
53
|
+
/** The summary of a session's durable RECORDS — the one-time migration's
|
|
54
|
+
* input (a legacy log read exactly once, never through a live session). */
|
|
55
|
+
export declare function sessionSummary(records: readonly StoreRecord[], opts: {
|
|
56
|
+
workspaceUnknown: boolean;
|
|
57
|
+
source: "run" | "migration";
|
|
58
|
+
fallbackUpdatedAt?: number;
|
|
59
|
+
}): SessionSummary;
|
|
60
|
+
/** The migration's marker: written once every legacy session has a
|
|
61
|
+
* summary. From then on the list never reads a log (lead's ruling A): a
|
|
62
|
+
* session still without a summary shows "no summary". */
|
|
63
|
+
export declare const SUMMARY_MIGRATION_MARKER = ".summaries-migrated";
|
|
64
|
+
export interface SessionListing {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
/** the log file's mtime — the last-activity fallback, never a guess at content */
|
|
67
|
+
readonly mtime: number;
|
|
68
|
+
readonly summary: SessionSummary | null;
|
|
69
|
+
readonly workspace: string | null;
|
|
70
|
+
readonly profileName: string | null;
|
|
71
|
+
}
|
|
72
|
+
/** Every session in `root`, from the DIRECTORY and the sidecars only — no
|
|
73
|
+
* log is opened. The 0.40.0 dogfood's list path. */
|
|
74
|
+
export declare function listSessionSidecars(root: string): SessionListing[];
|
|
75
|
+
/** Whether the one-time migration still has work: the marker is absent. */
|
|
76
|
+
export declare function summaryMigrationPending(root: string): boolean;
|
|
77
|
+
/**
|
|
78
|
+
* The ONE-TIME migration (lead's ruling A). Each session without a summary
|
|
79
|
+
* has its log read exactly once — through the store's plain loader, never a
|
|
80
|
+
* live session, so the profile contract cannot throw it — and its summary
|
|
81
|
+
* written with `source: "migration"`. A log that cannot be read gets what
|
|
82
|
+
* CAN be said (last activity from the file's mtime; title and turns
|
|
83
|
+
* unknown) and never throws the list. Resumable: each summary is written on
|
|
84
|
+
* its own, so a crash mid-way leaves the done ones done; the marker lands
|
|
85
|
+
* only when every session has one.
|
|
86
|
+
*/
|
|
87
|
+
export declare function migrateSummaries(root: string, load: (id: string) => readonly StoreRecord[], onEach?: (done: number, total: number) => void): number;
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The 0.40.0 dogfood (item 2) — a session's SUMMARY, the one thing the
|
|
3
|
+
* session list reads.
|
|
4
|
+
*
|
|
5
|
+
* The list used to read every session's whole event log, three times, to
|
|
6
|
+
* draw one row each: on the owner's machine that is 118 sessions, 584 MB,
|
|
7
|
+
* 3.58M events, about 8.3 s before the picker appears. The sidecar the
|
|
8
|
+
* session already keeps (`<id>.meta.json`) is read in 2 ms for all 118. So
|
|
9
|
+
* the list reads the sidecar ONLY, and the sidecar carries what the row
|
|
10
|
+
* shows, as its own tenant beside the execution profile:
|
|
11
|
+
*
|
|
12
|
+
* title, turns, last activity, state (open or the terminal's outcome),
|
|
13
|
+
* uncertain executions, pending asks, whether the workspace is unknown,
|
|
14
|
+
* and where the summary came from (a run, or the one-time migration).
|
|
15
|
+
*
|
|
16
|
+
* The runtime writes it at a run's start (state open — a run killed there
|
|
17
|
+
* reads "interrupted" in the list, which is the truth) and at its end. It
|
|
18
|
+
* is OBSERVATION: nothing in recovery, projection or a request reads it
|
|
19
|
+
* (the §6 purity rule), and a failed write never fails a run.
|
|
20
|
+
*
|
|
21
|
+
* `sessionSummary` is a pure function of the durable records, shared by the
|
|
22
|
+
* live writes and the one-time migration so the two can never disagree.
|
|
23
|
+
*/
|
|
24
|
+
import { existsSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { readProfile, readSummary, writeSummary } from "./profile.js";
|
|
27
|
+
import { executionLedger } from "./ledger.js";
|
|
28
|
+
import { openRunId } from "./recovery.js";
|
|
29
|
+
import { sessionTitle } from "./store.js";
|
|
30
|
+
/** Pending asks from the records alone: a permission request of the OPEN
|
|
31
|
+
* run with no decision and no expiry — the session's own rule, restated
|
|
32
|
+
* over records so the migration needs no live session. */
|
|
33
|
+
function pendingAsks(records) {
|
|
34
|
+
const open = openRunId(records);
|
|
35
|
+
if (open === undefined)
|
|
36
|
+
return 0;
|
|
37
|
+
const decided = new Set();
|
|
38
|
+
const requested = new Set();
|
|
39
|
+
for (const r of records) {
|
|
40
|
+
const e = r.event;
|
|
41
|
+
if (e.type === "permission_requested" && r.runId === open && typeof e.decisionId === "string")
|
|
42
|
+
requested.add(e.decisionId);
|
|
43
|
+
else if ((e.type === "permission_decided" || e.type === "permission_expired") && typeof e.decisionId === "string")
|
|
44
|
+
decided.add(e.decisionId);
|
|
45
|
+
}
|
|
46
|
+
let n = 0;
|
|
47
|
+
for (const id of requested)
|
|
48
|
+
if (!decided.has(id))
|
|
49
|
+
n += 1;
|
|
50
|
+
return n;
|
|
51
|
+
}
|
|
52
|
+
/** The summary of a session's events — the core both writers share.
|
|
53
|
+
* `open`, `updatedAt` and `asks` come from whoever knows them: a live
|
|
54
|
+
* session knows its own run and its resolvers; the migration derives
|
|
55
|
+
* them from the records. */
|
|
56
|
+
export function summarizeEvents(events, opts) {
|
|
57
|
+
let state = opts.open ? "open" : null;
|
|
58
|
+
if (!opts.open) {
|
|
59
|
+
for (let i = events.length - 1; i >= 0; i -= 1) {
|
|
60
|
+
const e = events[i];
|
|
61
|
+
if (e.type === "terminal") {
|
|
62
|
+
state = e.outcome.kind;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const title = sessionTitle(events.map((event) => ({ runId: "", ts: 0, event })));
|
|
68
|
+
return {
|
|
69
|
+
title: title === "(no prompt)" ? null : title,
|
|
70
|
+
turns: events.filter((e) => e.type === "user_input").length,
|
|
71
|
+
updatedAt: opts.updatedAt,
|
|
72
|
+
state,
|
|
73
|
+
uncertain: [...executionLedger(events).values()].filter((x) => x.status === "uncertain").length,
|
|
74
|
+
asks: opts.asks,
|
|
75
|
+
workspaceUnknown: opts.workspaceUnknown,
|
|
76
|
+
source: opts.source,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/** The summary of a session's durable RECORDS — the one-time migration's
|
|
80
|
+
* input (a legacy log read exactly once, never through a live session). */
|
|
81
|
+
export function sessionSummary(records, opts) {
|
|
82
|
+
return summarizeEvents(records.map((r) => r.event), {
|
|
83
|
+
open: openRunId(records) !== undefined,
|
|
84
|
+
updatedAt: records.at(-1)?.ts ?? opts.fallbackUpdatedAt ?? 0,
|
|
85
|
+
asks: pendingAsks(records),
|
|
86
|
+
workspaceUnknown: opts.workspaceUnknown,
|
|
87
|
+
source: opts.source,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// ── the list's read path, and the one-time migration ───────────────────
|
|
91
|
+
/** The migration's marker: written once every legacy session has a
|
|
92
|
+
* summary. From then on the list never reads a log (lead's ruling A): a
|
|
93
|
+
* session still without a summary shows "no summary". */
|
|
94
|
+
export const SUMMARY_MIGRATION_MARKER = ".summaries-migrated";
|
|
95
|
+
/** Every session in `root`, from the DIRECTORY and the sidecars only — no
|
|
96
|
+
* log is opened. The 0.40.0 dogfood's list path. */
|
|
97
|
+
export function listSessionSidecars(root) {
|
|
98
|
+
let entries;
|
|
99
|
+
try {
|
|
100
|
+
entries = readdirSync(root);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
const out = [];
|
|
106
|
+
for (const entry of entries.sort()) {
|
|
107
|
+
if (!entry.endsWith(".jsonl"))
|
|
108
|
+
continue;
|
|
109
|
+
const id = entry.slice(0, -".jsonl".length);
|
|
110
|
+
let mtime = 0;
|
|
111
|
+
try {
|
|
112
|
+
const st = statSync(join(root, entry));
|
|
113
|
+
if (st.size === 0)
|
|
114
|
+
continue; // an empty log is no session (the old list skipped it too)
|
|
115
|
+
mtime = st.mtimeMs;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const profile = readProfile(root, id);
|
|
121
|
+
out.push({
|
|
122
|
+
id,
|
|
123
|
+
mtime,
|
|
124
|
+
summary: readSummary(root, id),
|
|
125
|
+
workspace: profile.kind === "ok" ? profile.profile.workspace : null,
|
|
126
|
+
profileName: profile.kind === "ok" ? profile.profile.profileName : null,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return out;
|
|
130
|
+
}
|
|
131
|
+
/** Whether the one-time migration still has work: the marker is absent. */
|
|
132
|
+
export function summaryMigrationPending(root) {
|
|
133
|
+
return !existsSync(join(root, SUMMARY_MIGRATION_MARKER));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* The ONE-TIME migration (lead's ruling A). Each session without a summary
|
|
137
|
+
* has its log read exactly once — through the store's plain loader, never a
|
|
138
|
+
* live session, so the profile contract cannot throw it — and its summary
|
|
139
|
+
* written with `source: "migration"`. A log that cannot be read gets what
|
|
140
|
+
* CAN be said (last activity from the file's mtime; title and turns
|
|
141
|
+
* unknown) and never throws the list. Resumable: each summary is written on
|
|
142
|
+
* its own, so a crash mid-way leaves the done ones done; the marker lands
|
|
143
|
+
* only when every session has one.
|
|
144
|
+
*/
|
|
145
|
+
export function migrateSummaries(root, load, onEach) {
|
|
146
|
+
const todo = listSessionSidecars(root).filter((s) => s.summary === null);
|
|
147
|
+
todo.forEach((s, i) => {
|
|
148
|
+
let summary;
|
|
149
|
+
try {
|
|
150
|
+
summary = sessionSummary(load(s.id), { workspaceUnknown: s.workspace === null, source: "migration", fallbackUpdatedAt: s.mtime });
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
summary = { title: null, turns: null, updatedAt: s.mtime, state: null, uncertain: 0, asks: 0, workspaceUnknown: s.workspace === null, source: "migration" };
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
writeSummary(root, s.id, summary);
|
|
157
|
+
}
|
|
158
|
+
catch {
|
|
159
|
+
// a sidecar that cannot be written leaves this session without a
|
|
160
|
+
// summary; the list says so, and the marker still lands below
|
|
161
|
+
}
|
|
162
|
+
onEach?.(i + 1, todo.length);
|
|
163
|
+
});
|
|
164
|
+
writeFileSync(join(root, SUMMARY_MIGRATION_MARKER), `${new Date().toISOString()}\n`);
|
|
165
|
+
return todo.length;
|
|
166
|
+
}
|
package/dist/session.d.ts
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
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
|
*/
|
|
30
|
-
import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExtension, type Message, type PermissionDecision, type Tool } from "@vincemakes/kiso-core";
|
|
30
|
+
import { EventLog, type AbortSignalLike, type Adapter, type Event, type KisoExtension, type Message, type PermissionDecision, type Tool, type ToolSpec, type EventInput } from "@vincemakes/kiso-core";
|
|
31
31
|
import { type TaskAssessment } from "./task-assessment.js";
|
|
32
32
|
import { type ContinuationScope } from "@vincemakes/kiso-core";
|
|
33
33
|
import { type ReasoningSetting } from "./provider/metadata.js";
|
|
@@ -93,10 +93,17 @@ export declare class AgentSession {
|
|
|
93
93
|
#private;
|
|
94
94
|
readonly id: string;
|
|
95
95
|
readonly log: EventLog;
|
|
96
|
+
/** 0.40.0: what this open's drift acknowledgement replaced — the CLI
|
|
97
|
+
* says so once. Null when the session opened without material drift. */
|
|
98
|
+
readonly driftAcknowledgement: import("./agent.js").DriftAcknowledgement | null;
|
|
99
|
+
/** 0.40.0: when the provider last billed this session (epoch ms) — the
|
|
100
|
+
* record's time on load, the write's time after. A provider's prompt
|
|
101
|
+
* cache expires with time, so an old bill means a cold prefix. */
|
|
102
|
+
get lastUsageAt(): number | undefined;
|
|
96
103
|
/** Permanently invalidate the session after a rejected disk write (round 1). */
|
|
97
104
|
poison(reason: string): void;
|
|
98
105
|
ensureHealthy(): void;
|
|
99
|
-
constructor(id: string, log: EventLog, store: SessionStore, adapter: Adapter, config: SessionConfig);
|
|
106
|
+
constructor(id: string, log: EventLog, store: SessionStore, adapter: Adapter, config: SessionConfig, lastUsageAt?: number);
|
|
100
107
|
/** Write-ahead through the store; a rejected write POISONS the session
|
|
101
108
|
* (round 1/round 4): the in-memory log no longer matches the disk — whatever
|
|
102
109
|
* the cause (stale handle, corruption, a live external writer, an I/O
|
|
@@ -107,6 +114,37 @@ export declare class AgentSession {
|
|
|
107
114
|
endRun(run: Run): void;
|
|
108
115
|
/** The conversation so far, as the model sees it. */
|
|
109
116
|
projected(): readonly Message[];
|
|
117
|
+
/** 0.40.0: the context as the last bill measured it, plus what the log
|
|
118
|
+
* appended since — or undefined when no bill describes it any more
|
|
119
|
+
* (context-anchor.ts). `events` defaults to the session's log. */
|
|
120
|
+
contextAnchor(events?: readonly Event[]): number | undefined;
|
|
121
|
+
/** 0.40.0: what the context holds — the anchored figure, or the estimate
|
|
122
|
+
* over the projection when no bill describes it. The thresholds and the
|
|
123
|
+
* ctx row read this, never the bare estimate. */
|
|
124
|
+
contextUsed(): number;
|
|
125
|
+
/**
|
|
126
|
+
* ADR-0055 Amendment 1 (A1b) — the kernel's compaction point for ONE run,
|
|
127
|
+
* or undefined when nothing in-run is armed (the kernel then never asks).
|
|
128
|
+
*
|
|
129
|
+
* With `contextPolicy.tiers`: the §1a figure against the tiers — soft
|
|
130
|
+
* waits for a phase end (A3), hard and emergency fire now, an overflow
|
|
131
|
+
* fires once. The summary is in-band (A2) with ONE fallback to the
|
|
132
|
+
* serialised form; if both fail, an emergency or an overflow may prune
|
|
133
|
+
* once (A4b), and otherwise nothing happens and the run goes on.
|
|
134
|
+
*
|
|
135
|
+
* Without tiers, an EXPLICITLY configured `microcompact` keeps its old
|
|
136
|
+
* standing prune here, for SDK callers — the kernel no longer has one.
|
|
137
|
+
*/
|
|
138
|
+
compactionPoint(run: {
|
|
139
|
+
readonly systemPrompt?: string;
|
|
140
|
+
readonly tools: () => readonly ToolSpec[];
|
|
141
|
+
readonly reasoning?: {
|
|
142
|
+
readonly thinking?: "adaptive" | "enabled" | "disabled";
|
|
143
|
+
readonly effort?: string;
|
|
144
|
+
};
|
|
145
|
+
readonly signal?: AbortSignalLike;
|
|
146
|
+
readonly maxRetries?: number;
|
|
147
|
+
}): ((events: readonly Event[], messages: readonly Message[], why: "request" | "overflow") => Promise<readonly EventInput[]>) | undefined;
|
|
110
148
|
/**
|
|
111
149
|
* merge round B (/model): replace the adapter for SUBSEQUENT runs. The
|
|
112
150
|
* kernel reads the adapter through the loop-config closure at each
|
|
@@ -140,12 +178,18 @@ export declare class AgentSession {
|
|
|
140
178
|
/** XP-1: the reasoning axes travel with the binding too; absent =
|
|
141
179
|
* fresh defaults (a new binding never inherits stale effort). */
|
|
142
180
|
readonly reasoning?: ReasoningSetting;
|
|
181
|
+
/** 0.40.0: the config profile that named this binding; absent = none
|
|
182
|
+
* (a direct provider/model, or an SDK caller) — recorded as null. */
|
|
183
|
+
readonly profileName?: string | null;
|
|
143
184
|
/** CTX-1: the compaction threshold follows the live model's window.
|
|
144
185
|
* Absent KEEPS the current one — a caller that does not know the new
|
|
145
186
|
* model's window must not silently reset the policy to nothing. */
|
|
146
187
|
readonly microcompact?: {
|
|
147
188
|
readonly thresholdTokens: number;
|
|
148
189
|
};
|
|
190
|
+
/** A1b: the new model's window, for the in-run tiers. Absent keeps the
|
|
191
|
+
* current one, as `microcompact` does. */
|
|
192
|
+
readonly contextWindow?: number;
|
|
149
193
|
}): void;
|
|
150
194
|
/**
|
|
151
195
|
* CTX-1: the compaction threshold alone, with NO profile revision.
|
|
@@ -164,6 +208,9 @@ export declare class AgentSession {
|
|
|
164
208
|
* every open.
|
|
165
209
|
*/
|
|
166
210
|
setMicrocompactThreshold(thresholdTokens: number): void;
|
|
211
|
+
/** A1b: the window the in-run tiers are drawn from, for a restored or
|
|
212
|
+
* re-bound session (the CLI's one binding step calls both). */
|
|
213
|
+
setContextWindow(windowTokens: number): void;
|
|
167
214
|
/** E2: the adapter identity (anthropic / openai-compat / openai-responses) — the route
|
|
168
215
|
* key the canonical consumer (CLI usage, the trace block) keys on. The
|
|
169
216
|
* per-run tracer reads the SAME live binding; one source, one
|
|
@@ -190,6 +237,15 @@ export declare class AgentSession {
|
|
|
190
237
|
};
|
|
191
238
|
/** XP-1: the selected reasoning axes (resolution happens per request). */
|
|
192
239
|
get reasoning(): ReasoningSetting;
|
|
240
|
+
/**
|
|
241
|
+
* The 0.40.0 dogfood (item 2): the session list's row, written into the
|
|
242
|
+
* sidecar's `summary` tenant at a run's start (`open`) and at its end.
|
|
243
|
+
* Computed from the IN-MEMORY log — never a re-read of the file — and
|
|
244
|
+
* best-effort: a summary that cannot be written leaves the list saying
|
|
245
|
+
* less, and never fails the run. Observation only: nothing that decides
|
|
246
|
+
* recovery, projection or a request reads it.
|
|
247
|
+
*/
|
|
248
|
+
recordSummary(open: boolean): void;
|
|
193
249
|
/**
|
|
194
250
|
* TUI2-R3v2 ③ — ONE model request that belongs to no run (the
|
|
195
251
|
* safer-options seam, adjudicated 2026-08-18).
|
|
@@ -238,6 +294,7 @@ export declare class AgentSession {
|
|
|
238
294
|
run(input: string | readonly import("@vincemakes/kiso-core").ContentBlock[], options?: {
|
|
239
295
|
signal?: AbortSignalLike;
|
|
240
296
|
source?: import("@vincemakes/kiso-core").MessageSource;
|
|
297
|
+
via?: import("@vincemakes/kiso-core").UserInputVia;
|
|
241
298
|
}): Run;
|
|
242
299
|
/**
|
|
243
300
|
* Continue the interrupted run (Area 2): apply durable decisions,
|
|
@@ -265,6 +322,7 @@ export declare class AgentSession {
|
|
|
265
322
|
drop?: boolean;
|
|
266
323
|
focus?: string;
|
|
267
324
|
manualBudget?: boolean;
|
|
325
|
+
onProgress?: (progress: import("./summarize.js").SummaryProgress) => void;
|
|
268
326
|
}): Promise<SummarizeResult | null>;
|
|
269
327
|
/**
|
|
270
328
|
* E6 — the run-start context policy (candidate A + the crux drop arm).
|
|
@@ -404,6 +462,19 @@ export interface ContextPolicy {
|
|
|
404
462
|
readonly keepResults?: number;
|
|
405
463
|
readonly minTurns?: number;
|
|
406
464
|
};
|
|
465
|
+
/**
|
|
466
|
+
* ADR-0055 Amendment 1 (A1b) — the IN-RUN tiers, drawn from the window:
|
|
467
|
+
* soft `min(0.5·w, 400K)` waits for a phase end, hard `min(0.8·w, 700K)`
|
|
468
|
+
* and emergency `w − reserve` fire at the next request, an overflow once.
|
|
469
|
+
* Present, the kernel asks before every request; the run-start summary
|
|
470
|
+
* and the standing microcompact above do not apply. `isCheck` is the
|
|
471
|
+
* phase detector's rule 1 (default: the runner table).
|
|
472
|
+
*/
|
|
473
|
+
readonly tiers?: {
|
|
474
|
+
readonly windowTokens: number;
|
|
475
|
+
readonly isCheck?: (command: string) => boolean;
|
|
476
|
+
readonly maxFailures?: number;
|
|
477
|
+
};
|
|
407
478
|
}
|
|
408
479
|
export interface SessionConfig {
|
|
409
480
|
readonly model: string;
|
|
@@ -423,6 +494,10 @@ export interface SessionConfig {
|
|
|
423
494
|
readonly reasoning?: import("./provider/metadata.js").ReasoningSetting;
|
|
424
495
|
/** XP-1 internal: a legacy session's deferred revision-1 write. */
|
|
425
496
|
readonly profilePending?: true;
|
|
497
|
+
/** 0.40.0: the config profile name in force — recorded per revision. */
|
|
498
|
+
readonly profileName?: string;
|
|
499
|
+
/** 0.40.0: set when this open acknowledged material drift. */
|
|
500
|
+
readonly driftAcknowledgement?: import("./agent.js").DriftAcknowledgement;
|
|
426
501
|
readonly systemPrompt?: string;
|
|
427
502
|
readonly tools?: readonly Tool<any>[];
|
|
428
503
|
readonly registry: import("@vincemakes/kiso-core").ToolRegistry;
|