@robota-sdk/agent-session 3.0.0-beta.78 → 3.0.0-beta.81

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.
@@ -1,6 +1,76 @@
1
- import { IAIProvider, IContextWindowState, IHistoryEntry, IHookTypeExecutor, ISpinner, ITerminalOutput, IToolSchema, IToolWithEventService, IUserInteraction, Robota, TModelEffort, TPermissionMode, TSessionEndReason, TToolArgs, TUniversalMessage, TUniversalValue } from "@robota-sdk/agent-core";
2
- import { ICompactEvent, TCompactTrigger } from "@robota-sdk/agent-interface-transport";
3
-
1
+ import { IAIProvider, IContextWindowState, IEventService, IHistoryEntry, IHookTypeExecutor, IResponseFormatConfig, IRunTraceContext, ISpinner, ISubprocessTraceEnv, ITerminalOutput, IToolExecutionContext, IToolExecutionResult, IToolResultSpillStore, IToolSchema, IToolWithEventService, IUserInteraction, Robota, TBackgroundPermissionPolicy, TModelEffort, TModelEffortSelection, TPermissionMode, TSessionEndReason, TToolArgs, TToolChoice, TToolParameters, TUniversalMessage, TUniversalValue } from "@robota-sdk/agent-core";
2
+ import { ICompactEvent, IInteractiveSessionRecord, IInteractiveSessionRecord as IInteractiveSessionRecord$1, IInteractiveSessionRecord as ISessionRecord, IInteractiveSessionStore, IInteractiveSessionStore as IInteractiveSessionStore$1, IInteractiveSessionStore as ISessionStore, IPromptHistoryBlock, IPromptHistoryEntry, IPromptHistoryReadOptions, IPromptHistorySource, IPromptHistoryWriter, ISessionListEntry, ISessionRecordDecodeIssue, TCompactTrigger, TPermissionResultValue, TSessionLoadOutcome } from "@robota-sdk/agent-interface-session";
3
+ //#region src/turn-claim.d.ts
4
+ /**
5
+ * Thrown when a turn is started on a session that is already running one. RUNTIME-003.
6
+ *
7
+ * A distinct type rather than a bare `Error`, because the point of giving the session a claim is to
8
+ * let consumers STOP maintaining their own busy flags — and a consumer that has to regex-match an
9
+ * error message to tell "busy, retry later" apart from a provider failure has not been given
10
+ * anything it can act on. Follows `CompactionError`, this package's existing precedent.
11
+ */
12
+ declare class SessionBusyError extends Error {
13
+ /** Always `true`: the caller can run this turn later; nothing about the session is broken. */
14
+ readonly recoverable = true;
15
+ constructor(message: string);
16
+ }
17
+ /**
18
+ * The identity of the turn a session is currently running. RUNTIME-003.
19
+ *
20
+ * A session used to express "is something running?" as a bare `AbortController | null` field that
21
+ * `run()` overwrote on entry. That field was doing three jobs at once — cancellation channel, busy
22
+ * flag, and turn identity — and it could only do them for ONE turn, so a second concurrent `run()`
23
+ * orphaned the first: `abort()` reached only whichever turn held the field, and the first turn to
24
+ * finish cleared it in its `finally`, making `abort()` on the survivor a silent no-op. `isRunning()`
25
+ * read the same field, so it answered about whichever turn happened to own it. That is why consumers
26
+ * of this library grew their own busy flags rather than trusting it.
27
+ *
28
+ * The fix is to give the unit of work an OWNER. A claim is taken synchronously, it belongs to the
29
+ * caller that took it, and ONLY that caller releases it.
30
+ *
31
+ * REFUSAL, not pre-emption. A session is a single conversation: two turns interleaving on it produce
32
+ * a history neither of them wrote, and silently cancelling the first would discard work the caller
33
+ * never asked to abandon. `claim()` throws {@link SessionBusyError}, whose message names the three
34
+ * ways forward.
35
+ */
36
+ declare class TurnClaim {
37
+ private controller;
38
+ /**
39
+ * Take the claim for a new turn, or throw if one is already held.
40
+ *
41
+ * MUST be called before the first `await` in the turn — a check that yields first is not a claim,
42
+ * it is a TOCTOU window, and two callers can pass it in the same tick.
43
+ *
44
+ * @throws {SessionBusyError} if a turn is already running, INCLUDING one that has been aborted but
45
+ * has not finished unwinding. See {@link abort} for why that case is not an exception.
46
+ */
47
+ claim(): AbortController;
48
+ /**
49
+ * Release the claim — but only if `controller` is still the one holding it.
50
+ *
51
+ * The ownership check is what stops the original defect from reappearing in a new shape: a turn
52
+ * that released unconditionally in its `finally` could free a claim a LATER turn already took, and
53
+ * `isRunning()` would then report idle while a turn was in flight.
54
+ */
55
+ release(controller: AbortController): void;
56
+ /**
57
+ * Signal the running turn to stop. Idempotent; a no-op if nothing is running.
58
+ *
59
+ * This does NOT release the claim, and that is deliberate. An earlier version cleared it here, so
60
+ * `isRunning()` answered `false` the instant `abort()` returned — while the aborted turn was still
61
+ * unwinding, still able to write history and finish tool calls. A new `run()` could then claim the
62
+ * session and interleave with it: exactly the two-turns-on-one-session defect RUNTIME-003 is
63
+ * about, just moved behind the abort boundary. Review of the first draft caught it.
64
+ *
65
+ * A turn is not over when it is asked to stop; it is over when it has stopped. The claim is
66
+ * therefore held until the owning turn's `finally` releases it, and until then `isRunning()` says
67
+ * `true` and a further `run()` is refused. Cancel and restart is `abort()`, then AWAIT the turn,
68
+ * then `run()` — which is what every caller in this repo already does.
69
+ */
70
+ abort(): void;
71
+ isRunning(): boolean;
72
+ }
73
+ //#endregion
4
74
  //#region src/context-window-tracker.d.ts
5
75
  /** Auto-compact when context usage reaches this fraction */
6
76
  declare const AUTO_COMPACT_THRESHOLD = 0.835;
@@ -32,17 +102,68 @@ declare class ContextWindowTracker {
32
102
  reset(): void;
33
103
  }
34
104
  //#endregion
35
- //#region src/session-logger.d.ts
105
+ //#region src/auto-mode-gate.d.ts
106
+ /** The call the classifier judges. */
107
+ interface IClassifiedCall {
108
+ readonly toolName: string;
109
+ readonly toolArgs: TToolArgs;
110
+ readonly cwd: string;
111
+ }
112
+ interface IClassifierVerdict {
113
+ readonly decision: 'allow' | 'block';
114
+ /** Short, for the model and the user: which rule, and why. */
115
+ readonly reason: string;
116
+ }
117
+ /**
118
+ * Judges one call. `undefined` means no usable verdict (an error, a refusal, output that does not
119
+ * parse): the call is not run. It counts toward a run of refusals, so a classifier that cannot
120
+ * answer hands the decision to a person instead of refusing every call.
121
+ */
122
+ interface IPermissionClassifier {
123
+ classify(call: IClassifiedCall, signal?: AbortSignal): Promise<IClassifierVerdict | undefined>;
124
+ }
125
+ /** Refusals in a row (blocks or unusable verdicts), and blocks in the session, that pause the mode. */
126
+ declare const CONSECUTIVE_BLOCK_LIMIT = 3;
127
+ declare const TOTAL_BLOCK_LIMIT = 20;
128
+ /** `reason` is for the user (`/permissions`); `message` is what the model is told. */
129
+ type TAutoModeJudgement = {
130
+ readonly kind: 'allow';
131
+ } | {
132
+ readonly kind: 'block';
133
+ readonly reason: string;
134
+ readonly message: string;
135
+ } | {
136
+ readonly kind: 'unusable';
137
+ readonly reason: string;
138
+ readonly message: string;
139
+ };
140
+ declare class AutoModeGate {
141
+ private readonly classifier;
142
+ private consecutive;
143
+ private total;
144
+ private paused;
145
+ /** Calls a person allowed to be retried once after the classifier blocked them. */
146
+ private readonly retries;
147
+ constructor(classifier: IPermissionClassifier);
148
+ /** The mode asks a person until one approves. */
149
+ isPaused(): boolean;
150
+ /** A person approved while paused: the classifier decides again. */
151
+ resume(): void;
152
+ /** Let this exact call through once, without the classifier. */
153
+ grantRetry(toolName: string, toolArgs: TToolArgs): void;
154
+ /** Consume a retry grant for this exact call, if there is one. */
155
+ takeRetry(toolName: string, toolArgs: TToolArgs): boolean;
156
+ judge(call: IClassifiedCall, signal?: AbortSignal): Promise<TAutoModeJudgement>;
157
+ }
158
+ //#endregion
159
+ //#region src/session-log-reference-types.d.ts
36
160
  /**
37
- * Session Logger — pluggable logging interface for session events.
161
+ * Leaf type module for {@link IExternalPayloadReference}.
38
162
  *
39
- * ISessionLogger defines the contract. FileSessionLogger is the default
40
- * implementation that writes JSONL to disk. Consumers can implement their
41
- * own (e.g., remote, database, silent) and inject via Session constructor.
163
+ * Split out of `session-logger.ts` so `session-log-sinks.ts` and `session-log-payload.ts` can
164
+ * depend on this type without importing back from `session-logger.ts`, which previously created
165
+ * import cycles among the three files.
42
166
  */
43
- /** Session log event data — extensible record of event metadata. */
44
- type TSessionLogValue = string | number | boolean | object | null | undefined;
45
- type TSessionLogData = Record<string, TSessionLogValue>;
46
167
  interface IExternalPayloadReference {
47
168
  kind: 'external-payload';
48
169
  encoding: 'json';
@@ -50,10 +171,37 @@ interface IExternalPayloadReference {
50
171
  byteLength: number;
51
172
  relativePath: string;
52
173
  }
174
+ /** Session log event data — extensible record of event metadata. */
175
+ type TSessionLogValue = string | number | boolean | object | null | undefined;
176
+ type TSessionLogData = Record<string, TSessionLogValue>;
53
177
  interface IFileSessionLoggerOptions {
54
178
  externalPayloadThresholdBytes?: number;
55
179
  redactedValue?: string;
56
180
  }
181
+ //#endregion
182
+ //#region src/session-log-sinks.d.ts
183
+ /** Canonical validation and construction for every session-log external-payload reference. */
184
+ declare function createSessionLogExternalPayloadReference(sessionId: string, sha256: string, serialized: string): IExternalPayloadReference;
185
+ /** Workspace-neutral sink for content-addressed external JSON payloads. */
186
+ interface IExternalPayloadSink {
187
+ writeJson(sessionId: string, sha256: string, serialized: string): IExternalPayloadReference;
188
+ }
189
+ /** Workspace-neutral append sink for session-log bytes. */
190
+ interface ISessionLogSink {
191
+ append(sessionId: string, text: string): void;
192
+ readonly externalPayloadSink?: IExternalPayloadSink;
193
+ }
194
+ /** Explicit host-filesystem sink for JSONL logs and their content-addressed sidecars. */
195
+ declare class NodeSessionLogSink implements ISessionLogSink, IExternalPayloadSink {
196
+ private readonly logDirectory;
197
+ readonly externalPayloadSink: IExternalPayloadSink;
198
+ private readonly enabled;
199
+ constructor(logDirectory: string);
200
+ append(sessionId: string, text: string): void;
201
+ writeJson(sessionId: string, sha256: string, serialized: string): IExternalPayloadReference;
202
+ }
203
+ //#endregion
204
+ //#region src/session-logger.d.ts
57
205
  /**
58
206
  * Session logger interface — injected into Session for pluggable logging.
59
207
  *
@@ -63,33 +211,59 @@ interface IFileSessionLoggerOptions {
63
211
  interface ISessionLogger {
64
212
  /** Log a session event with structured data. */
65
213
  log(sessionId: string, event: string, data: TSessionLogData): void;
214
+ /**
215
+ * Write out anything buffered.
216
+ *
217
+ * Optional because an implementation that never buffers has nothing to do. A caller that needs
218
+ * the log to be complete on disk — session end, or a reader about to parse it — calls this.
219
+ */
220
+ flush?(): void;
66
221
  }
