@robota-sdk/agent-session 3.0.0-beta.79 → 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.
- package/CHANGELOG.md +645 -0
- package/README.md +117 -51
- package/dist/node/index.cjs +9 -6
- package/dist/node/index.d.cts +1620 -0
- package/dist/node/index.d.cts.map +1 -0
- package/dist/node/index.d.ts +1144 -148
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.js +9 -6
- package/dist/node/index.js.map +1 -1
- package/package.json +35 -20
package/dist/node/index.d.ts
CHANGED
|
@@ -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-
|
|
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/
|
|
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
|
-
*
|
|
161
|
+
* Leaf type module for {@link IExternalPayloadReference}.
|
|
38
162
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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
|
-
*
|
|
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
|
|
229
|
+
private readonly sink;
|
|
75
230
|
private readonly options;
|
|
76
|
-
|
|
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
|
|
90
|
-
*
|
|
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 =
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
-
|
|
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?:
|
|
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?:
|
|
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
|
|
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
|
-
*
|
|
430
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 -
|
|
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
|
|
518
|
-
* callers must leave the conversation history untouched in
|
|
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
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|