@robota-sdk/agent-session 3.0.7 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +366 -0
- package/README.md +98 -40
- package/dist/index.cjs +9 -0
- package/dist/{node/index.d.cts → index.d.cts} +100 -242
- package/dist/index.d.cts.map +1 -0
- package/dist/{node/index.d.ts → index.d.ts} +100 -242
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/node.cjs +13 -0
- package/dist/node.d.cts +876 -0
- package/dist/node.d.cts.map +1 -0
- package/dist/node.d.ts +876 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +14 -0
- package/dist/node.js.map +1 -0
- package/dist/session-claims-BQGdD3Fe.cjs +1 -0
- package/dist/session-claims-D-Azmxq6.js +2 -0
- package/dist/session-claims-D-Azmxq6.js.map +1 -0
- package/dist/session-claims-DELiAZkZ.d.cts +145 -0
- package/dist/session-claims-DELiAZkZ.d.cts.map +1 -0
- package/dist/session-claims-DELiAZkZ.d.ts +145 -0
- package/dist/session-claims-DELiAZkZ.d.ts.map +1 -0
- package/package.json +26 -25
- package/dist/node/index.cjs +0 -10
- package/dist/node/index.d.cts.map +0 -1
- package/dist/node/index.d.ts.map +0 -1
- package/dist/node/index.js +0 -11
- package/dist/node/index.js.map +0 -1
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
import { c as IExternalPayloadSink, d as IExternalPayloadReference, l as ISessionLogSink, o as IExternalPayloadSource, s as ISessionLogSource, t as ISessionClaimRefusalWords } from "./session-claims-DELiAZkZ.js";
|
|
2
|
+
import { IArtifactSealer, IProcessMarkers, IToolResultSpillStore, TArtifactKind, TArtifactLockedReason, TArtifactSealing, TResolveInWorkspace } from "@robota-sdk/agent-core";
|
|
3
|
+
import { IInteractiveSessionRecord, IInteractiveSessionStore, IPromptHistoryBlock, IPromptHistoryEntry, IPromptHistoryReadOptions, IPromptHistorySource, IPromptHistoryWriter, ISessionAuthorship, ISessionClaim, ISessionClaimOptions, ISessionExportOptions, ISessionLease, ISessionListEntry, ISessionLoadOptions, ISessionSaveOptions, ISessionStoreScan, TSessionLoadOutcome } from "@robota-sdk/agent-interface-session";
|
|
4
|
+
//#region src/process-operation-lock.d.ts
|
|
5
|
+
/** A held OS lock; releasing it twice is harmless. */
|
|
6
|
+
interface IProcessOperationLockHandle {
|
|
7
|
+
release(): void;
|
|
8
|
+
}
|
|
9
|
+
/** Nonblocking exclusive operation locks for names in one host-owned directory. */
|
|
10
|
+
interface IProcessOperationLock {
|
|
11
|
+
tryAcquire(name: string): IProcessOperationLockHandle | undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Creates a host lock whose file is never unlinked while this namespace is in use. The OS releases
|
|
15
|
+
* the lock if its owner exits, including on a crash. A separate same-user program can still replace
|
|
16
|
+
* the path on POSIX; this is cooperative coordination, not a same-user security boundary.
|
|
17
|
+
*/
|
|
18
|
+
declare function createProcessOperationLock(directory: string): IProcessOperationLock;
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/stored-session-codec.d.ts
|
|
21
|
+
/** A stored record: the bytes that go on disk and the plaintext envelope they were made from. */
|
|
22
|
+
interface IEncodedSession {
|
|
23
|
+
readonly stored: Uint8Array;
|
|
24
|
+
readonly text: string;
|
|
25
|
+
}
|
|
26
|
+
/** What a record's stored bytes are to a migration that seals what is still plain (#3679). */
|
|
27
|
+
type TStoredSessionMigrationState =
|
|
28
|
+
/** Not sealed framing: whatever it holds — a valid, corrupt, unsupported or bare record. */
|
|
29
|
+
'plain' |
|
|
30
|
+
/** Sealed in this build's framing, and it opens for this id. */
|
|
31
|
+
'sealed' |
|
|
32
|
+
/** Sealed in this build's framing and it does not open for this id: damaged, or other keys. */
|
|
33
|
+
'does-not-open' |
|
|
34
|
+
/** Sealed in a framing a newer build writes. */
|
|
35
|
+
'newer-format';
|
|
36
|
+
/**
|
|
37
|
+
* Where the bytes of session `id` stand for a migration: decided by whether they open for this id,
|
|
38
|
+
* never by framing alone. Opening needs the keys unlocked; with them locked it throws
|
|
39
|
+
* `ArtifactLockedError`, since "intact but locked" decides nothing.
|
|
40
|
+
*/
|
|
41
|
+
declare function storedSessionMigrationState(id: string, stored: Uint8Array, sealer: IArtifactSealer): TStoredSessionMigrationState;
|
|
42
|
+
/**
|
|
43
|
+
* `raw` — a plain record's bytes exactly as stored, whatever they hold — sealed to session `id`, so a
|
|
44
|
+
* store that opens it reports the same status the plain bytes gave: a corrupt or unsupported record
|
|
45
|
+
* stays one, only no longer readable without the keys.
|
|
46
|
+
*/
|
|
47
|
+
declare function sealStoredSessionBytes(id: string, raw: Uint8Array, sealer: IArtifactSealer): Uint8Array;
|
|
48
|
+
/** Whether plain `bytes` hold a valid record of session `id`: what a migration may offer to mark as the owner's. */
|
|
49
|
+
declare function isPlainSessionRecordOf(id: string, bytes: Uint8Array): boolean;
|
|
50
|
+
/** What a store found at a record's path before it writes over it. */
|
|
51
|
+
type TStoredSessionFile =
|
|
52
|
+
/** Nothing is there: "no such file" and nothing else. */
|
|
53
|
+
{
|
|
54
|
+
readonly kind: 'absent';
|
|
55
|
+
} |
|
|
56
|
+
/**
|
|
57
|
+
* Something is there and it is not sealed framing; its bytes when the store read them whole (a store
|
|
58
|
+
* that seals does, so it can tell whether they are exactly what it holds).
|
|
59
|
+
*/
|
|
60
|
+
{
|
|
61
|
+
readonly kind: 'plain';
|
|
62
|
+
readonly bytes?: Uint8Array;
|
|
63
|
+
} |
|
|
64
|
+
/** A sealed file, read whole. */
|
|
65
|
+
{
|
|
66
|
+
readonly kind: 'sealed';
|
|
67
|
+
readonly bytes: Uint8Array;
|
|
68
|
+
} |
|
|
69
|
+
/** Something is there and it could not be read: it may be sealed, so it is protected like one. */
|
|
70
|
+
{
|
|
71
|
+
readonly kind: 'unreadable';
|
|
72
|
+
} |
|
|
73
|
+
/** Something is there, too large for this store to read back at all. */
|
|
74
|
+
{
|
|
75
|
+
readonly kind: 'oversized';
|
|
76
|
+
};
|
|
77
|
+
/** What `bytes` (or their absence) are, for a store that read the whole file. */
|
|
78
|
+
declare function classifyStoredSessionBytes(bytes: Uint8Array | undefined): TStoredSessionFile;
|
|
79
|
+
//#endregion
|
|
80
|
+
//#region src/stored-session-record.d.ts
|
|
81
|
+
/**
|
|
82
|
+
* How a session store is built. `sealing` is required: every store states whether it keeps its
|
|
83
|
+
* records sealed or plain, because a store that silently kept them plain beside sealed ones would
|
|
84
|
+
* write plaintext next to them. A host that seals states it for every store it builds.
|
|
85
|
+
* `releaseCommand` is the host's command that releases a writer marker whose holder is not running,
|
|
86
|
+
* for a `has_writer` refusal to name; without it the refusal names none.
|
|
87
|
+
*/
|
|
88
|
+
interface ISessionStoreOptions extends ISessionClaimRefusalWords {
|
|
89
|
+
readonly sealing: TArtifactSealing;
|
|
90
|
+
}
|
|
91
|
+
/** The sealing `options` state; one that is not a {@link TArtifactSealing} (a call that ignores the types) is refused. */
|
|
92
|
+
declare function sessionSealingOf(options: ISessionStoreOptions): TArtifactSealing;
|
|
93
|
+
/** Refuses anything that is not a {@link TArtifactSealing}. */
|
|
94
|
+
declare function assertSessionSealing(value: unknown): asserts value is TArtifactSealing;
|
|
95
|
+
/**
|
|
96
|
+
* How a store reaches the file of each record — the one thing it gives {@link StoredSessionRecords}.
|
|
97
|
+
* Each method validates the id the way the store addresses its files.
|
|
98
|
+
*/
|
|
99
|
+
interface IStoredSessionFiles {
|
|
100
|
+
/**
|
|
101
|
+
* What is at session `id`'s path for a load or a listing, read whole, and what such a read reports
|
|
102
|
+
* when it could not read the file whole.
|
|
103
|
+
*/
|
|
104
|
+
load(id: string): {
|
|
105
|
+
readonly file: TStoredSessionFile;
|
|
106
|
+
readonly unreadable: TSessionLoadOutcome;
|
|
107
|
+
};
|
|
108
|
+
/** What is at session `id`'s path now, read whole (plain files too in a store that seals), before it is replaced. */
|
|
109
|
+
current(id: string): TStoredSessionFile;
|
|
110
|
+
/** Replaces session `id`'s file with `stored`, whole. */
|
|
111
|
+
replace(id: string, stored: Uint8Array): void;
|
|
112
|
+
/** What an absent record is rebuilt as (a project session's replay log); without it, `missing`. */
|
|
113
|
+
rebuild?(id: string): TSessionLoadOutcome;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* One store's stored form of its records: how it encodes, decodes, guards a replace, claims and
|
|
117
|
+
* remembers what it holds, over the files {@link IStoredSessionFiles} reaches. A store routes every
|
|
118
|
+
* read and write of a record through one of these.
|
|
119
|
+
*/
|
|
120
|
+
declare class StoredSessionRecords {
|
|
121
|
+
#private;
|
|
122
|
+
/** What a store that seals offers as `IInteractiveSessionStore.authorship`; absent from one that never does. */
|
|
123
|
+
readonly authorship: ISessionAuthorship | undefined;
|
|
124
|
+
/**
|
|
125
|
+
* @param markers the writer markers of the store's `claims` directory, beside its records: every
|
|
126
|
+
* claim, lend and write of an id holds the id's marker, so a store without them does not exist.
|
|
127
|
+
* @param words how refusals are worded (`ISessionStoreOptions.releaseCommand`).
|
|
128
|
+
*/
|
|
129
|
+
constructor(sealing: TArtifactSealing, files: IStoredSessionFiles, markers: IProcessMarkers, words?: ISessionClaimRefusalWords);
|
|
130
|
+
/** Whether this store writes its records sealed. */
|
|
131
|
+
get seals(): boolean;
|
|
132
|
+
/**
|
|
133
|
+
* What session `id` holds, saying which of the five things happened. Only a load that asks to `hold`
|
|
134
|
+
* remembers a record it read, so that this instance can go on saving the session after the keys
|
|
135
|
+
* lock; a probe remembers nothing, and so can never push a held session out of the bounded memory
|
|
136
|
+
* or keep plaintext it was only looking at.
|
|
137
|
+
*/
|
|
138
|
+
load(id: string, options?: ISessionLoadOptions): TSessionLoadOutcome;
|
|
139
|
+
/** {@link load} for one entry of a listing, which no writer merges. */
|
|
140
|
+
listed(id: string): TSessionLoadOutcome;
|
|
141
|
+
/**
|
|
142
|
+
* Claims session `id` for a session this process runs and reads it in the same call (see
|
|
143
|
+
* `IInteractiveSessionStore.claim`): the id's writer marker is taken first — published, or taken
|
|
144
|
+
* over under `lease` — and the read follows, so nothing another writer saves can come between them.
|
|
145
|
+
* An in-process runner's claim of a record a handoff save left takes it over; a copy's read takes
|
|
146
|
+
* no marker. Throws `SessionClaimedError`: `has_writer` for an id claimed or lent here or held by
|
|
147
|
+
* another writer, naming it, and `released` for a lease that has ended.
|
|
148
|
+
*/
|
|
149
|
+
claim(id: string, options?: ISessionClaimOptions): ISessionClaim;
|
|
150
|
+
/**
|
|
151
|
+
* Lends session `id` to another process (see `IInteractiveSessionStore.lend`): its marker is
|
|
152
|
+
* published for this process with the digest of a fresh nonce, which the borrowing process claims
|
|
153
|
+
* with, and the id counts as written here until the end, which removes the marker whoever holds it.
|
|
154
|
+
*/
|
|
155
|
+
lend(id: string): ISessionLease;
|
|
156
|
+
/**
|
|
157
|
+
* Whether session `id` may have a writer now: a claim or a lend of this store, or the writer marker
|
|
158
|
+
* of any other holder whose owner is not provably gone. A surface that would open the id as another
|
|
159
|
+
* session asks this first.
|
|
160
|
+
*/
|
|
161
|
+
hasWriter(id: string): boolean;
|
|
162
|
+
/**
|
|
163
|
+
* Saves `record` after the checks a save must pass: a claimed id is saved only under its live claim,
|
|
164
|
+
* and a claim that ended or lost its marker is refused (the write fence); a save without a claim
|
|
165
|
+
* takes the id's marker for the write and is refused while another writer holds it. Then the file
|
|
166
|
+
* there must be replaceable — absent, plain, exactly what this instance last wrote or holds, or
|
|
167
|
+
* sealed and openable here (a claimed id's only when it is what the owner wrote). What the check
|
|
168
|
+
* stops is a store replacing a sealed session nobody in this process can read. A write that fails
|
|
169
|
+
* with its bytes already in place is remembered as this store's own.
|
|
170
|
+
*/
|
|
171
|
+
save(record: IInteractiveSessionRecord, options?: ISessionSaveOptions): void;
|
|
172
|
+
/** Removes session `id`'s record with `remove`, holding the id's marker; refused while another writer holds it. */
|
|
173
|
+
delete(id: string, remove: () => void): void;
|
|
174
|
+
/**
|
|
175
|
+
* The record this instance itself last wrote or holds, for a writer whose `load` said `locked`:
|
|
176
|
+
* offered only while the file is exactly the bytes it remembers. `undefined` when it holds nothing
|
|
177
|
+
* for `id` (and always in a store that seals nothing); `locked` with the reason `changed` when it
|
|
178
|
+
* does and the file is no longer those bytes, so the writer can say which of the two stopped it.
|
|
179
|
+
*/
|
|
180
|
+
loadHeld(id: string): TSessionLoadOutcome | undefined;
|
|
181
|
+
/**
|
|
182
|
+
* The sealed record of session `id` as a token another store over the same files can take with
|
|
183
|
+
* {@link importHeld} (see `StoredSessionAuthorship.exportHeld`); `undefined` from a store that seals
|
|
184
|
+
* nothing, whose records anyone with the store reads from disk.
|
|
185
|
+
*/
|
|
186
|
+
exportHeld(id: string, options?: ISessionExportOptions): string | undefined;
|
|
187
|
+
/**
|
|
188
|
+
* Holds the record a token from {@link exportHeld} carries, pinned, so a store that cannot open the
|
|
189
|
+
* file (a write-only subagent process) can go on with the session. Throws `TypeError` in a store
|
|
190
|
+
* that seals nothing: it would write the record in plaintext over the sealed one.
|
|
191
|
+
*/
|
|
192
|
+
importHeld(id: string, token: string): void;
|
|
193
|
+
/** Forgets what this instance holds of session `id`, which was deleted. */
|
|
194
|
+
forget(id: string): void;
|
|
195
|
+
}
|
|
196
|
+
//#endregion
|
|
197
|
+
//#region src/session-store.d.ts
|
|
198
|
+
/**
|
|
199
|
+
* Where a store keeps the writer markers of its claims and lends: a directory beside the records that
|
|
200
|
+
* is not a `.json` file, so a listing never reads it as a session.
|
|
201
|
+
*/
|
|
202
|
+
declare const SESSION_CLAIMS_DIRECTORY = "claims";
|
|
203
|
+
/** How a {@link NodeSessionStore} is built. */
|
|
204
|
+
interface INodeSessionStoreOptions extends ISessionStoreOptions {
|
|
205
|
+
/**
|
|
206
|
+
* An ancestor of the store's directory the HOST also owns, tightened along with it (SEC-020).
|
|
207
|
+
* Optional because this adapter does not interpret its base directory as a trusted root and must
|
|
208
|
+
* not guess that a parent belongs to the product — which of them do is composition's knowledge.
|
|
209
|
+
* Left out, a store root that already exists keeps whatever mode it was given: the leaf is made
|
|
210
|
+
* 0700 and the directory above it is not touched.
|
|
211
|
+
*/
|
|
212
|
+
readonly ownedRoot?: string;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Persistent session store backed by individual JSON files.
|
|
216
|
+
*
|
|
217
|
+
* Construct with a host-owned `baseDir`; framework project composition uses a separate
|
|
218
|
+
* authority-backed adapter over the same neutral port.
|
|
219
|
+
*/
|
|
220
|
+
declare class NodeSessionStore implements IInteractiveSessionStore {
|
|
221
|
+
private readonly baseDir;
|
|
222
|
+
private readonly ownedRoot;
|
|
223
|
+
/** How records are stored, and what this instance claims, wrote or holds. */
|
|
224
|
+
private readonly records;
|
|
225
|
+
/** What only a store that seals offers (`IInteractiveSessionStore.authorship`); absent from one that never does. */
|
|
226
|
+
readonly authorship?: ISessionAuthorship;
|
|
227
|
+
/**
|
|
228
|
+
* @param baseDir the directory holding the records.
|
|
229
|
+
* @param options `sealing` (required): `{ sealer }` seals what `save` writes and opens what `load`
|
|
230
|
+
* reads; `'plain'` writes plain JSON and reports a sealed file it finds as `locked`.
|
|
231
|
+
* `ownedRoot`: see {@link INodeSessionStoreOptions}. `releaseCommand`: see `ISessionStoreOptions`.
|
|
232
|
+
*/
|
|
233
|
+
constructor(baseDir: string, options: INodeSessionStoreOptions);
|
|
234
|
+
/**
|
|
235
|
+
* Claims `id` for a session this process runs, reading it in the same call
|
|
236
|
+
* (`IInteractiveSessionStore.claim`): exclusive across processes and store instances, and every
|
|
237
|
+
* later save of the id must carry the claim until it is released.
|
|
238
|
+
*/
|
|
239
|
+
claim(id: string, options?: ISessionClaimOptions): ISessionClaim;
|
|
240
|
+
/** Lends `id` to another process that will write it (`IInteractiveSessionStore.lend`). */
|
|
241
|
+
lend(id: string): ISessionLease;
|
|
242
|
+
/** Whether session `id` may have a writer now (`IInteractiveSessionStore.hasWriter`). */
|
|
243
|
+
hasWriter(id: string): boolean;
|
|
244
|
+
/**
|
|
245
|
+
* Ensure the storage directory exists AND that only its owner can enter it (SEC-020).
|
|
246
|
+
*
|
|
247
|
+
* The `existsSync` guard this replaces is the whole defect. It skipped the case that matters: a
|
|
248
|
+
* directory some earlier version, a shared CI checkout, or another local user left at a wider
|
|
249
|
+
* mode was adopted as ours with no signal. Measured under umask 022 before this change, a fresh
|
|
250
|
+
* sessions directory came out 0755 and its records 0644 — and a directory pre-created at 0777
|
|
251
|
+
* stayed 0777.
|
|
252
|
+
*/
|
|
253
|
+
private ensureDir;
|
|
254
|
+
/**
|
|
255
|
+
* Absolute path to a session's JSON file.
|
|
256
|
+
*
|
|
257
|
+
* SEC-006: every public method routes through here, so validating the id at this one point covers
|
|
258
|
+
* `save` (write), `load` (read), and `delete` (unlink) at once.
|
|
259
|
+
*
|
|
260
|
+
* Issue #2240: `assertSafeSessionId` is the guard, and it is sound — no separator, no `.`/`..`
|
|
261
|
+
* can pass it. But it is a regex reject behind a helper, which static analysis does not model as
|
|
262
|
+
* a path sanitizer, so `js/path-injection` re-opened on `load` every time this function changed
|
|
263
|
+
* length. The containment check below is the shape such tools DO recognise: the resolved path
|
|
264
|
+
* must stay inside the resolved base directory. It is unreachable after the assertion and costs
|
|
265
|
+
* one `resolve`; it exists so the guard is visible where the sink is, not to replace the guard.
|
|
266
|
+
*/
|
|
267
|
+
private filePath;
|
|
268
|
+
/**
|
|
269
|
+
* Persist a session record to disk atomically (CORE-019).
|
|
270
|
+
* Creates the storage directory if needed.
|
|
271
|
+
*
|
|
272
|
+
* Bytes go to a same-directory temp file first, then move into place with rename —
|
|
273
|
+
* a crash mid-write can therefore never leave a truncated JSON where the previous
|
|
274
|
+
* record used to be. Same-directory is load-bearing: cross-device rename is a copy.
|
|
275
|
+
*
|
|
276
|
+
* SEC-020: the atomic write now comes from `writeOwnerOnlyFile`, which carries the mode from the
|
|
277
|
+
* moment the temp file is created. The hand-rolled version here wrote it at the umask's default
|
|
278
|
+
* and let `rename` carry that mode to the final path, so every record was 0644 — and even setting
|
|
279
|
+
* the mode after the write would leave a window in which the full transcript was world-readable
|
|
280
|
+
* on disk.
|
|
281
|
+
*/
|
|
282
|
+
save(session: IInteractiveSessionRecord, options?: ISessionSaveOptions): void;
|
|
283
|
+
/**
|
|
284
|
+
* What is at `path` now, for the guard before a save. Only "no such file" means there is nothing
|
|
285
|
+
* to protect; a file that cannot be opened or read is `unreadable`. The first bytes tell plain from
|
|
286
|
+
* sealed, so the common plain case costs one small read in a store that seals nothing, and the rest
|
|
287
|
+
* of the file is read through the same descriptor, so the bytes judged are those of one file: a
|
|
288
|
+
* sealed file's always, a plain one's in a store that seals, which must tell whether it is exactly
|
|
289
|
+
* what the store holds.
|
|
290
|
+
*/
|
|
291
|
+
private readExisting;
|
|
292
|
+
/**
|
|
293
|
+
* Load a session by its ID, saying WHICH of the five things happened.
|
|
294
|
+
*
|
|
295
|
+
* `undefined` used to answer the first four — never saved, damaged, written by a build this one
|
|
296
|
+
* cannot read, and the read failed — and a caller that meant to preserve fields it does not own
|
|
297
|
+
* then treated "damaged" as "no prior record" and overwrote the file. The outcome type is what
|
|
298
|
+
* removes that, by making the caller answer the question it was not asking. The fifth, `locked`,
|
|
299
|
+
* is a sealed file this process cannot open right now: present and intact, so neither of the two.
|
|
300
|
+
*
|
|
301
|
+
* Only a load that asks to `hold` the session remembers a sealed record it opens (see
|
|
302
|
+
* {@link StoredSessionRecords.load}); a probe and the listing remember nothing.
|
|
303
|
+
*/
|
|
304
|
+
load(id: string, options?: ISessionLoadOptions): TSessionLoadOutcome;
|
|
305
|
+
/** What is at session `id`'s path, read whole, for one load or listing of it. */
|
|
306
|
+
private loadFile;
|
|
307
|
+
/**
|
|
308
|
+
* The record this instance itself last wrote or holds, offered only while the file on disk is
|
|
309
|
+
* still exactly those bytes. It is what a writer needs when `load` says `locked` for a session it
|
|
310
|
+
* is running: the keys that read it back are locked, the copy it holds is not. `undefined` for
|
|
311
|
+
* a session this instance never held, or whose file has changed since.
|
|
312
|
+
*/
|
|
313
|
+
loadHeld(id: string): TSessionLoadOutcome | undefined;
|
|
314
|
+
/** The sealed record of `id` as a token for another store over these files (see {@link StoredSessionRecords.exportHeld}). */
|
|
315
|
+
exportHeld(id: string, options?: ISessionExportOptions): string | undefined;
|
|
316
|
+
/** Holds the record a token from another store's `exportHeld` carries (see {@link StoredSessionRecords.importHeld}). */
|
|
317
|
+
importHeld(id: string, token: string): void;
|
|
318
|
+
/**
|
|
319
|
+
* Every session this directory holds, each with what the store concluded about it.
|
|
320
|
+
*
|
|
321
|
+
* Unreadable entries are REPORTED rather than skipped. A store that distinguishes four outcomes on
|
|
322
|
+
* `load` and then hides two of them from the surface a person browses has moved the defect rather
|
|
323
|
+
* than removed it: the difference a user experiences is between "my session vanished" and "my
|
|
324
|
+
* session needs a different build".
|
|
325
|
+
*
|
|
326
|
+
* The one exception is a sealed session this process cannot open: there is no record to describe
|
|
327
|
+
* (no title, no time, no workspace), so it is not an entry, and {@link scan} is how a surface
|
|
328
|
+
* still says that some sessions are there.
|
|
329
|
+
*/
|
|
330
|
+
list(): readonly ISessionListEntry[];
|
|
331
|
+
/**
|
|
332
|
+
* The listing and how many sealed sessions it left out because they cannot be opened right now,
|
|
333
|
+
* from one read of each file, counted by why: locked keys, or no keys here at all. A record that
|
|
334
|
+
* cannot be read has nothing to show, so the counts are what keep those sessions from looking gone.
|
|
335
|
+
*/
|
|
336
|
+
scan(): ISessionStoreScan;
|
|
337
|
+
private readAll;
|
|
338
|
+
/**
|
|
339
|
+
* The outcome for one directory entry, without letting a bad NAME throw out of `list`.
|
|
340
|
+
*
|
|
341
|
+
* `load` validates the id, because an id reaching it is a caller's value and a malformed one is a
|
|
342
|
+
* bug or an attack (SEC-006). A name read out of the directory is neither: the store did not
|
|
343
|
+
* choose it, and one file it cannot use as an id must not take the whole listing down with it.
|
|
344
|
+
* Routing `list` through `load` made exactly that happen — a single `my session.json` in the
|
|
345
|
+
* sessions directory threw, and the resume picker went with it.
|
|
346
|
+
*
|
|
347
|
+
* Reporting it is the same answer `list` gives for every other file it cannot read, which is the
|
|
348
|
+
* property this work exists to establish.
|
|
349
|
+
*/
|
|
350
|
+
private outcomeForListedId;
|
|
351
|
+
/**
|
|
352
|
+
* Delete a session by its ID, holding its writer marker: refused while another writer holds it.
|
|
353
|
+
* No-ops silently if the session does not exist.
|
|
354
|
+
*/
|
|
355
|
+
delete(id: string): void;
|
|
356
|
+
}
|
|
357
|
+
//#endregion
|
|
358
|
+
//#region src/node-session-log-sink.d.ts
|
|
359
|
+
/** Explicit host-filesystem sink for JSONL logs and their content-addressed sidecars. */
|
|
360
|
+
declare class NodeSessionLogSink implements ISessionLogSink, IExternalPayloadSink {
|
|
361
|
+
private readonly logDirectory;
|
|
362
|
+
readonly externalPayloadSink: IExternalPayloadSink;
|
|
363
|
+
private readonly enabled;
|
|
364
|
+
private readonly lines;
|
|
365
|
+
private readonly payloads;
|
|
366
|
+
/**
|
|
367
|
+
* @param options `sealing` (required): `{ sealer }` seals every append (one sealed line per call)
|
|
368
|
+
* and every sidecar; `'plain'` writes plain lines and content-addressed sidecars.
|
|
369
|
+
*/
|
|
370
|
+
constructor(logDirectory: string, options: ISessionStoreOptions);
|
|
371
|
+
append(sessionId: string, text: string): void;
|
|
372
|
+
writeJson(sessionId: string, sha256: string, serialized: string): IExternalPayloadReference;
|
|
373
|
+
private writePayload;
|
|
374
|
+
}
|
|
375
|
+
//#endregion
|
|
376
|
+
//#region src/stored-append-lines.d.ts
|
|
377
|
+
/** The kinds of artifact an append store's lines are sealed as. */
|
|
378
|
+
type TAppendLineKind = Extract<TArtifactKind, 'prompt-history-line' | 'session-log-line'>;
|
|
379
|
+
/** One physical line of an append store, as this process can read it. Never carries key material. */
|
|
380
|
+
type TStoredLine =
|
|
381
|
+
/** A plain line (one element, verbatim), or a sealed record opened to the lines it holds. */
|
|
382
|
+
{
|
|
383
|
+
readonly kind: 'lines';
|
|
384
|
+
readonly lines: readonly string[];
|
|
385
|
+
} |
|
|
386
|
+
/** Sealed and intact as far as anyone can tell; `reason` says whether an unlock here would read it. */
|
|
387
|
+
{
|
|
388
|
+
readonly kind: 'locked';
|
|
389
|
+
readonly reason: TArtifactLockedReason;
|
|
390
|
+
} |
|
|
391
|
+
/** Sealed in this build's framing and it does not open for this owner: damaged, torn, or sealed with other keys. */
|
|
392
|
+
{
|
|
393
|
+
readonly kind: 'does-not-open';
|
|
394
|
+
} |
|
|
395
|
+
/** Sealed in a framing version a newer build writes: never opened, never counted as damage. */
|
|
396
|
+
{
|
|
397
|
+
readonly kind: 'newer-format';
|
|
398
|
+
};
|
|
399
|
+
/** How one append store keeps its lines: sealed as `kind`, bound to the owner id each call names. */
|
|
400
|
+
declare class StoredLines {
|
|
401
|
+
#private;
|
|
402
|
+
constructor(sealing: TArtifactSealing, kind: TAppendLineKind);
|
|
403
|
+
/**
|
|
404
|
+
* What to append for `text` — one or more `\n`-terminated lines, exactly what a plain store
|
|
405
|
+
* appends in that call. Plain: `text` itself. Sealing: one sealed line with a newline before and
|
|
406
|
+
* after it. Throws whatever sealing throws; it never falls back to plaintext.
|
|
407
|
+
*/
|
|
408
|
+
encode(text: string, ownerId: string): string;
|
|
409
|
+
/** One physical line (no `\n` in it), as this process can read it. Never throws. */
|
|
410
|
+
decode(line: string, ownerId: string): TStoredLine;
|
|
411
|
+
/**
|
|
412
|
+
* Strict: the plain-equivalent text of a whole store's `text`, for decoders that fail on any bad
|
|
413
|
+
* line (see {@link openedText}). A failure of the sealer that is neither `ArtifactLockedError` nor
|
|
414
|
+
* `ArtifactCorruptError` is a defect (a refused context, a misused key), not a damaged line, so it
|
|
415
|
+
* is thrown as it was rather than reported as corruption.
|
|
416
|
+
*/
|
|
417
|
+
openText(text: string, ownerId: string): string;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* The physical lines of a store's text. An UNTERMINATED final fragment with sealed framing, or so
|
|
421
|
+
* short that it is still only the start of it (`RSL` and a version digit, cut anywhere), is left
|
|
422
|
+
* out: every sealed append ends with a newline, so such a fragment is an append in progress (or one
|
|
423
|
+
* whose writer died), not a record. A plain fragment is kept as it is.
|
|
424
|
+
*/
|
|
425
|
+
declare function splitStoredLines(text: string): string[];
|
|
426
|
+
/** One sidecar a sink is asked to write. */
|
|
427
|
+
interface IStoredPayloadWrite {
|
|
428
|
+
readonly relativePath: string;
|
|
429
|
+
readonly data: string | Uint8Array;
|
|
430
|
+
/**
|
|
431
|
+
* The name is the content's own digest, so a file already at it holds these exact bytes and need
|
|
432
|
+
* not be written again. A sealed sidecar's name is random: a file already there is not this one.
|
|
433
|
+
*/
|
|
434
|
+
readonly contentAddressed: boolean;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* How a session-log sink places externalized JSON values beside its log, and how a source opens what
|
|
438
|
+
* it reads back. Plain: the content-addressed `<id>.payloads/<sha256>.json`. Sealing:
|
|
439
|
+
* `<id>.payloads/<22 random base64url characters>.sealed`, holding the payload sealed as
|
|
440
|
+
* `session-log-payload` for the session, written once per payload per instance.
|
|
441
|
+
*/
|
|
442
|
+
declare class StoredPayloads {
|
|
443
|
+
#private;
|
|
444
|
+
constructor(sealing: TArtifactSealing);
|
|
445
|
+
/**
|
|
446
|
+
* The reference a log line carries for `serialized`, after `write` has put its sidecar in place.
|
|
447
|
+
* `write` is called at most once; a sealed name is remembered only once `write` returned, so a
|
|
448
|
+
* failed write is attempted again for the next reference to the same payload.
|
|
449
|
+
*/
|
|
450
|
+
place(sessionId: string, sha256: string, serialized: string, write: (target: IStoredPayloadWrite) => void): IExternalPayloadReference;
|
|
451
|
+
/** How many bytes to read for a payload whose plaintext budget is `maxBytes`. */
|
|
452
|
+
rawBudget(maxBytes: number): number;
|
|
453
|
+
/**
|
|
454
|
+
* The payload bytes in `stored` (what was read at `relativePath`), held to the plaintext budget
|
|
455
|
+
* exactly. A plain store reads every payload as bytes: only a sealed line can
|
|
456
|
+
* reference a sealed payload, and a plain reader never gets past the line. A sealing store passes
|
|
457
|
+
* plain bytes through too, so a directory written before sealing still resolves — a JSON payload
|
|
458
|
+
* never starts with the sealed magic — and opens sealed bytes bound to `ownerId`:
|
|
459
|
+
* `ArtifactLockedError` while they cannot be opened here, and `PAYLOAD_UNREADABLE` when they do
|
|
460
|
+
* not open, were sealed by a newer build, or have no owner to open them for. Any other failure of
|
|
461
|
+
* the sealer is a defect, thrown as it was. The caller then checks the result against the
|
|
462
|
+
* reference's digest and length.
|
|
463
|
+
*/
|
|
464
|
+
open(stored: Uint8Array, maxBytes: number, ownerId: string | undefined, relativePath: string): Uint8Array;
|
|
465
|
+
}
|
|
466
|
+
/** What a stored sidecar is to a migration that seals what is still plain in place (#3679). */
|
|
467
|
+
type TStoredPayloadMigrationState = 'plain' | 'sealed' | 'does-not-open' | 'newer-format';
|
|
468
|
+
/**
|
|
469
|
+
* Where a sidecar of session `ownerId` stands: decided by whether it opens for that session, never by
|
|
470
|
+
* framing alone. With the keys locked it throws `ArtifactLockedError`.
|
|
471
|
+
*/
|
|
472
|
+
declare function storedPayloadMigrationState(stored: Uint8Array, ownerId: string, sealer: IArtifactSealer): TStoredPayloadMigrationState;
|
|
473
|
+
/**
|
|
474
|
+
* A plain sidecar's bytes sealed for session `ownerId`, to be put back under the SAME name: a source
|
|
475
|
+
* opens sealed bytes whatever the name ({@link StoredPayloads.open}), and the digest and length every
|
|
476
|
+
* reference — nested ones included — checks are the plaintext's, so no reference changes.
|
|
477
|
+
*/
|
|
478
|
+
declare function sealStoredPayloadBytes(plain: Uint8Array, ownerId: string, sealer: IArtifactSealer): Uint8Array;
|
|
479
|
+
/** The plain records a session log's text holds, as a migration would seal them. */
|
|
480
|
+
declare function countPlainSessionLogLines(text: string): number;
|
|
481
|
+
/** A whole session log's text with every plain line sealed, for a store that replaces the file whole. */
|
|
482
|
+
interface ISealedSessionLogText {
|
|
483
|
+
readonly text: string;
|
|
484
|
+
readonly sealedLines: number;
|
|
485
|
+
readonly carriedLines: number;
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* `text` (a whole log, nothing appending to it) with every plain record sealed for session
|
|
489
|
+
* `ownerId` as {@link SessionLogLineSealer} seals it. An unfinished sealed append at the end, which
|
|
490
|
+
* no reader shows, is dropped; an unfinished plain line is a record, and is sealed.
|
|
491
|
+
*/
|
|
492
|
+
declare function sealSessionLogText(text: string, ownerId: string, sealer: IArtifactSealer): ISealedSessionLogText;
|
|
493
|
+
//#endregion
|
|
494
|
+
//#region src/session-log-sources.d.ts
|
|
495
|
+
interface INodeExternalPayloadSourceOptions extends ISessionStoreOptions {
|
|
496
|
+
/**
|
|
497
|
+
* The session the payloads belong to, which a sealed one is bound to. Left out, it is the session
|
|
498
|
+
* whose `<id>.payloads` directory the reference names, the layout every sink writes.
|
|
499
|
+
*/
|
|
500
|
+
readonly ownerId?: string;
|
|
501
|
+
}
|
|
502
|
+
/** Explicit host-filesystem adapter. A file path is never accepted by the neutral parser itself. */
|
|
503
|
+
declare class NodeExternalPayloadSource implements IExternalPayloadSource {
|
|
504
|
+
private readonly baseDirectory;
|
|
505
|
+
private readonly payloads;
|
|
506
|
+
private readonly ownerId;
|
|
507
|
+
/**
|
|
508
|
+
* @param options `sealing` (required): `{ sealer }` opens sealed payloads (plain ones still
|
|
509
|
+
* resolve); `'plain'` meets a sealed payload as one no key in this process opens.
|
|
510
|
+
*/
|
|
511
|
+
constructor(baseDirectory: string, options: INodeExternalPayloadSourceOptions);
|
|
512
|
+
readBytes(relativePath: string, maxBytes: number): Uint8Array | undefined;
|
|
513
|
+
/** The bytes at the path, within the raw budget a sealed payload of `maxBytes` needs. */
|
|
514
|
+
private readStored;
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Explicit host-filesystem adapter for a JSONL session log. The log's owner — the session id its
|
|
518
|
+
* sealed lines and payloads are bound to — is the file's stem, `<id>.jsonl`, the name every sink
|
|
519
|
+
* writes; a sealed log copied under another session's name does not open as that session's.
|
|
520
|
+
*/
|
|
521
|
+
declare class NodeSessionLogSource implements ISessionLogSource {
|
|
522
|
+
private readonly logFile;
|
|
523
|
+
readonly externalPayloadSource: IExternalPayloadSource;
|
|
524
|
+
private readonly lines;
|
|
525
|
+
private readonly ownerId;
|
|
526
|
+
/**
|
|
527
|
+
* @param options `sealing` (required): `{ sealer }` opens sealed lines and payloads (plain ones
|
|
528
|
+
* still read); `'plain'` meets a sealed line as one no key in this process opens.
|
|
529
|
+
*/
|
|
530
|
+
constructor(logFile: string, options: ISessionStoreOptions);
|
|
531
|
+
/** Strict: the plain-equivalent text, failing on the first line it cannot read (see {@link StoredLines.openText}). */
|
|
532
|
+
readText(): string | undefined;
|
|
533
|
+
/** Tolerant: every physical line in file order, each as this process can read it; `undefined` when there is no log. */
|
|
534
|
+
readStoredLines(): readonly TStoredLine[] | undefined;
|
|
535
|
+
}
|
|
536
|
+
//#endregion
|
|
537
|
+
//#region src/append-file-rewrite.d.ts
|
|
538
|
+
/** Why a file was left as it is. */
|
|
539
|
+
type TAppendFileRefusal =
|
|
540
|
+
/** A symbolic link, a directory, or anything else that is not a regular file. */
|
|
541
|
+
'not-a-regular-file' |
|
|
542
|
+
/** A regular file with another name too, which a rename would leave holding the old bytes. */
|
|
543
|
+
'hard-linked';
|
|
544
|
+
/** A catch-up that failed after the rename: what of the replaced file it never carried over. */
|
|
545
|
+
interface IAppendFileCatchUpFailure {
|
|
546
|
+
/** Bytes of the replaced file past the last line carried over (at most what was lost). */
|
|
547
|
+
readonly unreadBytes: number;
|
|
548
|
+
/** The failure's name; never its message, which may quote what was being rewritten. */
|
|
549
|
+
readonly reason: string;
|
|
550
|
+
}
|
|
551
|
+
//#endregion
|
|
552
|
+
//#region src/session-log-migration.d.ts
|
|
553
|
+
interface IMigrateSessionLogsOptions {
|
|
554
|
+
/** Seals what is plain and opens what is sealed; the keys must be unlocked. */
|
|
555
|
+
readonly sealer: IArtifactSealer;
|
|
556
|
+
/** `false` plans: everything is read and counted exactly as a run would, and nothing is written. */
|
|
557
|
+
readonly apply: boolean;
|
|
558
|
+
/** An ancestor of the directory the host also owns, tightened along with it (SEC-020). */
|
|
559
|
+
readonly ownedRoot?: string;
|
|
560
|
+
/** Runs before each sidecar replace and each log replace; throwing stops the migration there. */
|
|
561
|
+
readonly beforeCommit?: () => void;
|
|
562
|
+
/** Waits between catch-up passes over a replaced log; defaults to a short timer. */
|
|
563
|
+
readonly pause?: () => Promise<void>;
|
|
564
|
+
/**
|
|
565
|
+
* Also removes the temp files crashed writes left; leave it off while another writer may be running.
|
|
566
|
+
* Off, they are still counted in `staleTemporaries` and left where they are.
|
|
567
|
+
*/
|
|
568
|
+
readonly sweepTemporaries?: boolean;
|
|
569
|
+
/**
|
|
570
|
+
* When the migration started, in milliseconds by the filesystem's clock (its marker's time): a temp
|
|
571
|
+
* file is also judged stale against it, so one in a directory nothing changed in since is found.
|
|
572
|
+
*/
|
|
573
|
+
readonly migrationStartedAt?: number;
|
|
574
|
+
}
|
|
575
|
+
/** A log whose catch-up failed after its rename, and what of the replaced file it left unread. */
|
|
576
|
+
interface ISessionLogCatchUpFailure extends IAppendFileCatchUpFailure {
|
|
577
|
+
readonly path: string;
|
|
578
|
+
}
|
|
579
|
+
/** What a migration of one directory's logs did, or (planning) would do. */
|
|
580
|
+
interface ISessionLogMigration {
|
|
581
|
+
/** Logs that held plain lines: sealed (or to seal). */
|
|
582
|
+
readonly logs: number;
|
|
583
|
+
/** The plain lines among them. */
|
|
584
|
+
readonly lines: number;
|
|
585
|
+
/** Plain sidecars sealed in place (or to seal). */
|
|
586
|
+
readonly payloads: number;
|
|
587
|
+
/** Logs and sidecars that hold no plaintext. */
|
|
588
|
+
readonly alreadySealed: number;
|
|
589
|
+
/** Sealed sidecars that do not open for their session, or that a newer build sealed: left as they are. */
|
|
590
|
+
readonly unopenable: readonly string[];
|
|
591
|
+
/**
|
|
592
|
+
* Links, entries that are not regular, logs and sidecars with another hard link, names that are not
|
|
593
|
+
* session ids.
|
|
594
|
+
*/
|
|
595
|
+
readonly refused: readonly string[];
|
|
596
|
+
/**
|
|
597
|
+
* Logs and sidecars that changed while being sealed, that plaintext reached meanwhile, or that
|
|
598
|
+
* another rewrite replaced first.
|
|
599
|
+
*/
|
|
600
|
+
readonly changed: readonly string[];
|
|
601
|
+
/** Logs sealed whose catch-up failed: lines appended to the replaced file meanwhile are lost. */
|
|
602
|
+
readonly catchUpFailed: readonly ISessionLogCatchUpFailure[];
|
|
603
|
+
/** Temp files crashed writes left: removed (or to remove) with `sweepTemporaries`, else left. */
|
|
604
|
+
readonly staleTemporaries: number;
|
|
605
|
+
}
|
|
606
|
+
/** Seals the plain logs and sidecars of `directory` as the module documentation describes. */
|
|
607
|
+
declare function migrateSessionLogDirectory(directory: string, options: IMigrateSessionLogsOptions): Promise<ISessionLogMigration>;
|
|
608
|
+
//#endregion
|
|
609
|
+
//#region src/session-record-migration.d.ts
|
|
610
|
+
interface IMigrateSessionRecordsOptions {
|
|
611
|
+
/** Seals what is plain and opens what is sealed; the keys must be unlocked. */
|
|
612
|
+
readonly sealer: IArtifactSealer;
|
|
613
|
+
/** `false` plans: everything is read and counted exactly as a run would, and nothing is written. */
|
|
614
|
+
readonly apply: boolean;
|
|
615
|
+
/**
|
|
616
|
+
* Runs before each replace, after the sealed record is written and flushed beside it; throwing
|
|
617
|
+
* stops the migration there with that record as it was.
|
|
618
|
+
*/
|
|
619
|
+
readonly beforeCommit?: () => void;
|
|
620
|
+
/**
|
|
621
|
+
* Also removes the temp files crashed writes left; leave it off while another writer may be running.
|
|
622
|
+
* Off, they are still counted in `staleTemporaries` and left where they are.
|
|
623
|
+
*/
|
|
624
|
+
readonly sweepTemporaries?: boolean;
|
|
625
|
+
/**
|
|
626
|
+
* When the migration started, in milliseconds by the filesystem's clock (its marker's time): a temp
|
|
627
|
+
* file is also judged stale against it, so one in a directory nothing changed in since is found.
|
|
628
|
+
*/
|
|
629
|
+
readonly migrationStartedAt?: number;
|
|
630
|
+
}
|
|
631
|
+
/** What a migration of a store's records did, or (planning) would do. */
|
|
632
|
+
interface ISessionRecordMigration {
|
|
633
|
+
/** Plain records sealed (or to seal). */
|
|
634
|
+
readonly sealed: number;
|
|
635
|
+
/** Records that already open sealed. */
|
|
636
|
+
readonly alreadySealed: number;
|
|
637
|
+
/** Sealed records that do not open here, or that a newer build sealed: left as they are. */
|
|
638
|
+
readonly unopenable: readonly string[];
|
|
639
|
+
/** `.json` files whose name is not a session id: left as they are. */
|
|
640
|
+
readonly unsafeNames: readonly string[];
|
|
641
|
+
/**
|
|
642
|
+
* `.json` entries that are links, not regular files, or regular files with another hard link:
|
|
643
|
+
* never followed or replaced, left as they are.
|
|
644
|
+
*/
|
|
645
|
+
readonly refused: readonly string[];
|
|
646
|
+
/** Records that changed while being sealed: left as they are, for the next run. */
|
|
647
|
+
readonly changed: readonly string[];
|
|
648
|
+
/**
|
|
649
|
+
* The ids of records open in another process — their writer marker stands for an owner not
|
|
650
|
+
* provably gone — left as they are, for a run after it has closed.
|
|
651
|
+
*/
|
|
652
|
+
readonly held: readonly string[];
|
|
653
|
+
/** Temp files crashed writes left with a record in them: removed (or to remove) with `sweepTemporaries`, else left. */
|
|
654
|
+
readonly staleTemporaries: number;
|
|
655
|
+
/**
|
|
656
|
+
* Each record this run sealed, by its id and the SHA-256 of the sealed bytes it put in place — the
|
|
657
|
+
* bytes the owner may then vouch for (`ISessionAuthorship.adopt`), and no others.
|
|
658
|
+
* Empty when planning.
|
|
659
|
+
*/
|
|
660
|
+
readonly sealedRecords: readonly ISealedSessionRecord[];
|
|
661
|
+
}
|
|
662
|
+
/** One record a migration sealed: its id and the SHA-256 (lowercase hex) of the bytes it wrote. */
|
|
663
|
+
interface ISealedSessionRecord {
|
|
664
|
+
readonly id: string;
|
|
665
|
+
readonly digest: string;
|
|
666
|
+
}
|
|
667
|
+
/** Seals the plain records of the store at `baseDir` as the module documentation describes. */
|
|
668
|
+
declare function migrateNodeSessionStore(baseDir: string, options: IMigrateSessionRecordsOptions): ISessionRecordMigration;
|
|
669
|
+
/**
|
|
670
|
+
* The working directories the store's records name — a valid record's `cwd`, and the `cwd` a corrupt
|
|
671
|
+
* or unsupported one still carries — opened with `sealer`, distinct, in file order. Writes nothing;
|
|
672
|
+
* a record that does not open names none.
|
|
673
|
+
*/
|
|
674
|
+
declare function readSessionStoreWorkingDirectories(baseDir: string, options: {
|
|
675
|
+
readonly sealer: IArtifactSealer;
|
|
676
|
+
}): readonly string[];
|
|
677
|
+
//#endregion
|
|
678
|
+
//#region src/tool-result-spill-store.d.ts
|
|
679
|
+
type TToolResultSpillErrorCode = 'invalid-options' | 'unsafe-root' | 'write-failed' | 'missing' | 'expired' | 'invalid-reference' | 'read-failed' | 'cleanup-failed' | 'closed';
|
|
680
|
+
declare class ToolResultSpillError extends Error {
|
|
681
|
+
readonly code: TToolResultSpillErrorCode;
|
|
682
|
+
constructor(code: TToolResultSpillErrorCode);
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Remove the spill directories under `parent` whose recorded owner can no longer be alive (a process
|
|
686
|
+
* killed before `shutdown()` or its exit removal could run). Returns the names removed.
|
|
687
|
+
*/
|
|
688
|
+
declare function sweepStaleToolResultSpillDirectories(parent: string): readonly string[];
|
|
689
|
+
interface INodeToolResultSpillStoreOptions {
|
|
690
|
+
/**
|
|
691
|
+
* Host-owned, owner-only parent that already exists; it is used as it is. Spilled results are
|
|
692
|
+
* conversation content, so they never go to a directory the store picks: the store's directory in
|
|
693
|
+
* the parent records its owner, is removed when the process ends without reaching `shutdown()`,
|
|
694
|
+
* and is removed after a kill by {@link sweepStaleToolResultSpillDirectories}.
|
|
695
|
+
*/
|
|
696
|
+
readonly parentDirectory: string;
|
|
697
|
+
readonly retentionMs?: number;
|
|
698
|
+
readonly now?: () => number;
|
|
699
|
+
/** Receives a fixed, payload-free reason if an idle expiry timer cannot delete a file. */
|
|
700
|
+
readonly onCleanupFailure?: (reason: 'cleanup-failed') => void;
|
|
701
|
+
/**
|
|
702
|
+
* Required. `{ sealer }` keeps every result sealed on disk, bound to its reference, as
|
|
703
|
+
* `<token>.sealed`, and opens it with the same sealer to read it back; a result that does not open
|
|
704
|
+
* reads as `read-failed`. `'plain'` writes each result as it is to `<token>.txt`.
|
|
705
|
+
*/
|
|
706
|
+
readonly sealing: TArtifactSealing;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Node host implementation of core's opaque, session-lifetime spill port. A sealing store keeps the
|
|
710
|
+
* last result it opened in memory, so paging through a large result opens it once.
|
|
711
|
+
*/
|
|
712
|
+
declare class NodeToolResultSpillStore implements IToolResultSpillStore {
|
|
713
|
+
private readonly directory;
|
|
714
|
+
private readonly retentionMs;
|
|
715
|
+
private readonly now;
|
|
716
|
+
private readonly onCleanupFailure?;
|
|
717
|
+
private readonly sealer;
|
|
718
|
+
private readonly entries;
|
|
719
|
+
/** The result a sealing store opened last; forgotten with its entry. */
|
|
720
|
+
private opened?;
|
|
721
|
+
private expiryTimer?;
|
|
722
|
+
private cancelExitRemoval?;
|
|
723
|
+
private closed;
|
|
724
|
+
constructor(options: INodeToolResultSpillStoreOptions);
|
|
725
|
+
private assertRoot;
|
|
726
|
+
private scheduleExpiry;
|
|
727
|
+
write(content: string): Promise<{
|
|
728
|
+
readonly reference: string;
|
|
729
|
+
}>;
|
|
730
|
+
read(reference: string): Promise<string>;
|
|
731
|
+
/** What goes on disk for `content`: the text as it is, or sealed to the result's token. */
|
|
732
|
+
private storedForm;
|
|
733
|
+
/**
|
|
734
|
+
* The text a sealed result holds, held to the plaintext budget exactly. Any failure to open it —
|
|
735
|
+
* damaged, moved from another reference, sealed with other keys — is the caller's `read-failed`.
|
|
736
|
+
*/
|
|
737
|
+
private openStored;
|
|
738
|
+
/**
|
|
739
|
+
* Removes a result. What a sealing store had open of it is forgotten first, so a removal that fails
|
|
740
|
+
* does not keep it in memory; a file already gone counts as removed, or one result deleted from
|
|
741
|
+
* outside would fail every later cleanup, and with it every write.
|
|
742
|
+
*/
|
|
743
|
+
private removeEntry;
|
|
744
|
+
cleanupExpired(): Promise<void>;
|
|
745
|
+
shutdown(): Promise<void>;
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/prompt-history-file.d.ts
|
|
749
|
+
/** 64 KiB: a few hundred prompts per block — enough for the first frame, small enough to yield often. */
|
|
750
|
+
declare const DEFAULT_PROMPT_HISTORY_BLOCK_BYTES: number;
|
|
751
|
+
interface INodePromptHistoryFileOptions extends ISessionStoreOptions {
|
|
752
|
+
/** An ancestor of the file the host also owns, tightened along with the directory (SEC-020). */
|
|
753
|
+
readonly ownedRoot?: string;
|
|
754
|
+
/** Test seam: the read block size in bytes. */
|
|
755
|
+
readonly blockBytes?: number;
|
|
756
|
+
}
|
|
757
|
+
/** One line → an entry, or `undefined` when the line is not a well-formed entry. */
|
|
758
|
+
declare function parsePromptHistoryLine(line: string): IPromptHistoryEntry | undefined;
|
|
759
|
+
declare class NodePromptHistoryFile implements IPromptHistoryWriter, IPromptHistorySource {
|
|
760
|
+
private readonly path;
|
|
761
|
+
private readonly blockBytes;
|
|
762
|
+
private readonly ownedRoot;
|
|
763
|
+
private readonly lines;
|
|
764
|
+
/**
|
|
765
|
+
* @param options `sealing` (required): `{ sealer }` appends each entry as one sealed line and
|
|
766
|
+
* opens sealed lines it reads (plain ones still read); `'plain'` appends plain lines and counts
|
|
767
|
+
* a sealed line as one no key in this process opens.
|
|
768
|
+
*/
|
|
769
|
+
constructor(path: string, options: INodePromptHistoryFileOptions);
|
|
770
|
+
append(entry: IPromptHistoryEntry): void;
|
|
771
|
+
/**
|
|
772
|
+
* Newest-first blocks. Only a missing file is the empty state (a fresh install, or history off);
|
|
773
|
+
* any other open or read failure is thrown so the surface renders it instead of an empty list.
|
|
774
|
+
*/
|
|
775
|
+
read(options: IPromptHistoryReadOptions): AsyncIterable<IPromptHistoryBlock>;
|
|
776
|
+
}
|
|
777
|
+
//#endregion
|
|
778
|
+
//#region src/prompt-history-compaction.d.ts
|
|
779
|
+
/** Why a history was left as it is. */
|
|
780
|
+
type TPromptHistoryRefusal = TAppendFileRefusal;
|
|
781
|
+
interface IInspectPromptHistoryOptions {
|
|
782
|
+
/** Opens sealed lines; with the keys locked, inspecting a sealed line throws `ArtifactLockedError`. */
|
|
783
|
+
readonly sealer: IArtifactSealer;
|
|
784
|
+
}
|
|
785
|
+
interface ICompactPromptHistoryOptions {
|
|
786
|
+
/**
|
|
787
|
+
* Seals every entry and opens the sealed ones. Give the compaction a sealer of its own so all of
|
|
788
|
+
* its lines share one writer epoch. With the keys locked it stops with `ArtifactLockedError` and
|
|
789
|
+
* replaces nothing.
|
|
790
|
+
*/
|
|
791
|
+
readonly sealer: IArtifactSealer;
|
|
792
|
+
/** An ancestor of the file the host also owns, tightened along with the directory (SEC-020). */
|
|
793
|
+
readonly ownedRoot?: string;
|
|
794
|
+
/**
|
|
795
|
+
* Runs once the new file is written and flushed, just before it replaces the history. Throwing
|
|
796
|
+
* abandons the compaction with nothing replaced and no temporary file left.
|
|
797
|
+
*/
|
|
798
|
+
readonly beforeCommit?: () => void;
|
|
799
|
+
/** Waits between catch-up passes over the replaced file; defaults to a short timer. */
|
|
800
|
+
readonly pause?: () => Promise<void>;
|
|
801
|
+
}
|
|
802
|
+
type TPromptHistoryInspection = {
|
|
803
|
+
readonly status: 'missing';
|
|
804
|
+
} | {
|
|
805
|
+
readonly status: 'refused';
|
|
806
|
+
readonly reason: TPromptHistoryRefusal;
|
|
807
|
+
} | {
|
|
808
|
+
readonly status: 'present';
|
|
809
|
+
/** Physical lines that are plaintext. */
|
|
810
|
+
readonly plainLines: number;
|
|
811
|
+
/** Physical lines sealed in this build's framing that open. */
|
|
812
|
+
readonly sealedLines: number;
|
|
813
|
+
/** Physical lines that do not open here or that a newer build sealed. */
|
|
814
|
+
readonly unreadableLines: number;
|
|
815
|
+
};
|
|
816
|
+
type TPromptHistoryCompaction = {
|
|
817
|
+
readonly status: 'missing';
|
|
818
|
+
} | {
|
|
819
|
+
readonly status: 'refused';
|
|
820
|
+
readonly reason: TPromptHistoryRefusal;
|
|
821
|
+
} |
|
|
822
|
+
/** Another compaction replaced the history before this one's rename; nothing was replaced. */
|
|
823
|
+
{
|
|
824
|
+
readonly status: 'replaced';
|
|
825
|
+
} |
|
|
826
|
+
/** The history was deleted before this compaction's rename; nothing was put in its place. */
|
|
827
|
+
{
|
|
828
|
+
readonly status: 'removed';
|
|
829
|
+
} | {
|
|
830
|
+
readonly status: 'compacted';
|
|
831
|
+
/** Entries written sealed, one per line. */
|
|
832
|
+
readonly entries: number;
|
|
833
|
+
/** Of those, entries that were plain lines. */
|
|
834
|
+
readonly fromPlain: number;
|
|
835
|
+
/** Of those, entries appended after the snapshot and caught up from the replaced file. */
|
|
836
|
+
readonly caughtUp: number;
|
|
837
|
+
/** Lines that do not open here, or that a newer build sealed, carried over unchanged. */
|
|
838
|
+
readonly carriedOver: number;
|
|
839
|
+
/** The replaced file ended in an unfinished sealed append, which no reader shows; it was dropped. */
|
|
840
|
+
readonly droppedTail: boolean;
|
|
841
|
+
/** Plain lines in the new file afterwards: plaintext an older build appended meanwhile. */
|
|
842
|
+
readonly plainLinesAfter: number;
|
|
843
|
+
/**
|
|
844
|
+
* Set when the catch-up failed after the rename: entries appended to the replaced file from
|
|
845
|
+
* then on are not in the history (their session records hold them).
|
|
846
|
+
*/
|
|
847
|
+
readonly catchUpFailed?: IAppendFileCatchUpFailure;
|
|
848
|
+
};
|
|
849
|
+
/** What the history holds, line by line, as {@link compactPromptHistoryFile} would find it. */
|
|
850
|
+
declare function inspectPromptHistoryFile(path: string, options: IInspectPromptHistoryOptions): TPromptHistoryInspection;
|
|
851
|
+
/**
|
|
852
|
+
* The distinct `project` values the history's entries name, plain or sealed, in the order first met;
|
|
853
|
+
* lines that do not open or do not parse are passed over. A history that is missing or refused names
|
|
854
|
+
* none. Writes nothing.
|
|
855
|
+
*/
|
|
856
|
+
declare function readPromptHistoryProjects(path: string, options: IInspectPromptHistoryOptions): readonly string[];
|
|
857
|
+
/**
|
|
858
|
+
* Compacts the history at `path` as the module documentation describes. Rerunning it gives the same
|
|
859
|
+
* entries under a new writer epoch.
|
|
860
|
+
*/
|
|
861
|
+
declare function compactPromptHistoryFile(path: string, options: ICompactPromptHistoryOptions): Promise<TPromptHistoryCompaction>;
|
|
862
|
+
//#endregion
|
|
863
|
+
//#region src/workspace-path-resolver.d.ts
|
|
864
|
+
declare function createNodeWorkspacePathResolver(cwd: string): TResolveInWorkspace;
|
|
865
|
+
//#endregion
|
|
866
|
+
//#region src/process-exit-flush.d.ts
|
|
867
|
+
/**
|
|
868
|
+
* The Node host's answer to `IFileSessionLoggerOptions.registerExitFlush`: run `flush` when the
|
|
869
|
+
* process is about to end. `exit` listeners may only do synchronous work, which is what a logger's
|
|
870
|
+
* flush does. Pass this very function to every logger — the logger registers once per distinct
|
|
871
|
+
* function, so one process gets one listener however many loggers it creates.
|
|
872
|
+
*/
|
|
873
|
+
declare function registerProcessExitFlush(flush: () => void): void;
|
|
874
|
+
//#endregion
|
|
875
|
+
export { DEFAULT_PROMPT_HISTORY_BLOCK_BYTES, type IAppendFileCatchUpFailure, type ICompactPromptHistoryOptions, type IEncodedSession, type IInspectPromptHistoryOptions, type IMigrateSessionLogsOptions, type IMigrateSessionRecordsOptions, type INodeExternalPayloadSourceOptions, type INodePromptHistoryFileOptions, type INodeSessionStoreOptions, type INodeToolResultSpillStoreOptions, type ISealedSessionLogText, type ISealedSessionRecord, type ISessionLogCatchUpFailure, type ISessionLogMigration, type ISessionRecordMigration, type ISessionStoreOptions, type IStoredPayloadWrite, type IStoredSessionFiles, NodeExternalPayloadSource, NodePromptHistoryFile, NodeSessionLogSink, NodeSessionLogSource, NodeSessionStore, NodeToolResultSpillStore, SESSION_CLAIMS_DIRECTORY, StoredLines, StoredPayloads, StoredSessionRecords, type TAppendLineKind, type TPromptHistoryCompaction, type TPromptHistoryInspection, type TPromptHistoryRefusal, type TStoredLine, type TStoredPayloadMigrationState, type TStoredSessionFile, type TStoredSessionMigrationState, type TToolResultSpillErrorCode, ToolResultSpillError, assertSessionSealing, classifyStoredSessionBytes, compactPromptHistoryFile, countPlainSessionLogLines, createNodeWorkspacePathResolver, createProcessOperationLock, inspectPromptHistoryFile, isPlainSessionRecordOf, migrateNodeSessionStore, migrateSessionLogDirectory, parsePromptHistoryLine, readPromptHistoryProjects, readSessionStoreWorkingDirectories, registerProcessExitFlush, sealSessionLogText, sealStoredPayloadBytes, sealStoredSessionBytes, sessionSealingOf, splitStoredLines, storedPayloadMigrationState, storedSessionMigrationState, sweepStaleToolResultSpillDirectories };
|
|
876
|
+
//# sourceMappingURL=node.d.ts.map
|