67
222
  /**
68
- * File-based session logger — writes JSONL to {logDir}/{sessionId}.jsonl.
223
+ * Sink-driven session logger — writes JSONL through `ISessionLogSink`.
69
224
  *
70
225
  * This is the default implementation used by the CLI.
71
226
  * Each line is a self-contained JSON object with timestamp, sessionId, event, and data.
72
227
  */
73
228
  declare class FileSessionLogger implements ISessionLogger {
74
- private readonly logDir;
229
+ private readonly sink;
75
230
  private readonly options;
76
- constructor(logDir: string, options?: IFileSessionLoggerOptions);
231
+ /** Buffered hot-path lines, per session file. Keyed by session id (CORE-029). */
232
+ private readonly pending;
233
+ private pendingBytes;
234
+ constructor(sink: ISessionLogSink, options?: IFileSessionLoggerOptions);
77
235
  log(sessionId: string, event: string, data: TSessionLogData): void;
236
+ /** Write out every buffered hot-path line. Safe to call when nothing is pending. */
237
+ flush(): void;
238
+ private buffer;
239
+ private write;
240
+ private report;
78
241
  }
79
- /** No-op logger — used when logging is disabled. */
80
242
  declare class SilentSessionLogger implements ISessionLogger {
81
243
  log(): void;
82
244
  }
83
245
  //#endregion
84
246
  //#region src/permission-types.d.ts
247
+ /** The part of a sandbox client the permission gate consults. */
248
+ interface ICommandSandboxApproval {
249
+ /**
250
+ * Whether `toolName` runs `shellCommand` inside the sandbox and the sandbox's settings let it
251
+ * proceed without a prompt. Only a tool the sandbox actually wraps may answer yes.
252
+ */
253
+ autoApproves(toolName: string, shellCommand: string): boolean;
254
+ }
85
255
  /**
86
- * Permission handler result:
256
+ * Permission handler result (issue #2052: the union is OWNED by `agent-interface-session` as
257
+ * `TPermissionResultValue`; this name is the session-layer alias, not a second declaration):
87
258
  * - true: allow this invocation
88
259
  * - false: deny this invocation
89
- * - 'allow-session': allow this invocation and auto-approve this tool for the rest of the session
90
- * - 'allow-project': allow this invocation and persist the approval to .robota/settings.local.json
260
+ * - 'allow-session': allow this invocation and auto-approve the CONSENT SCOPE — the pattern
261
+ * `consentScopeFor` projects from this invocation's argument (issue #2351), e.g. `Bash(git *)` —
262
+ * for the rest of the session
263
+ * - 'allow-project': allow this invocation and persist that same scope pattern to the project's
264
+ * local settings; the storage location is owned by the consuming layer (via `onProjectAllowTool`)
91
265
  */
92
- type TPermissionResult = boolean | 'allow-session' | 'allow-project';
266
+ type TPermissionResult = TPermissionResultValue;
93
267
  /**
94
268
  * Custom permission handler — called when a tool needs user approval.
95
269
  * Returns true to allow, false to deny, or 'allow-session' to remember for the session.
@@ -100,12 +274,34 @@ interface IPermissionEnforcerOptions {
100
274
  cwd: string;
101
275
  getPermissionMode: () => TPermissionMode;
102
276
  config: {
277
+ /** `ask` patterns always ask, in every mode including bypassPermissions (issue #3081). */
103
278
  permissions: {
104
279
  allow: string[];
105
280
  deny: string[];
281
+ ask?: string[];
106
282
  };
107
283
  hooks?: Record<string, unknown>;
108
284
  };
285
+ /**
286
+ * The OS sandbox the shell tools run under, when there is one: whether it confines a command and
287
+ * lets it run without a prompt.
288
+ */
289
+ commandSandbox?: ICommandSandboxApproval;
290
+ /** Where `~` and `$HOME` point for critical-path removal checks. Defaults to the OS home directory. */
291
+ homeDirectory?: string;
292
+ /**
293
+ * ARCH-040 Group C (issue #1934): the rules BEFORE any preset contributed.
294
+ *
295
+ * Supplied by the composition root, never derived here. `config.permissions` already carries the
296
+ * STARTUP preset's patterns, so capturing a base from it on the first live `/preset` would keep
297
+ * the first preset's allowlist through every later switch — the accumulation the replace rule
298
+ * exists to prevent, arriving through the base rather than through the merge. Absent ⇒ no preset
299
+ * contributed, and `config.permissions` is itself the preset-free base.
300
+ */
301
+ presetFreePermissions?: {
302
+ allow: readonly string[];
303
+ deny: readonly string[];
304
+ };
109
305
  terminal: ITerminalOutput;
110
306
  permissionHandler?: TPermissionHandler;
111
307
  promptForApprovalFn?: (terminal: ITerminalOutput, toolName: string, toolArgs: TToolArgs) => Promise<TPermissionResult>;
@@ -125,6 +321,45 @@ interface IPermissionEnforcerOptions {
125
321
  transcriptPath?: string;
126
322
  /** Called when the user selects "allow for project" — persists the tool pattern to project settings. */
127
323
  onProjectAllowTool?: (toolName: string) => void;
324
+ /**
325
+ * CORE-025: a background/subagent task permission policy. It adds a ceiling (checked before bypass),
326
+ * an ask-everything flag and the task's own lists to the one evaluator, so `deny`/`preapproved`/
327
+ * `inherit-allowlist` still bind under a permissive mode. Absent → no policy constraints.
328
+ */
329
+ permissionPolicy?: TBackgroundPermissionPolicy;
330
+ /**
331
+ * CORE-025: the task's OWN declared allow/deny rules (distinct from the parent session's `config.permissions`
332
+ * which `inherit-allowlist` inherits). `preapproved` consults these.
333
+ */
334
+ taskPermissions?: {
335
+ allow?: readonly string[];
336
+ deny?: readonly string[];
337
+ };
338
+ /**
339
+ * Judges, in `auto` mode, the calls the mode would otherwise ask a person about. Absent → the
340
+ * session cannot enter `auto`.
341
+ */
342
+ permissionClassifier?: IPermissionClassifier;
343
+ }
344
+ //#endregion
345
+ //#region src/permission-denial-log.d.ts
346
+ /**
347
+ * Why a call was refused:
348
+ * - `policy` — the gate answered deny: a deny rule, a background ceiling, or plan mode;
349
+ * - `user` — a person was asked and declined, or the turn was cancelled while asking;
350
+ * - `no-approver` — the call needed a person and none was attached;
351
+ * - `classifier` — in `auto` mode, the classifier blocked the call or gave no usable verdict.
352
+ */
353
+ type TPermissionDenialReason = 'policy' | 'user' | 'no-approver' | 'classifier';
354
+ interface IPermissionDenial {
355
+ readonly toolName: string;
356
+ /** The argument the tool's permission profile names (command, path, URL), when it has one. */
357
+ readonly argument?: string;
358
+ readonly reason: TPermissionDenialReason;
359
+ /** The classifier's reason, for a `classifier` denial. */
360
+ readonly detail?: string;
361
+ /** Epoch milliseconds. */
362
+ readonly at: number;
128
363
  }
129
364
  //#endregion
130
365
  //#region src/permission-enforcer.d.ts
@@ -140,30 +375,129 @@ declare class PermissionEnforcer {
140
375
  private readonly onToolExecution?;
141
376
  private readonly hookTypeExecutors?;
142
377
  private readonly transcriptPath?;
378
+ /**
379
+ * Issue #2351: consent is remembered as PATTERNS (`consentScopeFor`), not tool names, and read
380
+ * back through the gate's own matcher — approving one argument does not allow every argument.
381
+ */
143
382
  private readonly sessionAllowedTools;
383
+ /** The configured rules before any preset contributed — see {@link applyPresetToolLists}. */
384
+ private readonly presetFreeRules;
144
385
  private readonly onProjectAllowTool?;
386
+ private readonly permissionPolicy?;
387
+ private readonly taskPermissions?;
388
+ private readonly homeDirectory;
389
+ private readonly resolveInWorkspace;
390
+ private readonly commandSandbox?;
391
+ private readonly denials;
392
+ /** A turn a peer's message started is in progress: the one place the reply to that peer exists. */
393
+ private peerTurn;
394
+ private readonly autoMode?;
145
395
  constructor(options: IPermissionEnforcerOptions);
396
+ /**
397
+ * Start a turn, which a peer's message started when `peerTurn` is true. That decides only whether
398
+ * the reply to the peer exists; every other call is decided exactly as in any turn.
399
+ */
400
+ beginTurn(peerTurn: boolean): void;
401
+ /** End the turn; the reply to a peer is gone until the next peer turn begins. */
402
+ endTurn(): void;
403
+ /** Whether `auto` mode can run here: it needs a classifier to decide for it. */
404
+ hasPermissionClassifier(): boolean;
405
+ /**
406
+ * Let the call behind a classifier denial run once, unjudged, when the model tries it again.
407
+ * Returns the denial, or `undefined` when `index` names no classifier denial.
408
+ */
409
+ allowRetryOfDenial(index: number): IPermissionDenial | undefined;
410
+ /** Every configured pattern, split by the grammar it is held to. */
411
+ private configuredRules;
412
+ /**
413
+ * Whether the model is shown this tool at all. A bare-name deny (`Tool`, `Tool(*)`, a name glob)
414
+ * removes it rather than offering it and refusing every call (issue #3081). Read live, so a
415
+ * `/preset` that denies a tool hides it from the next round.
416
+ */
417
+ isToolVisible(toolName: string): boolean;
418
+ /**
419
+ * Tell the gate each tool's parameter names — the schema is what makes `Tool(name:value)` a
420
+ * parameter rule — then re-check the rules against them, before any turn runs.
421
+ */
422
+ private registerToolParameters;
146
423
  /** Wrap all tools with permission checking */
147
424
  wrapTools(tools: IToolWithEventService[]): IToolWithEventService[];
148
- /** Get tools that have been session-approved (via "Allow always" choice). */
425
+ /** The consent patterns granted this session via "Allow always" — e.g. `Bash(git *)` (issue #2351). */
149
426
  getSessionAllowedTools(): string[];
427
+ /** The calls this session refused, most recent first (issue #3082). */
428
+ getRecentDenials(): readonly IPermissionDenial[];
150
429
  /** Clear all session-scoped allow rules. */
151
430
  clearSessionAllowedTools(): void;
152
431
  /**
153
- * Wrap a tool with permission checking.
154
- * The wrapper intercepts execute() and runs permission evaluation before delegating.
155
- * If denied, returns a tool result indicating the action was blocked.
432
+ * Replace the configured permission rules on a LIVE session (ARCH-040 Group C, issue #1934).
433
+ *
434
+ * The seam is this small because `checkPermission` reads `this.config.permissions` on every call
435
+ * rather than snapshotting it at construction — so the next call sees the new rules and nothing
436
+ * needs re-wiring. Without a seam the startup path could apply a preset's tool lists and the live
437
+ * `/preset` path could not, which is the divergence `scan-preset-projection` exists to measure:
438
+ * one session holding two answers for the same preset depending on WHEN it was chosen.
439
+ *
440
+ * **A call already in flight runs to completion.** `checkPermission` is awaited BEFORE the tool
441
+ * executes, so such a call has already passed its gate, and a gate is a decision at a point in
442
+ * time. There is also no rollback for a partially applied tool — a file already written stays
443
+ * written — so a revocation that cannot undo is a stop, not a denial. Building one would rest on
444
+ * the cancellation path, which RUNTIME-004 records as declared at four layers and honoured at none.
445
+ *
446
+ * A newly applied denial DOES outrank an earlier "always allow": `evaluatePermission` answers
447
+ * `deny` before `promptForApproval` — the only reader of `sessionAllowedTools` — is reached. That
448
+ * is not new behaviour here; it is the existing precedence, and it agrees with the combine rule
449
+ * that a denial is not weakened by a later layer.
156
450
  */
157
- private wrapToolWithPermission;
158
- /** Evaluate permission for a tool call using the current mode and config */
159
- checkPermission(toolName: string, toolArgs: TToolArgs): Promise<boolean>;
451
+ /**
452
+ * The rules the next `checkPermission` will read.
453
+ *
454
+ * Exposed so a case can assert what a live re-application PRODUCED, not merely that the method
455
+ * exists. Review found the first cut composing onto a contaminated base and no test could see it,
456
+ * because nothing could look at the rules.
457
+ */
458
+ currentPermissionRules(): {
459
+ allow: readonly string[];
460
+ deny: readonly string[];
461
+ ask: readonly string[];
462
+ };
463
+ applyPresetToolLists(preset: {
464
+ allowedTools?: readonly string[];
465
+ deniedTools?: readonly string[];
466
+ }): void;
467
+ /** Evaluate permission for a tool call. `signal` — RUNTIME-005; see `decideApproval` for why a
468
+ * cancelled approval denies. */
469
+ checkPermission(toolName: string, toolArgs: TToolArgs, signal?: AbortSignal, interaction?: IToolExecutionContext['permissionInteraction'], hookTraceEnv?: IToolExecutionContext['hookTraceEnv']): Promise<boolean>;
470
+ /**
471
+ * Decide an action that has `toolName`'s effect but does not run through that tool — a command
472
+ * that starts a process, say. It passes what the tool call would: the PreToolUse hooks (so
473
+ * guardrails apply), then the gate's rules, mode, remembered consent and prompt. It never takes
474
+ * the command sandbox's auto-approval, because the action does not run inside that sandbox.
475
+ */
476
+ checkDelegatedToolCall(toolName: string, toolParameters: TToolParameters, signal?: AbortSignal): Promise<boolean>;
477
+ /** {@link checkPermission}, keeping the reason a refusal carries for the model. */
478
+ private decidePermission;
479
+ private decideInAutoMode;
480
+ /**
481
+ * The human-approval path: session-scoped allow list → custom handler → injected approval fn → fail-closed
482
+ * deny. Every `approve` decision comes here, whoever the caller, so every ask fails closed identically
483
+ * when no approver is attached (e.g. a detached background task).
484
+ */
485
+ private promptForApproval;
486
+ /**
487
+ * SELFHOST-009: fire the PermissionDecision hook (informational-only, non-blocking) via the shared
488
+ * `runHooks` path. Fire-and-forget — the result is never awaited or consulted, so it cannot gate the
489
+ * permission outcome. The sole blocking gate remains PreToolUse (`runPreToolHook`).
490
+ */
491
+ private firePermissionDecisionHook;
492
+ /** Whether the OS sandbox confines this shell command and lets it run without a prompt. */
493
+ private sandboxAutoApproves;
160
494
  /** Delegate session event to the injected logger. */
161
495
  private log;
162
496
  }
163
497
  //#endregion
164
498
  //#region src/session-base.d.ts
165
499
  declare abstract class SessionBase {
166
- protected abstract readonly robota: Robota;
500
+ protected abstract readonly agent: Robota;
167
501
  protected abstract readonly permissionEnforcer: PermissionEnforcer;
168
502
  protected abstract readonly contextTracker: ContextWindowTracker;
169
503
  protected abstract permissionMode: TPermissionMode;
@@ -175,10 +509,21 @@ declare abstract class SessionBase {
175
509
  protected abstract model: string;
176
510
  protected abstract systemMessage: string;
177
511
  protected abstract messageCount: number;
178
- protected abstract abortController: AbortController | null;
512
+ /** ARCH-010: the session's execution root — owned here, with the check that it was supplied. */
513
+ protected readonly cwd: string;
514
+ protected constructor(cwd: string);
515
+ /**
516
+ * RUNTIME-003: the turn currently running, and its owner. Was a bare `AbortController | null` that
517
+ * `run()` overwrote, which is why `abort()` and `isRunning()` below could answer about a turn that
518
+ * was not the one in flight. See `turn-claim.ts`.
519
+ */
520
+ protected readonly turnClaim: TurnClaim;
521
+ private readonly permissionModeGuards;
179
522
  getPermissionMode(): TPermissionMode;
180
523
  /** Change the active permission mode — future tool calls will use the new mode. */
181
524
  setPermissionMode(mode: TPermissionMode): void;
525
+ /** Register a synchronous policy check at the single session-mode mutation boundary. */
526
+ addPermissionModeGuard(guard: (next: TPermissionMode) => void): () => void;
182
527
  /** Read the active preset id (PRESET-011 runtime state). */
183
528
  getActivePresetId(): string;
184
529
  /**
@@ -192,6 +537,14 @@ declare abstract class SessionBase {
192
537
  /** Toggle subagent dispatch live. Only effective if the agent runtime was built at assembly. */
193
538
  setParallelSubagentsEnabled(enabled: boolean): void;
194
539
  getSessionId(): string;
540
+ /**
541
+ * The session's execution root (ARCH-010).
542
+ *
543
+ * Readable because a caller that derives something FROM the session — a fork, a subagent, a hook
544
+ * input — must be able to ask which root this session actually runs in. Re-deriving it from
545
+ * `process.cwd()` is how the two silently diverged.
546
+ */
547
+ getCwd(): string;
195
548
  getSystemMessage(): string;
196
549
  /**
197
550
  * Replace the active system message and propagate it so the next provider request carries it.
@@ -211,14 +564,68 @@ declare abstract class SessionBase {
211
564
  */
212
565
  applyModelOptions(options: {
213
566
  model?: string;
214
- effort?: TModelEffort;
567
+ effort?: TModelEffortSelection;
215
568
  temperature?: number;
216
569
  maxOutputTokens?: number;
217
570
  }): Promise<void>;
571
+ /** Read the selection for the next model call; provider default remains `auto`. */
572
+ getModelEffort(): TModelEffortSelection;
573
+ /** Run an operation with a temporary effort override and restore it on every exit path. */
574
+ withScopedModelEffort<T>(effort: TModelEffort, operation: () => Promise<T>): Promise<T>;
575
+ /**
576
+ * Re-apply the agent's identity label to a LIVE session.
577
+ *
578
+ * ARCH-040 (issue #1820): a preset's `agentName` reached the agent only at construction, so
579
+ * starting with a preset set the name while switching to the SAME preset mid-session left the old
580
+ * one — one preset with two answers, decided by when it was chosen.
581
+ *
582
+ * Goes through `updateConfiguration`, the agent's own config seam: the agent's `name` reads THROUGH
583
+ * its config, so writing the config is the whole rename and no copy is left stale.
584
+ */
585
+ applyAgentName(name: string): Promise<void>;
218
586
  getToolSchemas(): IToolSchema[];
219
587
  getMessageCount(): number;
220
588
  /** Get tools that have been session-approved (via "Allow always" choice). */
589
+ /**
590
+ * ARCH-040 Group C (issue #1934): re-apply a preset's tool lists to the live enforcer.
591
+ *
592
+ * The BASE it composes onto is the session's configured rules minus whatever a previous preset
593
+ * contributed — which is why the enforcer keeps the original: an allowlist REPLACES the preset
594
+ * layer's contribution rather than accumulating across successive `/preset` switches, while a
595
+ * denial UNIONS because it must not be weakened by a later layer that forgot to repeat it.
596
+ */
597
+ applyPresetToolLists(preset: {
598
+ allowedTools?: readonly string[];
599
+ deniedTools?: readonly string[];
600
+ }): void;
601
+ /**
602
+ * The rules this session's gate reads right now — settings, preset lists and command auto-allows
603
+ * together — so a subagent inherits what the parent actually enforces (issue #3081). Session-scoped
604
+ * "allow always" consent is not included: it was given for this session's context.
605
+ */
606
+ getPermissionRules(): {
607
+ allow: string[];
608
+ deny: string[];
609
+ ask: string[];
610
+ };
221
611
  getSessionAllowedTools(): string[];
612
+ /**
613
+ * Decide an action that reaches `toolName`'s effect by another route (a command that starts a
614
+ * process), so that route cannot be a way around the tool's own permission: the PreToolUse hooks
615
+ * and guardrails, then the rules, mode, remembered consent and the prompt. The command sandbox's
616
+ * auto-approval never applies, because the action does not run inside that sandbox.
617
+ */
618
+ checkToolPermission(toolName: string, toolParameters: TToolParameters, signal?: AbortSignal): Promise<boolean>;
619
+ /** `auto` mode hands decisions to a classifier, so a session without one cannot enter it. */
620
+ protected requireClassifierFor(mode: TPermissionMode): void;
621
+ /**
622
+ * Let the call behind a classifier denial (by its index in the recent denials) run once when the
623
+ * model tries it again. Returns the denial, or `undefined` when the index names no classifier
624
+ * denial.
625
+ */
626
+ retryPermissionDenial(index: number): IPermissionDenial | undefined;
627
+ /** The calls this session refused, most recent first (issue #3082). */
628
+ getRecentPermissionDenials(): readonly IPermissionDenial[];
222
629
  clearSessionAllowedTools(): void;
223
630
  /** Abort the currently running execution. No-op if nothing is running. */
224
631
  abort(): void;
@@ -235,6 +642,17 @@ declare abstract class SessionBase {
235
642
  outputTokens: number;
236
643
  } | undefined;
237
644
  getModelId(): string;
645
+ /**
646
+ * The tool schemas the model is offered at the next request (CLI-1990).
647
+ *
648
+ * The offered set, not the registered one: a deferred tool that has not been loaded is absent,
649
+ * because it is absent from the request. `/context` reads this to report what the tool schemas
650
+ * actually cost, which is the only surface that makes deferral's saving observable.
651
+ */
652
+ getOfferedToolSchemas(): IToolSchema[];
653
+ /** The provider the session sends its turns to now; a provider switch replaces it. */
654
+ getProvider(): IAIProvider;
655
+ getProviderId(): string;
238
656
  /** Add an event entry to history (not a chat message) */
239
657
  addHistoryEntry(entry: IHistoryEntry): void;
240
658
  /** Inject a message into conversation history without execution (used for session restore). */
@@ -250,94 +668,6 @@ declare abstract class SessionBase {
250
668
  clearHistory(): void;
251
669
  }
252
670
  //#endregion
253
- //#region src/session-store.d.ts
254
- /** A persisted session record */
255
- interface ISessionRecord {
256
- /** Unique session identifier */
257
- id: string;
258
- /** Optional human-readable session name */
259
- name?: string;
260
- /** Working directory when the session was created */
261
- cwd: string;
262
- /** ISO-8601 creation timestamp */
263
- createdAt: string;
264
- /** ISO-8601 last-updated timestamp */
265
- updatedAt: string;
266
- /** Conversation messages (opaque to the store) */
267
- messages: unknown[];
268
- /** Full UI timeline (chat + events) for rendering restoration */
269
- history?: unknown[];
270
- /** Exact system prompt used to create the session. */
271
- systemPrompt?: string;
272
- /** Tool schemas registered for the session. */
273
- toolSchemas?: IToolSchema[];
274
- /** Latest background task snapshots for resume/debugging. */
275
- backgroundTasks?: unknown[];
276
- /** Durable non-streaming background task events for resume/debugging. */
277
- backgroundTaskEvents?: unknown[];
278
- /** Latest background job group snapshots for resume/debugging. */
279
- backgroundJobGroups?: unknown[];
280
- /** Durable background job group events for resume/debugging. */
281
- backgroundJobGroupEvents?: unknown[];
282
- /** Durable skill activation events for resume/debugging. */
283
- skillActivationEvents?: unknown[];
284
- /** Durable automatic memory events for resume/debugging. */
285
- memoryEvents?: unknown[];
286
- /** Memory references used by the latest prompt turn. */
287
- usedMemoryReferences?: unknown[];
288
- /** SDK-owned context reference inventory for resume/debugging. */
289
- contextReferences?: unknown[];
290
- /** Provider sandbox snapshot identifier for workspace hydration on resume. */
291
- sandboxSnapshotId?: string;
292
- }
293
- /** Minimal persistence port consumed by Session. */
294
- interface ISessionStore {
295
- save(session: ISessionRecord): void;
296
- load(id: string): ISessionRecord | undefined;
297
- list(): ISessionRecord[];
298
- delete(id: string): void;
299
- /** Return the absolute file path for a session file, if the store is file-backed. */
300
- getFilePath?(id: string): string;
301
- }
302
- /**
303
- * Persistent session store backed by individual JSON files.
304
- *
305
- * Construct with a custom `baseDir` to redirect storage (useful in tests).
306
- */
307
- declare class SessionStore implements ISessionStore {
308
- private readonly baseDir;
309
- constructor(baseDir?: string);
310
- /** Ensure the storage directory exists */
311
- private ensureDir;
312
- /** Absolute path to a session's JSON file */
313
- private filePath;
314
- /** Return the absolute file path for a session — implements ISessionStore.getFilePath */
315
- getFilePath(id: string): string;
316
- /**
317
- * Persist a session record to disk atomically (CORE-019).
318
- * Creates the storage directory if needed.
319
- *
320
- * Bytes go to a same-directory temp file first, then move into place with rename —
321
- * a crash mid-write can therefore never leave a truncated JSON where the previous
322
- * record used to be. Same-directory is load-bearing: cross-device rename is a copy.
323
- */
324
- save(session: ISessionRecord): void;
325
- /**
326
- * Load a session by its ID.
327
- * Returns `undefined` when the session file does not exist or is corrupt.
328
- */
329
- load(id: string): ISessionRecord | undefined;
330
- /**
331
- * List all persisted sessions, sorted by `updatedAt` descending (most recent first).
332
- */
333
- list(): ISessionRecord[];
334
- /**
335
- * Delete a session by its ID.
336
- * No-ops silently if the session does not exist.
337
- */
338
- delete(id: string): void;
339
- }
340
- //#endregion
341
671
  //#region src/session-types.d.ts
342
672
  /** Options for graceful session shutdown. */
343
673
  interface ISessionShutdownOptions {
@@ -347,20 +677,65 @@ interface ISessionShutdownOptions {
347
677
  interface ISessionOptions {
348
678
  /** Pre-constructed tools to register with the agent */
349
679
  tools: IToolWithEventService[];
680
+ /**
681
+ * Applies to a tool added after construction (`Session.addTools`) the wrappers the assembler
682
+ * applied to `tools`, so a late tool is held to the same safety policy as one present from the
683
+ * start. The permission gate is applied by the session itself either way.
684
+ */
685
+ wrapAddedTools?: (tools: IToolWithEventService[]) => IToolWithEventService[];
350
686
  /** Pre-constructed AI provider */
351
687
  provider: IAIProvider;
352
688
  /** Pre-built system message string */
353
689
  systemMessage: string;
354
690
  /** Terminal I/O for permission prompts */
355
691
  terminal: ITerminalOutput;
692
+ /**
693
+ * The session's execution root. REQUIRED — ARCH-010.
694
+ *
695
+ * This field did not exist. `Session` read `process.cwd()` in its constructor and that ambient
696
+ * value became the session's identity everywhere it matters: every hook input,
697
+ * `CLAUDE_PROJECT_DIR`, `PermissionEnforcer`'s root, and the persisted record. Meanwhile the
698
+ * subagent spawn contract has always declared `cwd` REQUIRED — and the in-process runner passed
699
+ * none, because there was no option to pass it to. A session that cannot be told where it runs
700
+ * silently runs wherever the process happens to be, which for a subagent is not its own workspace.
701
+ *
702
+ * A caller that genuinely means "this process's directory" passes `process.cwd()` where a reader
703
+ * can see the decision.
704
+ */
705
+ cwd: string;
356
706
  /** Permission and hook configuration */
357
707
  permissions?: {
358
708
  allow: string[];
359
709
  deny: string[];
710
+ ask?: string[];
711
+ };
712
+ /**
713
+ * ARCH-040 Group C (issue #1934): the permission rules BEFORE any preset contributed.
714
+ *
715
+ * Passed rather than inferred. `permissions` above already has the startup preset's patterns baked
716
+ * in, so an enforcer that captured its own base from it on the first live `/preset` would keep the
717
+ * FIRST preset's allowlist forever — the accumulation the replace rule exists to prevent, arriving
718
+ * through the base instead of through the merge. Absent ⇒ no preset contributed and the two are
719
+ * the same.
720
+ */
721
+ presetFreePermissions?: {
722
+ allow: readonly string[];
723
+ deny: readonly string[];
360
724
  };
361
725
  hooks?: Record<string, unknown>;
362
726
  /** Initial permission mode */
363
727
  permissionMode?: TPermissionMode;
728
+ /**
729
+ * CORE-025: background/subagent task permission policy. Resolved BEFORE the session-mode gate, so
730
+ * `deny`/`preapproved`/`inherit-allowlist` override even a permissive `permissionMode`. Absent on a normal
731
+ * interactive session (mode gate alone). Set by `createSubagentSession` from the spawn request.
732
+ */
733
+ permissionPolicy?: TBackgroundPermissionPolicy;
734
+ /** CORE-025: the task's OWN allow/deny lists (what `preapproved` consults; distinct from `permissions`). */
735
+ taskPermissions?: {
736
+ allow?: readonly string[];
737
+ deny?: readonly string[];
738
+ };
364
739
  /**
365
740
  * Injected "ask the user" port (CMD-005): forwarded into the agent config so model-invoked tools
366
741
  * (AskUserQuestion) can solicit a structured answer. Absent in headless/automation sessions.
@@ -382,11 +757,15 @@ interface ISessionOptions {
382
757
  /** Maximum number of agentic turns per run() call. Undefined = unlimited. */
383
758
  maxTurns?: number;
384
759
  /** Optional session store for persistence */
385
- sessionStore?: ISessionStore;
760
+ sessionStore?: IInteractiveSessionStore$1;
386
761
  /** Override session ID (used when resuming a session to reuse the original ID) */
387
762
  sessionId?: string;
388
763
  /** Custom permission handler (overrides terminal-based prompts, used by Ink UI) */
389
764
  permissionHandler?: TPermissionHandler;
765
+ /** The OS sandbox the shell tools run under, which may let a confined command skip the prompt. */
766
+ commandSandbox?: ICommandSandboxApproval;
767
+ /** Decides for `auto` mode. Without one the session refuses that mode. */
768
+ permissionClassifier?: IPermissionClassifier;
390
769
  /** Called when the user selects "allow for project" — persists the tool pattern to project settings. */
391
770
  onProjectAllowTool?: (toolName: string) => void;
392
771
  /** Callback for text deltas — enables streaming text to the UI in real-time */
@@ -409,33 +788,112 @@ interface ISessionOptions {
409
788
  onCompact?: (summary: string) => void;
410
789
  /** Callback with structured compaction metadata */
411
790
  onCompactEvent?: (event: ICompactEvent) => void;
412
- /** Instructions to include in the compaction prompt (e.g. from CLAUDE.md) */
791
+ /** Instructions to include in the compaction prompt (e.g. from project context files) */
413
792
  compactInstructions?: string;
793
+ /**
794
+ * Replaces the base instruction template of the compaction summarization prompt
795
+ * (default: `DEFAULT_COMPACTION_PROMPT`, a domain-neutral template). Lets the consuming
796
+ * layer own the compaction prompt wording entirely.
797
+ */
798
+ compactionBasePrompt?: string;
799
+ /**
800
+ * Concrete remediation wording for the core's hard-capacity notice, forwarded to the Robota
801
+ * agent config as `IAgentConfig.contextCapacityHint`. The zero-dependency core emits a
802
+ * product-neutral default; a surface tier that owns a real remediation command (e.g. a
803
+ * `/compact` slash command) injects its own actionable hint here. Absent ⇒ the neutral
804
+ * `DEFAULT_CONTEXT_CAPACITY_HINT` applies.
805
+ */
806
+ contextCapacityHint?: string;
414
807
  /** Override context max tokens (otherwise derived from model name) */
415
808
  contextMaxTokens?: number;
416
809
  /** Auto-compact threshold as a 0-1 fraction. Set false to disable automatic compaction. */
417
810
  autoCompactThreshold?: TAutoCompactThreshold;
418
811
  /** Session logger — injected for pluggable session event logging. */
419
812
  sessionLogger?: ISessionLogger;
813
+ /** Host-projected transcript path for hook compatibility; never inferred from a record store. */
814
+ transcriptPath?: string;
420
815
  /** Additional hook type executors (e.g. prompt, agent) beyond the core defaults. */
421
816
  hookTypeExecutors?: IHookTypeExecutor[];
422
817
  /** Name reported to the Robota agent config. Defaults to 'agent' if not provided. */
423
818
  agentName?: string;
424
- /** Request structured output from the provider for this session. */
425
- responseFormat?: {
426
- type: 'text' | 'json_object';
427
- };
428
819
  /**
429
- * Reasoning-effort dial threaded to the Robota agent config and on to the provider
430
- * request builder. When unset, the framework→provider seam defaults it to `'high'`.
820
+ * Request structured output from the provider for this session. Issue #2056: the agent config's
821
+ * own shape (`json_schema` included), so a schema request reaches the provider intact.
822
+ */
823
+ responseFormat?: IResponseFormatConfig;
824
+ /**
825
+ * Reasoning-effort selection threaded to the Robota agent config and provider boundary.
826
+ * When unset, Core preserves provider-default selection as `auto`.
827
+ */
828
+ effort?: TModelEffortSelection;
829
+ /**
830
+ * ARCH-040: sampling temperature and output cap, threaded to the agent config at CONSTRUCTION.
831
+ *
832
+ * The live `/preset` path has always applied both through `applyModelOptions`, and startup applied
833
+ * neither — so one session held two answers for the same preset depending on WHEN it was chosen,
834
+ * which is what `effort` did before ARCH-013 stage 1 fixed it. `maxOutputTokens` maps to the
835
+ * agent's `maxTokens` channel, the same name `applyModelOptions` already writes.
431
836
  */
432
- effort?: TModelEffort;
837
+ temperature?: number;
838
+ maxOutputTokens?: number;
839
+ }
840
+ /**
841
+ * The per-TURN options a caller may attach to one `Session.run()`.
842
+ *
843
+ * Both are optional and independent, and both are absent on the dominant path, so they are spread
844
+ * as a group rather than tested one at a time at each call site.
845
+ */
846
+ interface ISessionRunOptions {
847
+ /** Cancellation belongs to this submission and remains linked until execution settles. */
848
+ signal?: AbortSignal;
849
+ /**
850
+ * SELFHOST-008 P3: a transient system-role block for THIS turn's provider request only. Never
851
+ * written to the conversation store.
852
+ */
853
+ ephemeralSystemContext?: string;
854
+ /**
855
+ * PEER-007 (issue #1915): display attribution for the stored user message — who drove this turn.
856
+ * Never an authorization input (issue #1809).
857
+ */
858
+ driverId?: string;
859
+ /** Run-scoped model tool directive; 'none' remains in force for this turn only. */
860
+ toolChoice?: TToolChoice;
861
+ /**
862
+ * A message from another agent session started this turn. The reply to that session exists only
863
+ * here, and the provider's hosted tools are withheld because no permission step can decide them;
864
+ * every other call is decided by the session's ordinary permissions.
865
+ */
866
+ peerTurn?: boolean;
867
+ /** Host-owned trusted trace context for this turn's provider calls (agent-core `IRunOptions`). */
868
+ traceContext?: IRunTraceContext;
869
+ }
870
+ interface IProviderCallTraceObservation {
871
+ readonly callId?: string;
872
+ readonly round: number;
873
+ readonly startedAt: string;
874
+ readonly endedAt: string;
875
+ readonly outcome: 'success' | 'failure' | 'interrupted';
876
+ readonly disposition?: 'invoked' | 'cache-hit' | 'preflight-refused';
877
+ readonly providerId?: string;
878
+ readonly modelId?: string;
879
+ readonly usageProvenance?: 'complete' | 'partial' | 'absent';
880
+ readonly promptTokens?: number;
881
+ readonly completionTokens?: number;
882
+ readonly totalTokens?: number;
883
+ /** Present only for an invoked call whose adapter attested one; never fabricated. */
884
+ readonly providerRequestId?: string;
433
885
  }
434
886
  //#endregion
435
887
  //#region src/session.d.ts
436
888
  /** Wraps a Robota agent with project context, permission state, and optional persistence. */
437
889
  declare class Session extends SessionBase {
438
- protected readonly robota: Robota;
890
+ protected readonly agent: Robota;
891
+ /**
892
+ * SELFHOST-004: session-owned observable event bus. Injected into the agent so tools (incl. the
893
+ * `FunctionTool` span-completion emit) publish here; the interactive turn subscribes to it to
894
+ * project per-operation spans onto session history. Exposed read-only via {@link getEventService}.
895
+ */
896
+ protected readonly eventService: IEventService;
439
897
  protected readonly permissionEnforcer: PermissionEnforcer;
440
898
  protected readonly contextTracker: ContextWindowTracker;
441
899
  protected permissionMode: TPermissionMode;
@@ -447,10 +905,8 @@ declare class Session extends SessionBase {
447
905
  protected model: string;
448
906
  protected systemMessage: string;
449
907
  protected messageCount: number;
450
- protected abortController: AbortController | null;
451
908
  private readonly terminal;
452
909
  private readonly sessionStore?;
453
- private readonly cwd;
454
910
  private readonly hooks?;
455
911
  private readonly hookTypeExecutors?;
456
912
  private readonly onTextDeltaCallback?;
@@ -461,13 +917,50 @@ declare class Session extends SessionBase {
461
917
  private readonly sessionLogger?;
462
918
  private readonly maxTurns?;
463
919
  private readonly compactionOrchestrator;
920
+ private readonly runtimeTools;
921
+ private readonly wrapAddedTools;
922
+ /** Tools added while a turn ran, applied when the next one starts. */
923
+ private readonly pendingTools;
924
+ /** The last tool change; the next one waits for it. */
925
+ private toolChange;
926
+ private shuttingDown;
464
927
  private shutdownPromise;
465
928
  /** Stdout collected from SessionStart hooks, injected on first run(). */
466
929
  private sessionStartStdout;
467
930
  /** Absolute path to the session transcript file, if file-backed storage is active. */
468
931
  private readonly transcriptPath;
469
932
  constructor(options: ISessionOptions);
470
- run(message: string, rawInput?: string): Promise<string>;
933
+ /**
934
+ * @param options.ephemeralSystemContext SELFHOST-008 P3 — a transient system-role block included in this
935
+ * turn's model call only, never persisted to history (thin pass-through to agent-core `IRunOptions`).
936
+ * REJECTS with `SessionBusyError` if a turn is in flight — RUNTIME-003; see `turn-claim.ts`.
937
+ */
938
+ run(message: string, rawInput?: string, options?: ISessionRunOptions): Promise<string>;
939
+ /**
940
+ * Make tools available from the next turn on — for a capability that became usable mid-session,
941
+ * such as an MCP server connected after its sign-in. Each goes through the same wrappers and
942
+ * permission gate as a tool present from the start. A tool whose name the session already has, or
943
+ * has queued, is left out rather than replacing the one the conversation has been using.
944
+ *
945
+ * The tool list is part of what a provider caches a prompt by, and a turn's rounds must all see
946
+ * the same list: while a turn runs, the tools wait and are applied when the next turn starts;
947
+ * otherwise they are applied now. Calls are serialized, so two concurrent ones both land.
948
+ * Resolves to the names that will be offered.
949
+ */
950
+ addTools(tools: readonly IToolWithEventService[]): Promise<readonly string[]>;
951
+ /** Runs `change` after every tool change before it, so each reads the list the last one wrote. */
952
+ private serializeToolChange;
953
+ /** Registers the queued tools with the agent. Only ever called inside `serializeToolChange`. */
954
+ private applyPendingTools;
955
+ listRuntimeTools(): Promise<IToolSchema[]>;
956
+ invokeRuntimeTool(name: string, parameters: TToolParameters, options?: {
957
+ signal?: AbortSignal;
958
+ }): Promise<IToolExecutionResult>;
959
+ /**
960
+ * SELFHOST-004: the session-owned observable event bus the agent's tools publish to. The interactive
961
+ * turn subscribes to it to collect span-completion events and project them onto session history.
962
+ */
963
+ getEventService(): IEventService;
471
964
  private log;
472
965
  private persistSessionInternal;
473
966
  /**
@@ -478,10 +971,16 @@ declare class Session extends SessionBase {
478
971
  */
479
972
  shutdown(options?: ISessionShutdownOptions): Promise<void>;
480
973
  swapProvider(newProvider: IAIProvider, model: string): void;
481
- compact(instructions?: string, trigger?: TCompactTrigger): Promise<void>;
974
+ compact(instructions?: string, trigger?: TCompactTrigger, signal?: AbortSignal): Promise<void>;
975
+ /** `hookTraceEnv` reaches PreCompact only for a compaction inside a prompt (see `executeRun`). */
976
+ private compactWith;
482
977
  private buildRunContext;
483
978
  }
484
979
  //#endregion
980
+ //#region src/consent-scope.d.ts
981
+ /** The permission pattern a "don't ask again" answer for this invocation grants. */
982
+ declare function consentScopeFor(toolName: string, toolArgs: TToolArgs): string;
983
+ //#endregion
485
984
  //#region src/compaction-orchestrator.d.ts
486
985
  /**
487
986
  * Thrown when a compaction summary is invalid (non-string or empty provider content).
@@ -491,12 +990,25 @@ declare class Session extends SessionBase {
491
990
  declare class CompactionError extends Error {
492
991
  constructor(message: string);
493
992
  }
993
+ /**
994
+ * Default base template for the compaction summarization prompt — the one model-facing
995
+ * prompt surface this package owns (declared in SPEC § Boundaries). Intentionally
996
+ * domain-neutral: it must not assume a software-development conversation. Replaceable
997
+ * wholesale via {@link ICompactionOptions.basePrompt}.
998
+ */
999
+ declare const DEFAULT_COMPACTION_PROMPT: string;
494
1000
  interface ICompactionOptions {
495
1001
  sessionId: string;
496
1002
  cwd: string;
497
1003
  model: string;
498
1004
  hooks?: Record<string, unknown>;
499
1005
  compactInstructions?: string;
1006
+ /**
1007
+ * Replaces the entire base instruction template of the compaction prompt
1008
+ * (default: {@link DEFAULT_COMPACTION_PROMPT}). Focus instructions and the
1009
+ * formatted conversation are appended after it.
1010
+ */
1011
+ basePrompt?: string;
500
1012
  /** Additional hook type executors (e.g. prompt, agent) beyond the core defaults. */
501
1013
  hookTypeExecutors?: IHookTypeExecutor[];
502
1014
  }
@@ -506,31 +1018,97 @@ declare class CompactionOrchestrator {
506
1018
  private readonly model;
507
1019
  private readonly hooks?;
508
1020
  private readonly compactInstructions?;
1021
+ private readonly basePrompt?;
509
1022
  private readonly hookTypeExecutors?;
510
1023
  constructor(options: ICompactionOptions);
511
1024
  /**
512
1025
  * Run compaction — summarize the conversation to free context space.
513
1026
  * @param provider - The AI provider to use for summarization
514
- * @param history - Current conversation history
1027
+ * @param history - The messages to summarise. Must not be empty: whether there is anything worth
1028
+ * compacting is the caller's judgement, made before it commits to replacing the conversation
1029
+ * (CORE-031).
515
1030
  * @param instructions - Optional focus instructions for the summary
1031
+ * @param signal - The turn's cancellation signal (RUNTIME-004). Checked before the provider call
1032
+ * and again after it: an abort throws rather than returning, so the caller's existing
1033
+ * leave-history-untouched path covers a cancel as well as a failure.
1034
+ * @param hookTraceEnv - The prompt's trace for PreCompact's command hooks, only inside a prompt
516
1035
  * @returns The generated summary string (always a non-empty string)
517
- * @throws {CompactionError} when the provider returns a non-string or empty summary —
518
- * callers must leave the conversation history untouched in that case
1036
+ * @throws {CompactionError} when `history` is empty, or when the provider returns a non-string or
1037
+ * empty summary — callers must leave the conversation history untouched in every such case
519
1038
  */
520
- compact(provider: IAIProvider, history: TUniversalMessage[], instructions?: string): Promise<string>;
1039
+ compact(provider: IAIProvider, history: TUniversalMessage[], instructions?: string, signal?: AbortSignal, trigger?: TCompactTrigger, hookTraceEnv?: ISubprocessTraceEnv): Promise<string>;
521
1040
  /** Build the compaction prompt from conversation history */
522
1041
  private buildCompactionPrompt;
523
1042
  }
524
1043
  //#endregion
1044
+ //#region src/conversation-transcript.d.ts
1045
+ /**
1046
+ * Render each message as one entry, in order. An entry is one line, except an assistant message
1047
+ * with several tool calls, which is one line per call. One entry per message so a caller that must
1048
+ * drop the oldest part of a long conversation can drop whole messages.
1049
+ */
1050
+ declare function formatConversationEntries(history: readonly TUniversalMessage[]): string[];
1051
+ //#endregion
1052
+ //#region src/session-artifact.d.ts
1053
+ /**
1054
+ * TRANS-006: the envelope type and its version constant are the codec's (`session-record-codec/`).
1055
+ * They used to be declared here as `ISessionArtifact` and a local constant, which was a second name
1056
+ * and a second number for one shape — the envelope the codec decodes and the envelope this module
1057
+ * writes were always the same `{ schemaVersion, record }`. Nothing on disk changed when they were
1058
+ * unified, because there was nothing to change.
1059
+ */
1060
+ interface ISerializeSessionArtifactOptions {
1061
+ /**
1062
+ * SHARE-PATH ONLY. An app-supplied, policy-free transform applied to the record before serialization — the app
1063
+ * decides which trust-boundary fields to strip (composing the opt-in `scrubSensitiveKeys`). Omit for the
1064
+ * full-fidelity local round-trip.
1065
+ */
1066
+ redact?: (record: IInteractiveSessionRecord$1) => IInteractiveSessionRecord$1;
1067
+ }
1068
+ /**
1069
+ * Serialize a session record into a portable, versioned artifact. With no `redact`, this is the full-fidelity
1070
+ * round-trip form; with `redact`, the caller's transform is applied first (the share path).
1071
+ */
1072
+ declare function serializeSessionArtifact(record: IInteractiveSessionRecord$1, options?: ISerializeSessionArtifactOptions): string;
1073
+ /**
1074
+ * Parse a session artifact back into an `IInteractiveSessionRecord`, rejecting an artifact whose schema version this build
1075
+ * does not understand (so an incompatible artifact is never silently mis-imported).
1076
+ */
1077
+ declare function deserializeSessionArtifact(bytes: string): IInteractiveSessionRecord$1;
1078
+ //#endregion
1079
+ //#region src/scrub-sensitive.d.ts
1080
+ /**
1081
+ * SELFHOST-014 — the single source of the sensitive-key redaction (SSOT).
1082
+ *
1083
+ * The recursive secret-key scrub used to live privately in `session-logger.ts`, logging-coupled and not exported.
1084
+ * It is extracted here so exactly ONE definition of "which keys are sensitive" exists, consumed by BOTH the file
1085
+ * session logger (persistence-time redaction) and the SELFHOST-014 share-artifact `redact` transform (an opt-in
1086
+ * the app composes). This utility is a pure, mechanism-level key scrub only: it carries NO field/trust-boundary
1087
+ * policy (which of `cwd`/`sandboxSnapshotId`/… to strip is an app decision) and is NEVER forced into the
1088
+ * full-fidelity local round-trip.
1089
+ */
1090
+ /** Values a scrub can walk (mirrors the logger's log-value shape; avoids `any`/`unknown`). */
1091
+ type TScrubbableValue = string | number | boolean | object | null | undefined;
1092
+ /**
1093
+ * Keys whose VALUE is a secret and must be redacted before persistence or sharing:
1094
+ * `apiKey`/`authorization`/`accessToken`/`refreshToken`/`secret`/`password`/`xApiKey` (case/`-`/`_`-insensitive).
1095
+ */
1096
+ declare const SENSITIVE_KEY_PATTERN: RegExp;
1097
+ /** True when a key's value should be redacted. The one predicate both the logger and the artifact scrub use. */
1098
+ declare function isSensitiveKey(key: string): boolean;
1099
+ /**
1100
+ * Deep-copy `value`, replacing any value whose KEY is sensitive with `redactedValue` (default `[REDACTED]`).
1101
+ * Pure — does not mutate the input. Returns the same shape (`T`).
1102
+ */
1103
+ declare function scrubSensitiveKeys<T>(value: T, redactedValue?: string): T;
1104
+ //#endregion
525
1105
  //#region src/session-log-events.d.ts
526
1106
  /**
527
1107
  * INFRA-017: typed contract for session-log event names + replay keys (SSOT).
528
1108
  *
529
- * The `FileSessionLogger` writes JSONL lines `{ timestamp, sessionId, event, ...data }`. The event
530
- * names were previously implicit string literals scattered across the session/execution code. This
531
- * module names them once so the writer, the replay validator (`session-log-validation.ts`), and the
532
- * session-log replay provider (INFRA-017 / TEST-008) share one type-safe schema — without changing
533
- * what is written (it formalizes the existing format, it does not add a new one).
1109
+ * `FileSessionLogger` owns the `{ schemaVersion, timestamp, sessionId, event }` JSONL envelope.
1110
+ * This module owns its version and event vocabulary; `session-log-codec` validates each payload
1111
+ * before replay or completeness checks.
534
1112
  *
535
1113
  * The **replay substrate** is the provider/tool execution layer, keyed deterministically:
536
1114
  * a `provider_request` (executionId + round) is answered by its recorded
@@ -538,20 +1116,42 @@ declare class CompactionOrchestrator {
538
1116
  * (executionId + toolCallId) by its `tool_execution_result`. `validateSessionReplayLogEntries`
539
1117
  * proves a log carries all of these (i.e. is replay-complete).
540
1118
  */
1119
+ /** Supported persisted session-log envelope version. */
1120
+ declare const SESSION_LOG_SCHEMA_VERSION = 1;
541
1121
  /** Canonical session-log event names. */
542
1122
  declare const SESSION_LOG_EVENT: {
543
1123
  readonly sessionInit: "session_init";
544
1124
  readonly sessionShutdown: "session_shutdown";
1125
+ readonly sessionShutdownStepError: "session_shutdown_step_error";
545
1126
  readonly context: "context";
546
1127
  readonly contextCompact: "context_compact";
547
1128
  readonly error: "error";
548
1129
  readonly historyMutation: "history_mutation";
549
1130
  readonly providerRequest: "provider_request";
550
1131
  readonly providerNativeRawPayload: "provider_native_raw_payload";
1132
+ readonly providerStreamRawDelta: "provider_stream_raw_delta";
551
1133
  readonly providerResponseRaw: "provider_response_raw";
552
1134
  readonly providerResponseNormalized: "provider_response_normalized";
1135
+ /**
1136
+ * CORE-043: which transport actually carried a structured-output schema on this request, and
1137
+ * whether the schema had to be stated in the prompt instead. Diagnostic, not replay substrate — a
1138
+ * replay answers a `provider_request` from its recorded response, and this line explains why that
1139
+ * request looked the way it did.
1140
+ */
1141
+ readonly structuredOutputTransport: "structured_output_transport";
1142
+ /**
1143
+ * A request moved to another model because the one it was on failed. Diagnostic, not replay
1144
+ * substrate: the `provider_request` announced for the new model is what a replay answers.
1145
+ */
1146
+ readonly providerFallback: "provider_fallback";
1147
+ readonly assistantMessageCommitted: "assistant_message_committed";
553
1148
  readonly toolExecutionRequest: "tool_execution_request";
554
1149
  readonly toolExecutionResult: "tool_execution_result";
1150
+ readonly toolBatchStarted: "tool_batch_started";
1151
+ readonly toolMessageCommitted: "tool_message_committed";
1152
+ readonly backgroundTaskEvent: "background_task_event";
1153
+ readonly backgroundJobGroupEvent: "background_job_group_event";
1154
+ readonly memoryEvent: "memory_event";
555
1155
  readonly user: "user";
556
1156
  readonly preRun: "pre_run";
557
1157
  readonly textDelta: "text_delta";
@@ -585,9 +1185,174 @@ declare function isSessionLogEvent<TName extends TSessionLogEventName>(line: ISe
585
1185
  event: TName;
586
1186
  };
587
1187
  //#endregion
1188
+ //#region src/session-log-entry-types.d.ts
1189
+ interface ISessionLogEntry extends Record<string, TUniversalValue> {
1190
+ schemaVersion?: number;
1191
+ timestamp: string;
1192
+ sessionId: string;
1193
+ event: string;
1194
+ }
1195
+ //#endregion
1196
+ //#region src/session-log-codec/index.d.ts
1197
+ type TSessionLogDecodeErrorCode = 'INVALID_JSON' | 'INVALID_EVENT' | 'UNSUPPORTED_VERSION';
1198
+ type TDecodedEvent<TName extends TSessionLogEventName> = ISessionLogEntry & {
1199
+ event: TName;
1200
+ };
1201
+ type TDecodedSessionLogEntry = (TDecodedEvent<'provider_response_normalized'> & {
1202
+ response: TUniversalMessage;
1203
+ }) | (TDecodedEvent<'history_mutation'> & {
1204
+ mutation: 'append_message';
1205
+ message: TUniversalMessage;
1206
+ }) | (TDecodedEvent<'assistant_message_committed'> & {
1207
+ message: TUniversalMessage | string;
1208
+ }) | (TDecodedEvent<'tool_message_committed'> & {
1209
+ message: TUniversalMessage;
1210
+ }) | (TDecodedEvent<'background_task_event'> & {
1211
+ backgroundEvent?: object;
1212
+ data?: object;
1213
+ }) | (TDecodedEvent<'background_job_group_event'> & {
1214
+ backgroundJobGroupEvent?: object;
1215
+ data?: object;
1216
+ }) | (TDecodedEvent<'memory_event'> & {
1217
+ memoryEvent?: object;
1218
+ data?: object;
1219
+ }) | TDecodedEvent<Exclude<TSessionLogEventName, 'provider_response_normalized' | 'history_mutation' | 'assistant_message_committed' | 'tool_message_committed' | 'background_task_event' | 'background_job_group_event' | 'memory_event'>>;
1220
+ declare class SessionLogDecodeError extends Error {
1221
+ readonly code: TSessionLogDecodeErrorCode;
1222
+ readonly issues: readonly ISessionRecordDecodeIssue[];
1223
+ readonly schemaVersion?: number;
1224
+ constructor(code: TSessionLogDecodeErrorCode, issues: readonly ISessionRecordDecodeIssue[], options?: {
1225
+ schemaVersion?: number;
1226
+ cause?: unknown;
1227
+ });
1228
+ }
1229
+ /** Validate the full input before returning any entry to replay consumers. */
1230
+ declare function decodeSessionLogEntries(entries: unknown, options?: {
1231
+ lineNumbers?: readonly number[];
1232
+ }): TDecodedSessionLogEntry[];
1233
+ //#endregion
1234
+ //#region src/external-payload-source-types.d.ts
1235
+ /**
1236
+ * Leaf type module for {@link IExternalPayloadSource} and {@link ISessionLogSource}.
1237
+ *
1238
+ * Split out of `session-log-sources.ts` so `external-payload-resolution-contracts.ts` can depend
1239
+ * on {@link IExternalPayloadSource} without importing back from `session-log-sources.ts`, which
1240
+ * previously created an import cycle between the two.
1241
+ */
1242
+ /** Workspace-neutral byte source for relative external-payload references. */
1243
+ interface IExternalPayloadSource {
1244
+ readBytes(relativePath: string, maxBytes: number): Uint8Array | undefined;
1245
+ }
1246
+ /** Workspace-neutral source for one session-log document and its optional payload source. */
1247
+ interface ISessionLogSource {
1248
+ readText(): string | undefined;
1249
+ readonly externalPayloadSource?: IExternalPayloadSource;
1250
+ }
1251
+ //#endregion
1252
+ //#region src/session-log-sources.d.ts
1253
+ /** Explicit host-filesystem adapter. A file path is never accepted by the neutral parser itself. */
1254
+ declare class NodeExternalPayloadSource implements IExternalPayloadSource {
1255
+ private readonly baseDirectory;
1256
+ constructor(baseDirectory: string);
1257
+ readBytes(relativePath: string, maxBytes: number): Uint8Array | undefined;
1258
+ }
1259
+ /** Explicit host-filesystem adapter for a JSONL session log. */
1260
+ declare class NodeSessionLogSource implements ISessionLogSource {
1261
+ private readonly logFile;
1262
+ readonly externalPayloadSource: IExternalPayloadSource;
1263
+ constructor(logFile: string);
1264
+ readText(): string | undefined;
1265
+ }
1266
+ //#endregion
1267
+ //#region src/tool-result-spill-store.d.ts
1268
+ type TToolResultSpillErrorCode = 'invalid-options' | 'unsafe-root' | 'write-failed' | 'missing' | 'expired' | 'invalid-reference' | 'read-failed' | 'cleanup-failed' | 'closed';
1269
+ declare class ToolResultSpillError extends Error {
1270
+ readonly code: TToolResultSpillErrorCode;
1271
+ constructor(code: TToolResultSpillErrorCode);
1272
+ }
1273
+ interface INodeToolResultSpillStoreOptions {
1274
+ /** Host-owned parent; defaults to the operating-system temporary directory. */
1275
+ readonly parentDirectory?: string;
1276
+ readonly retentionMs?: number;
1277
+ readonly now?: () => number;
1278
+ /** Receives a fixed, payload-free reason if an idle expiry timer cannot delete a file. */
1279
+ readonly onCleanupFailure?: (reason: 'cleanup-failed') => void;
1280
+ }
1281
+ /** Node host implementation of core's opaque, session-lifetime spill port. */
1282
+ declare class NodeToolResultSpillStore implements IToolResultSpillStore {
1283
+ private readonly directory;
1284
+ private readonly retentionMs;
1285
+ private readonly now;
1286
+ private readonly onCleanupFailure?;
1287
+ private readonly entries;
1288
+ private expiryTimer?;
1289
+ private closed;
1290
+ constructor(options?: INodeToolResultSpillStoreOptions);
1291
+ private assertRoot;
1292
+ private scheduleExpiry;
1293
+ write(content: string): Promise<{
1294
+ readonly reference: string;
1295
+ }>;
1296
+ read(reference: string): Promise<string>;
1297
+ private removeEntry;
1298
+ cleanupExpired(): Promise<void>;
1299
+ shutdown(): Promise<void>;
1300
+ }
1301
+ //#endregion
1302
+ //#region src/prompt-history-file.d.ts
1303
+ /** 64 KiB: a few hundred prompts per block — enough for the first frame, small enough to yield often. */
1304
+ declare const DEFAULT_PROMPT_HISTORY_BLOCK_BYTES: number;
1305
+ interface INodePromptHistoryFileOptions {
1306
+ /** An ancestor of the file the host also owns, tightened along with the directory (SEC-020). */
1307
+ readonly ownedRoot?: string;
1308
+ /** Test seam: the read block size in bytes. */
1309
+ readonly blockBytes?: number;
1310
+ }
1311
+ /** One line → an entry, or `undefined` when the line is not a well-formed entry. */
1312
+ declare function parsePromptHistoryLine(line: string): IPromptHistoryEntry | undefined;
1313
+ declare class NodePromptHistoryFile implements IPromptHistoryWriter, IPromptHistorySource {
1314
+ private readonly path;
1315
+ private readonly blockBytes;
1316
+ private readonly ownedRoot;
1317
+ constructor(path: string, options?: INodePromptHistoryFileOptions);
1318
+ append(entry: IPromptHistoryEntry): void;
1319
+ /**
1320
+ * Newest-first blocks. Only a missing file is the empty state (a fresh install, or history off);
1321
+ * any other open or read failure is thrown so the surface renders it instead of an empty list.
1322
+ */
1323
+ read(options: IPromptHistoryReadOptions): AsyncIterable<IPromptHistoryBlock>;
1324
+ }
1325
+ //#endregion
1326
+ //#region src/external-payload-resolution-contracts.d.ts
1327
+ type TSessionLogPayloadResolutionErrorCode = 'INVALID_LIMIT' | 'INVALID_REFERENCE' | 'UNRESOLVED_REFERENCE' | 'OUTSIDE_ROOT' | 'PAYLOAD_NOT_FOUND' | 'STABLE_PAYLOAD_READ_UNAVAILABLE' | 'PAYLOAD_UNREADABLE' | 'BYTE_LENGTH_MISMATCH' | 'SHA256_MISMATCH' | 'INVALID_JSON' | 'MAX_DEPTH_EXCEEDED' | 'MAX_TOTAL_BYTES_EXCEEDED' | 'CIRCULAR_REFERENCE';
1328
+ interface ISessionLogPayloadResolutionOptions {
1329
+ readonly source?: IExternalPayloadSource;
1330
+ readonly maxDepth?: number;
1331
+ readonly maxTotalBytes?: number;
1332
+ }
1333
+ interface ISessionLogPayloadResolutionErrorMetadata {
1334
+ readonly relativePath?: string;
1335
+ readonly resolvedPath?: string;
1336
+ readonly depth?: number;
1337
+ readonly expected?: string | number;
1338
+ readonly actual?: string | number;
1339
+ }
1340
+ declare class SessionLogPayloadResolutionError extends Error {
1341
+ readonly code: TSessionLogPayloadResolutionErrorCode;
1342
+ readonly metadata: Readonly<ISessionLogPayloadResolutionErrorMetadata>;
1343
+ constructor(code: TSessionLogPayloadResolutionErrorCode, message: string, metadata?: ISessionLogPayloadResolutionErrorMetadata, cause?: unknown);
1344
+ }
1345
+ //#endregion
1346
+ //#region src/external-payload-resolver.d.ts
1347
+ /**
1348
+ * Hydrate every external JSON payload reference in one value using one aggregate budget.
1349
+ * The input is treated as untrusted and the returned graph contains only JSON-compatible values.
1350
+ */
1351
+ declare function resolveSessionLogExternalPayloads(value: unknown, options: ISessionLogPayloadResolutionOptions): unknown;
1352
+ //#endregion
588
1353
  //#region src/session-log-validation.d.ts
589
1354
  interface ISessionReplayValidationIssue {
590
- code: 'PROVIDER_RESPONSE_RAW_MISSING' | 'PROVIDER_NATIVE_RAW_PAYLOAD_MISSING' | 'PROVIDER_RESPONSE_NORMALIZED_MISSING' | 'TOOL_RESULT_MISSING' | 'PAYLOAD_REFERENCE_INVALID';
1355
+ code: 'PROVIDER_RESPONSE_RAW_MISSING' | 'PROVIDER_NATIVE_RAW_PAYLOAD_MISSING' | 'PROVIDER_RESPONSE_NORMALIZED_MISSING' | 'TOOL_RESULT_MISSING' | 'PAYLOAD_REFERENCE_INVALID' | 'UNRESOLVED_REPLAY_PAYLOAD';
591
1356
  message: string;
592
1357
  eventIndex?: number;
593
1358
  executionId?: string;
@@ -601,11 +1366,6 @@ interface ISessionReplayValidationResult {
601
1366
  declare function validateSessionReplayLogEntries(entries: readonly ISessionLogEntry[]): ISessionReplayValidationResult;
602
1367
  //#endregion
603
1368
  //#region src/session-log-replay.d.ts
604
- interface ISessionLogEntry extends Record<string, TUniversalValue> {
605
- timestamp: string;
606
- sessionId: string;
607
- event: string;
608
- }
609
1369
  interface ISessionReplayRecord {
610
1370
  sessionId: string | undefined;
611
1371
  cwd: string | undefined;
@@ -617,8 +1377,244 @@ interface ISessionReplayRecord {
617
1377
  backgroundJobGroupEvents: object[];
618
1378
  memoryEvents: object[];
619
1379
  }
620
- declare function loadSessionLogEntries(logFile: string): ISessionLogEntry[];
1380
+ type ISessionLogLoadOptions = Omit<ISessionLogPayloadResolutionOptions, 'source'> & {
1381
+ readonly externalPayloadSource?: IExternalPayloadSource;
1382
+ };
1383
+ declare function loadSessionLogEntries(source: ISessionLogSource, options?: ISessionLogLoadOptions): ISessionLogEntry[];
621
1384
  declare function replaySessionLogEntries(entries: readonly ISessionLogEntry[]): ISessionReplayRecord;
622
1385
  //#endregion
623
- export { AUTO_COMPACT_THRESHOLD, CompactionError, CompactionOrchestrator, ContextWindowTracker, FileSessionLogger, type IExternalPayloadReference, type IFileSessionLoggerOptions, type IProviderEventKey, type ISessionLogEntry, type ISessionLogLine, type ISessionLogger, type ISessionOptions, type ISessionRecord, type ISessionReplayRecord, type ISessionReplayValidationIssue, type ISessionReplayValidationResult, type ISessionShutdownOptions, type ISessionStore, type ISpinner, type ITerminalOutput, type IToolEventKey, PermissionEnforcer, SESSION_LOG_EVENT, Session, SessionStore, SilentSessionLogger, type TAutoCompactThreshold, type TPermissionHandler, type TPermissionResult, type TSessionLogData, type TSessionLogEventName, type TSessionLogValue, isSessionLogEvent, loadSessionLogEntries, replaySessionLogEntries, validateSessionReplayLogEntries };
1386
+ //#region src/session-id.d.ts
1387
+ /**
1388
+ * Whether `id` is safe to interpolate into a filesystem path as a single component.
1389
+ *
1390
+ * The pattern admits no `/`, no `\` and no `:`, so the value cannot introduce a path separator or a
1391
+ * Windows drive qualifier; and because it must start with an alphanumeric, it can be neither `.` nor
1392
+ * `..`. With no separator available, an embedded `..` cannot form a traversal component.
1393
+ */
1394
+ declare function isSafeSessionId(id: string): boolean;
1395
+ /** Throw unless `id` is safe to use as a path component. */
1396
+ declare function assertSafeSessionId(id: string): void;
1397
+ //#endregion
1398
+ //#region src/session-store.d.ts
1399
+ /**
1400
+ * Persistent session store backed by individual JSON files.
1401
+ *
1402
+ * Construct with a host-owned `baseDir`; framework project composition uses a separate
1403
+ * authority-backed adapter over the same neutral port.
1404
+ */
1405
+ declare class NodeSessionStore implements IInteractiveSessionStore$1 {
1406
+ private readonly baseDir;
1407
+ private readonly ownedRoot;
1408
+ /**
1409
+ * @param baseDir the directory holding the records.
1410
+ * @param ownedRoot an ancestor of `baseDir` the HOST also owns, tightened along with it (SEC-020).
1411
+ * Optional because this adapter does not interpret its base directory as a trusted root and must
1412
+ * not guess that a parent belongs to the product — which of them do is composition's knowledge.
1413
+ * Omitting it leaves a store root an older version created at whatever mode it was given, which
1414
+ * is what review of PR #2224 found: the leaf was 0700 and the directory above it was not.
1415
+ */
1416
+ constructor(baseDir: string, ownedRoot?: string);
1417
+ /**
1418
+ * Ensure the storage directory exists AND that only its owner can enter it (SEC-020).
1419
+ *
1420
+ * The `existsSync` guard this replaces is the whole defect. It skipped the case that matters: a
1421
+ * directory some earlier version, a shared CI checkout, or another local user left at a wider
1422
+ * mode was adopted as ours with no signal. Measured under umask 022 before this change, a fresh
1423
+ * sessions directory came out 0755 and its records 0644 — and a directory pre-created at 0777
1424
+ * stayed 0777.
1425
+ */
1426
+ private ensureDir;
1427
+ /**
1428
+ * Absolute path to a session's JSON file.
1429
+ *
1430
+ * SEC-006: every public method routes through here, so validating the id at this one point covers
1431
+ * `save` (write), `load` (read), and `delete` (unlink) at once.
1432
+ *
1433
+ * Issue #2240: `assertSafeSessionId` is the guard, and it is sound — no separator, no `.`/`..`
1434
+ * can pass it. But it is a regex reject behind a helper, which static analysis does not model as
1435
+ * a path sanitizer, so `js/path-injection` re-opened on `load` every time this function changed
1436
+ * length. The containment check below is the shape such tools DO recognise: the resolved path
1437
+ * must stay inside the resolved base directory. It is unreachable after the assertion and costs
1438
+ * one `resolve`; it exists so the guard is visible where the sink is, not to replace the guard.
1439
+ */
1440
+ private filePath;
1441
+ /**
1442
+ * Persist a session record to disk atomically (CORE-019).
1443
+ * Creates the storage directory if needed.
1444
+ *
1445
+ * Bytes go to a same-directory temp file first, then move into place with rename —
1446
+ * a crash mid-write can therefore never leave a truncated JSON where the previous
1447
+ * record used to be. Same-directory is load-bearing: cross-device rename is a copy.
1448
+ *
1449
+ * SEC-020: the atomic write now comes from `writeOwnerOnlyFile`, which carries the mode from the
1450
+ * moment the temp file is created. The hand-rolled version here wrote it at the umask's default
1451
+ * and let `rename` carry that mode to the final path, so every record was 0644 — and even setting
1452
+ * the mode after the write would leave a window in which the full transcript was world-readable
1453
+ * on disk.
1454
+ */
1455
+ save(session: IInteractiveSessionRecord$1): void;
1456
+ /**
1457
+ * Load a session by its ID, saying WHICH of the four things happened.
1458
+ *
1459
+ * `undefined` used to answer all four — never saved, damaged, written by a build this one cannot
1460
+ * read, and the read failed — and a caller that meant to preserve fields it does not own then
1461
+ * treated "damaged" as "no prior record" and overwrote the file. The outcome type is what removes
1462
+ * that, by making the caller answer the question it was not asking.
1463
+ */
1464
+ load(id: string): TSessionLoadOutcome;
1465
+ /**
1466
+ * Every session this directory holds, each with what the store concluded about it.
1467
+ *
1468
+ * Unreadable entries are REPORTED rather than skipped. A store that distinguishes four outcomes on
1469
+ * `load` and then hides two of them from the surface a person browses has moved the defect rather
1470
+ * than removed it: the difference a user experiences is between "my session vanished" and "my
1471
+ * session needs a different build".
1472
+ */
1473
+ list(): readonly ISessionListEntry[];
1474
+ /**
1475
+ * The outcome for one directory entry, without letting a bad NAME throw out of `list`.
1476
+ *
1477
+ * `load` validates the id, because an id reaching it is a caller's value and a malformed one is a
1478
+ * bug or an attack (SEC-006). A name read out of the directory is neither: the store did not
1479
+ * choose it, and one file it cannot use as an id must not take the whole listing down with it.
1480
+ * Routing `list` through `load` made exactly that happen — a single `my session.json` in the
1481
+ * sessions directory threw, and the resume picker went with it.
1482
+ *
1483
+ * Reporting it is the same answer `list` gives for every other file it cannot read, which is the
1484
+ * property this work exists to establish.
1485
+ */
1486
+ private outcomeForListedId;
1487
+ /**
1488
+ * Delete a session by its ID.
1489
+ * No-ops silently if the session does not exist.
1490
+ */
1491
+ delete(id: string): void;
1492
+ }
1493
+ //#endregion
1494
+ //#region src/checkpoint-tree.d.ts
1495
+ /**
1496
+ * SELFHOST-007 — neutral checkpoint tree (branching time-travel).
1497
+ *
1498
+ * A pure, I/O-free branch-tree over opaque checkpoint-node ids — git-for-a-session with no file I/O,
1499
+ * no persistence, and no retention/prune policy. It lives beside the storage-neutral persistence
1500
+ * primitive (`SessionStore`/`IInteractiveSessionRecord`): same neutral-mechanism class (opaque payloads, no
1501
+ * product policy). The agent-framework checkpoint store consumes it over the existing one-way
1502
+ * `agent-framework → agent-session` edge; the reverse edge would be a cycle and is forbidden.
1503
+ *
1504
+ * Model: each node is `{ id, parentId }` (root has no `parentId`). Adding a checkpoint appends a child
1505
+ * of the active head. Forking moves the active head to a PAST node so the next append DIVERGES — the
1506
+ * original descendants stay reachable (a sibling branch). A "branch" is a leaf (a node with no
1507
+ * children); `switch` moves the active head to any existing node.
1508
+ */
1509
+ interface ICheckpointNode {
1510
+ id: string;
1511
+ /** Parent checkpoint id; absent for the root. */
1512
+ parentId?: string;
1513
+ }
1514
+ declare class CheckpointTree {
1515
+ private readonly nodes;
1516
+ /** Child adjacency (parentId → child ids in insertion order) for O(1) leaf/branch queries. */
1517
+ private readonly children;
1518
+ private activeId;
1519
+ /**
1520
+ * Build a tree from explicit `{ id, parentId }` edges (e.g. reconstructed from persisted checkpoint
1521
+ * manifests). Nodes may arrive in any order — parents are linked by id. The active head is left at
1522
+ * the given `activeId` (or undefined). This is the delegation entry point a consumer store uses to
1523
+ * answer navigation queries (`listBranches`/`ancestors`) without the tree owning any persistence.
1524
+ */
1525
+ static fromNodes(nodes: ICheckpointNode[], activeId?: string): CheckpointTree;
1526
+ /**
1527
+ * Append a checkpoint as a child of the current active head and make it the new active head.
1528
+ * The first append (no active head) becomes the root. Ids must be unique.
1529
+ */
1530
+ addCheckpoint(id: string): void;
1531
+ /**
1532
+ * Fork from a PAST checkpoint: move the active head to `fromId` so the next `addCheckpoint` diverges
1533
+ * into a sibling branch, leaving `fromId`'s original descendants reachable. Returns `fromId`.
1534
+ */
1535
+ fork(fromId: string): string;
1536
+ /** Move the active head to an existing node (typically a branch leaf). */
1537
+ switch(nodeId: string): void;
1538
+ /** The current active head id (undefined for an empty tree). */
1539
+ activeLeaf(): string | undefined;
1540
+ /** All branch tips (leaf nodes with no children), in insertion order. */
1541
+ listBranches(): string[];
1542
+ /**
1543
+ * The chain from `id` up to (and including) the root — nearest first. Empty if `id` is unknown. Only
1544
+ * REGISTERED nodes are included: a dangling `parentId` (edge to a node not in the tree — possible on
1545
+ * manifest drift/corruption) terminates the walk rather than emitting a phantom id.
1546
+ */
1547
+ ancestors(id: string): string[];
1548
+ /** Whether a checkpoint id exists in the tree. */
1549
+ has(id: string): boolean;
1550
+ /** The number of checkpoints in the tree. */
1551
+ get size(): number;
1552
+ }
1553
+ //#endregion
1554
+ //#region src/session-record-codec/decode-outcome.d.ts
1555
+ /** What a decode of a persisted session record can conclude. */
1556
+ type TSessionRecordDecodeOutcome = {
1557
+ readonly status: 'valid';
1558
+ readonly record: IInteractiveSessionRecord$1;
1559
+ } | {
1560
+ readonly status: 'corrupt';
1561
+ readonly issues: readonly ISessionRecordDecodeIssue[];
1562
+ } | {
1563
+ readonly status: 'unsupported';
1564
+ readonly schemaVersion: number | undefined;
1565
+ };
1566
+ //#endregion
1567
+ //#region src/session-record-codec/record-decoder.d.ts
1568
+ /**
1569
+ * The version of the persisted record envelope this build reads and writes.
1570
+ *
1571
+ * Bump it when the shape changes in a way an older reader would decode WRONGLY rather than not at
1572
+ * all. A reader that meets a version it does not implement reports `unsupported` and stops — it does
1573
+ * not decode the members it recognises, because a partially decoded session is the silent
1574
+ * field-loss this codec replaces.
1575
+ *
1576
+ * ONE concept, not two (issue #2185): the envelope `{ schemaVersion, record }` and the record it
1577
+ * wraps version together — a change to either shape bumps this number, and every consumer of the
1578
+ * envelope reads it: the portable session artifact (`serializeSessionArtifact`) and the session
1579
+ * store (`NodeSessionStore.save`, `WorkspaceSessionStore`). Two constants would let an envelope-only
1580
+ * change reject records that are fine, and the two shapes have never moved apart. The constant is
1581
+ * therefore named for the pair it versions, not for its first consumer: TRANS-006 kept the
1582
+ * incumbent `SESSION_ARTIFACT_SCHEMA_VERSION` (published, written by the producing path) over the
1583
+ * duplicate TRANS-005 introduced, and #2185 renamed it here — prerelease, so no alias.
1584
+ *
1585
+ * Its DECLARATION lives here rather than beside the artifact functions because `session-artifact.ts`
1586
+ * imports this module; declaring it there and importing it back would be a module cycle. The export
1587
+ * from the package barrel is unchanged.
1588
+ */
1589
+ declare const SESSION_RECORD_ENVELOPE_VERSION = 1;
1590
+ /** A persisted record with the version of the shape it was written in. */
1591
+ interface IVersionedInteractiveSessionRecord {
1592
+ schemaVersion: number;
1593
+ record: IInteractiveSessionRecord$1;
1594
+ }
1595
+ /**
1596
+ * Every key the record contract declares.
1597
+ *
1598
+ * Exported so a test can compare it against `keyof IInteractiveSessionRecord`: a member added to the
1599
+ * contract without a branch below then fails that comparison rather than being silently dropped by
1600
+ * a decoder that never heard of it.
1601
+ */
1602
+ declare const INTERACTIVE_SESSION_RECORD_KEYS: readonly string[];
1603
+ /**
1604
+ * Decode a bare persisted record.
1605
+ *
1606
+ * The value is decoded, never cast: what comes back is either a record every member of which was
1607
+ * checked, or the list of every place it failed — not the first place.
1608
+ */
1609
+ declare function decodeInteractiveSessionRecord(value: unknown): TSessionRecordDecodeOutcome;
1610
+ /**
1611
+ * Decode a versioned envelope.
1612
+ *
1613
+ * The version is read BEFORE the record, and a version this build does not implement returns
1614
+ * `unsupported` WITHOUT nested issues: reporting field defects against a shape from another version
1615
+ * describes the reader's expectations, not the data's condition, and a caller cannot act on it.
1616
+ */
1617
+ declare function decodeVersionedInteractiveSessionRecord(value: unknown): TSessionRecordDecodeOutcome;
1618
+ //#endregion
1619
+ export { AUTO_COMPACT_THRESHOLD, AutoModeGate, CONSECUTIVE_BLOCK_LIMIT, CheckpointTree, CompactionError, CompactionOrchestrator, ContextWindowTracker, DEFAULT_COMPACTION_PROMPT, DEFAULT_PROMPT_HISTORY_BLOCK_BYTES, FileSessionLogger, type ICheckpointNode, type IClassifiedCall, type IClassifierVerdict, type ICommandSandboxApproval, type IExternalPayloadReference, type IExternalPayloadSink, type IExternalPayloadSource, type IFileSessionLoggerOptions, type IInteractiveSessionRecord, type IInteractiveSessionStore, INTERACTIVE_SESSION_RECORD_KEYS, type INodePromptHistoryFileOptions, type INodeToolResultSpillStoreOptions, type IPermissionClassifier, type IPermissionDenial, type IProviderCallTraceObservation, type IProviderEventKey, type ISerializeSessionArtifactOptions, type ISessionLogEntry, type ISessionLogLine, type ISessionLogLoadOptions, type ISessionLogPayloadResolutionErrorMetadata, type ISessionLogPayloadResolutionOptions, type ISessionLogSink, type ISessionLogSource, type ISessionLogger, type ISessionOptions, type ISessionRecord, type ISessionReplayRecord, type ISessionReplayValidationIssue, type ISessionReplayValidationResult, type ISessionRunOptions, type ISessionShutdownOptions, type ISessionStore, type ISpinner, type ITerminalOutput, type IToolEventKey, type IVersionedInteractiveSessionRecord, NodeExternalPayloadSource, NodePromptHistoryFile, NodeSessionLogSink, NodeSessionLogSource, NodeSessionStore, NodeToolResultSpillStore, PermissionEnforcer, SENSITIVE_KEY_PATTERN, SESSION_LOG_EVENT, SESSION_LOG_SCHEMA_VERSION, SESSION_RECORD_ENVELOPE_VERSION, Session, SessionBusyError, SessionLogDecodeError, SessionLogPayloadResolutionError, SilentSessionLogger, type TAutoCompactThreshold, type TAutoModeJudgement, type TDecodedSessionLogEntry, TOTAL_BLOCK_LIMIT, type TPermissionDenialReason, type TPermissionHandler, type TPermissionResult, type TScrubbableValue, type TSessionLogData, type TSessionLogDecodeErrorCode, type TSessionLogEventName, type TSessionLogPayloadResolutionErrorCode, type TSessionLogValue, type TSessionRecordDecodeOutcome, type TToolResultSpillErrorCode, ToolResultSpillError, TurnClaim, assertSafeSessionId, consentScopeFor, createSessionLogExternalPayloadReference, decodeInteractiveSessionRecord, decodeSessionLogEntries, decodeVersionedInteractiveSessionRecord, deserializeSessionArtifact, formatConversationEntries, isSafeSessionId, isSensitiveKey, isSessionLogEvent, loadSessionLogEntries, parsePromptHistoryLine, replaySessionLogEntries, resolveSessionLogExternalPayloads, scrubSensitiveKeys, serializeSessionArtifact, validateSessionReplayLogEntries };
624
1620
  //# sourceMappingURL=index.d.ts.map