@namzu/sdk 62.8.0 → 62.10.0
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 +26 -0
- package/dist/compaction/plan.d.ts +12 -0
- package/dist/compaction/plan.d.ts.map +1 -1
- package/dist/compaction/plan.js +39 -11
- package/dist/compaction/plan.js.map +1 -1
- package/dist/compaction/salience/minhash.d.ts +9 -0
- package/dist/compaction/salience/minhash.d.ts.map +1 -1
- package/dist/compaction/salience/minhash.js +20 -0
- package/dist/compaction/salience/minhash.js.map +1 -1
- package/dist/compaction/salience/score.d.ts.map +1 -1
- package/dist/compaction/salience/score.js +87 -45
- package/dist/compaction/salience/score.js.map +1 -1
- package/dist/compaction/salience/working-set.d.ts +14 -0
- package/dist/compaction/salience/working-set.d.ts.map +1 -1
- package/dist/compaction/salience/working-set.js +84 -33
- package/dist/compaction/salience/working-set.js.map +1 -1
- package/dist/config/runtime.d.ts +2 -0
- package/dist/config/runtime.d.ts.map +1 -1
- package/dist/config/runtime.js +14 -0
- package/dist/config/runtime.js.map +1 -1
- package/dist/connector/http-operation.d.ts +1 -0
- package/dist/connector/http-operation.d.ts.map +1 -1
- package/dist/connector/http-operation.js +1 -1
- package/dist/connector/http-operation.js.map +1 -1
- package/dist/connector/mcp/adapter.d.ts.map +1 -1
- package/dist/connector/mcp/adapter.js +10 -0
- package/dist/connector/mcp/adapter.js.map +1 -1
- package/dist/connector/mcp/client.d.ts +12 -0
- package/dist/connector/mcp/client.d.ts.map +1 -1
- package/dist/connector/mcp/client.js +59 -10
- package/dist/connector/mcp/client.js.map +1 -1
- package/dist/constants/mcp/index.d.ts +15 -0
- package/dist/constants/mcp/index.d.ts.map +1 -1
- package/dist/constants/mcp/index.js +15 -0
- package/dist/constants/mcp/index.js.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +25 -0
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +193 -17
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -1
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -1
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/cancelled-before-start.js +1 -1
- package/dist/runtime/query/cancelled-before-start.js.map +1 -1
- package/dist/runtime/query/checkpoint.js +2 -1
- package/dist/runtime/query/checkpoint.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +4 -1
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/compaction.js +15 -1
- package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
- package/dist/runtime/query/prepare-turn.js +10 -4
- package/dist/runtime/query/prepare-turn.js.map +1 -1
- package/dist/store/session-log/core.d.ts +31 -1
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +95 -9
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/disk.d.ts +2 -0
- package/dist/store/session-log/disk.d.ts.map +1 -1
- package/dist/store/session-log/disk.js +37 -0
- package/dist/store/session-log/disk.js.map +1 -1
- package/dist/store/session-log/fold.d.ts +9 -1
- package/dist/store/session-log/fold.d.ts.map +1 -1
- package/dist/store/session-log/fold.js +18 -2
- package/dist/store/session-log/fold.js.map +1 -1
- package/dist/store/session-log/incremental.d.ts +8 -0
- package/dist/store/session-log/incremental.d.ts.map +1 -1
- package/dist/store/session-log/incremental.js +7 -2
- package/dist/store/session-log/incremental.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -0
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -0
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/store/session-log/verified-prefix.d.ts +46 -0
- package/dist/store/session-log/verified-prefix.d.ts.map +1 -0
- package/dist/store/session-log/verified-prefix.js +233 -0
- package/dist/store/session-log/verified-prefix.js.map +1 -0
- package/dist/types/connector/mcp.d.ts +19 -0
- package/dist/types/connector/mcp.d.ts.map +1 -1
- package/dist/types/session/records.d.ts +80 -1
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +22 -0
- package/dist/types/session/records.js.map +1 -1
- package/package.json +1 -1
- package/src/compaction/plan.ts +53 -11
- package/src/compaction/salience/minhash.ts +20 -0
- package/src/compaction/salience/score.ts +112 -43
- package/src/compaction/salience/working-set.ts +102 -31
- package/src/config/runtime.ts +14 -0
- package/src/connector/http-operation.ts +1 -1
- package/src/connector/mcp/adapter.ts +11 -0
- package/src/connector/mcp/client.ts +75 -12
- package/src/constants/mcp/index.ts +17 -0
- package/src/manager/session/turn-recorder.ts +218 -15
- package/src/public-runtime.ts +1 -0
- package/src/runtime/query/cancelled-before-start.ts +1 -1
- package/src/runtime/query/checkpoint.ts +2 -1
- package/src/runtime/query/index.ts +4 -1
- package/src/runtime/query/iteration/phases/compaction.ts +18 -0
- package/src/runtime/query/prepare-turn.ts +10 -3
- package/src/store/session-log/core.ts +129 -10
- package/src/store/session-log/disk.ts +32 -0
- package/src/store/session-log/fold.ts +19 -2
- package/src/store/session-log/incremental.ts +15 -2
- package/src/store/session-log/index.ts +1 -0
- package/src/store/session-log/verified-prefix.ts +243 -0
- package/src/types/connector/mcp.ts +19 -0
- package/src/types/session/records.ts +25 -0
|
@@ -59,6 +59,16 @@ export class DiskLogMedium implements LogMedium {
|
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
async identity(): Promise<string | undefined> {
|
|
63
|
+
try {
|
|
64
|
+
const stats = await stat(this.file)
|
|
65
|
+
return `${stats.dev}:${stats.ino}:${this.file}`
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (isErrno(error, 'ENOENT')) return undefined
|
|
68
|
+
throw error
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
async read(offset: number, length: number): Promise<Uint8Array> {
|
|
63
73
|
let handle: FileHandle
|
|
64
74
|
try {
|
|
@@ -76,6 +86,28 @@ export class DiskLogMedium implements LogMedium {
|
|
|
76
86
|
}
|
|
77
87
|
}
|
|
78
88
|
|
|
89
|
+
async scan(offset: number, visit: (chunk: Uint8Array) => boolean): Promise<void> {
|
|
90
|
+
let handle: FileHandle
|
|
91
|
+
try {
|
|
92
|
+
handle = await open(this.file, 'r')
|
|
93
|
+
} catch (error) {
|
|
94
|
+
if (isErrno(error, 'ENOENT')) return
|
|
95
|
+
throw error
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const buffer = Buffer.allocUnsafe(1 << 20)
|
|
99
|
+
let position = offset
|
|
100
|
+
for (;;) {
|
|
101
|
+
const { bytesRead } = await handle.read(buffer, 0, buffer.length, position)
|
|
102
|
+
if (bytesRead === 0) return
|
|
103
|
+
position += bytesRead
|
|
104
|
+
if (visit(buffer.subarray(0, bytesRead))) return
|
|
105
|
+
}
|
|
106
|
+
} finally {
|
|
107
|
+
await handle.close()
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
79
111
|
async *stream(offset: number): AsyncIterable<Uint8Array> {
|
|
80
112
|
if ((await this.size()) <= offset) return
|
|
81
113
|
const stream = createReadStream(this.file, { start: offset, highWaterMark: 1 << 20 })
|
|
@@ -22,7 +22,10 @@ import type { SpillRef } from './spill.js'
|
|
|
22
22
|
* override or a structured-output answer is what every reader of the fold
|
|
23
23
|
* sees; the model's raw text stays in the log for audit only.
|
|
24
24
|
*
|
|
25
|
-
*
|
|
25
|
+
* `message_edited` is applied the same way as `message_replaced`: a compaction
|
|
26
|
+
* pass's edit of one message, written under the message's own id.
|
|
27
|
+
*
|
|
28
|
+
* Only `message`, `message_replaced`, `message_edited` and `compaction` records move the fold.
|
|
26
29
|
* The live compaction events (`compaction_completed` and the rest) are
|
|
27
30
|
* reports, not instructions.
|
|
28
31
|
*/
|
|
@@ -118,7 +121,8 @@ export class SessionMessageFold {
|
|
|
118
121
|
case 'message':
|
|
119
122
|
this.#message(record as MessageRecord)
|
|
120
123
|
return
|
|
121
|
-
case 'message_replaced':
|
|
124
|
+
case 'message_replaced':
|
|
125
|
+
case 'message_edited': {
|
|
122
126
|
const spill = (record as { spill?: SpillRef }).spill
|
|
123
127
|
this.#replacements.set(record.targetMessageId, {
|
|
124
128
|
message: record.content,
|
|
@@ -311,6 +315,18 @@ export class SessionTurnState {
|
|
|
311
315
|
return this.#active
|
|
312
316
|
}
|
|
313
317
|
|
|
318
|
+
/**
|
|
319
|
+
* An independent copy at the records applied so far. `apply` replaces the
|
|
320
|
+
* active turn's object rather than editing it, so the copy may share it.
|
|
321
|
+
*/
|
|
322
|
+
clone(): SessionTurnState {
|
|
323
|
+
const copy = new SessionTurnState()
|
|
324
|
+
copy.#active = this.#active
|
|
325
|
+
copy.#lastTurnId = this.#lastTurnId
|
|
326
|
+
copy.#started = this.#started
|
|
327
|
+
return copy
|
|
328
|
+
}
|
|
329
|
+
|
|
314
330
|
/** The last turn that started, active or not. */
|
|
315
331
|
get lastTurnId(): TurnId | undefined {
|
|
316
332
|
return this.#lastTurnId
|
|
@@ -412,5 +428,6 @@ const CLOSES_OR_DECIDES: ReadonlySet<string> = new Set([
|
|
|
412
428
|
'audit',
|
|
413
429
|
'log_repaired',
|
|
414
430
|
'message_replaced',
|
|
431
|
+
'message_edited',
|
|
415
432
|
'session_updated',
|
|
416
433
|
])
|
|
@@ -27,7 +27,8 @@ import type { SessionLog } from './core.js'
|
|
|
27
27
|
*
|
|
28
28
|
* Cost that remains: the state of the "every message ever recorded" reduction
|
|
29
29
|
* holds each message the log ever recorded, superseded and compacted ones
|
|
30
|
-
* included,
|
|
30
|
+
* included, and the earlier texts of an edited message (one per edit, until a
|
|
31
|
+
* replacement clears them), so it grows with the log and not with the live context, and it
|
|
31
32
|
* lives for the life of the process (for the sessions kept). Reading it
|
|
32
33
|
* returns a copy of all of them, which is O(history) per turn. Both are small
|
|
33
34
|
* next to the whole-log walks this file removes, and are left as they are.
|
|
@@ -71,6 +72,14 @@ export interface IncrementalFold<S, R> {
|
|
|
71
72
|
/** The reduction's name; one cache slot per name and read mode. */
|
|
72
73
|
readonly name: string
|
|
73
74
|
readonly mode: 'strict' | 'tolerant'
|
|
75
|
+
/**
|
|
76
|
+
* Strict reductions only: before resuming, hash every byte before the
|
|
77
|
+
* resume point against what this process verified (`read({ reverify })`),
|
|
78
|
+
* so a resumed read refuses a log altered anywhere, as a read from byte 0
|
|
79
|
+
* does, without parsing the prefix again. Off by default: the window the
|
|
80
|
+
* header describes then applies.
|
|
81
|
+
*/
|
|
82
|
+
readonly reverify?: boolean
|
|
74
83
|
/** A fresh, empty state. */
|
|
75
84
|
create(): S
|
|
76
85
|
/** Fold one record into the state, in place. */
|
|
@@ -103,7 +112,11 @@ export async function foldSessionLog<S, R>(
|
|
|
103
112
|
}
|
|
104
113
|
let moved = false
|
|
105
114
|
try {
|
|
106
|
-
const walk = log.read({
|
|
115
|
+
const walk = log.read({
|
|
116
|
+
mode: fold.mode,
|
|
117
|
+
...(anchor ? { after: anchor } : {}),
|
|
118
|
+
...(fold.reverify && fold.mode === 'strict' ? { reverify: true } : {}),
|
|
119
|
+
})
|
|
107
120
|
let summary: Awaited<ReturnType<typeof walk.next>>
|
|
108
121
|
for (;;) {
|
|
109
122
|
summary = await walk.next()
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import * as crypto from 'node:crypto'
|
|
2
|
+
import type { SessionId } from '../../types/ids/index.js'
|
|
3
|
+
import type { RecordPointer } from '../../types/session/records.js'
|
|
4
|
+
import { LineSplitter, type LogBytes, type SessionLogEntry } from './chain.js'
|
|
5
|
+
import { SessionTurnState } from './fold.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* What this process has already verified of a session log, so that checking
|
|
9
|
+
* it again costs the bytes and not the parsing.
|
|
10
|
+
*
|
|
11
|
+
* A ledger remembers, for every record this process verified IN FULL (schema,
|
|
12
|
+
* parse, hash chain, seq, gen, links: everything `SessionLogChain.accept`
|
|
13
|
+
* checks), the byte length of its line and the SHA-256 of those bytes, plus
|
|
14
|
+
* the head pointer, the `gen`, the session id and the turn state folded to
|
|
15
|
+
* the head. A later claim of the same log reads every byte again and hashes
|
|
16
|
+
* each line; a line whose length and digest equal the remembered ones is
|
|
17
|
+
* byte-identical to a line that already passed every rule, so it passes them
|
|
18
|
+
* again without being parsed. Only what lies after the remembered head is
|
|
19
|
+
* verified in full. Nothing about the file's identity is trusted for
|
|
20
|
+
* soundness: the content is compared. Any difference (a changed byte, a
|
|
21
|
+
* shorter file, a replaced file, a line that no longer splits the same way)
|
|
22
|
+
* makes the caller discard the ledger and verify from byte 0 as before.
|
|
23
|
+
*
|
|
24
|
+
* A ledger is fed only from entries that came out of a full verification and
|
|
25
|
+
* that continue it (`seq` = count + 1 and `prev` equal to the head), so what
|
|
26
|
+
* it holds is a prefix of one valid chain and nothing else. It costs 36 bytes
|
|
27
|
+
* per record.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
const DIGEST_BYTES = 32
|
|
31
|
+
/** Logs kept; the least recently used is dropped. */
|
|
32
|
+
const LEDGERS_KEPT = 16
|
|
33
|
+
/** A log with more records than this is not remembered (about 72 MB of digests). */
|
|
34
|
+
const MAX_RECORDS = 1 << 21
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The digest of one line as lowercase hex. A string, not a Buffer: a Buffer per
|
|
38
|
+
* line (82,000 of them in a long log) is native memory the collector has to
|
|
39
|
+
* chase, and measured about twice the CPU. The one-shot form where Node has it.
|
|
40
|
+
*/
|
|
41
|
+
const hexDigestOf: (line: Uint8Array) => string =
|
|
42
|
+
typeof crypto.hash === 'function'
|
|
43
|
+
? (line) => crypto.hash('sha256', line, 'hex')
|
|
44
|
+
: (line) => crypto.createHash('sha256').update(line).digest('hex')
|
|
45
|
+
|
|
46
|
+
/** Whether `hex` (64 lowercase hex characters) spells the 32 bytes at `at` in `bytes`. */
|
|
47
|
+
function hexEqualsBytes(hex: string, bytes: Uint8Array, at: number): boolean {
|
|
48
|
+
if (hex.length !== DIGEST_BYTES * 2) return false
|
|
49
|
+
for (let i = 0; i < DIGEST_BYTES; i++) {
|
|
50
|
+
const high = hex.charCodeAt(i * 2)
|
|
51
|
+
const low = hex.charCodeAt(i * 2 + 1)
|
|
52
|
+
const value =
|
|
53
|
+
((high <= 0x39 ? high - 0x30 : high - 0x57) << 4) | (low <= 0x39 ? low - 0x30 : low - 0x57)
|
|
54
|
+
if (value !== bytes[at + i]) return false
|
|
55
|
+
}
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export class VerifiedPrefix {
|
|
60
|
+
#count = 0
|
|
61
|
+
#lengths = new Uint32Array(1024)
|
|
62
|
+
#digests = Buffer.allocUnsafe(1024 * DIGEST_BYTES)
|
|
63
|
+
#head: RecordPointer | null = null
|
|
64
|
+
#gen = 0
|
|
65
|
+
#sessionId: SessionId | undefined
|
|
66
|
+
#end = 0
|
|
67
|
+
#dead = false
|
|
68
|
+
readonly #turns = new SessionTurnState()
|
|
69
|
+
|
|
70
|
+
/** Records remembered. */
|
|
71
|
+
get count(): number {
|
|
72
|
+
return this.#count
|
|
73
|
+
}
|
|
74
|
+
get head(): RecordPointer | null {
|
|
75
|
+
return this.#head
|
|
76
|
+
}
|
|
77
|
+
get gen(): number {
|
|
78
|
+
return this.#gen
|
|
79
|
+
}
|
|
80
|
+
get sessionId(): SessionId | undefined {
|
|
81
|
+
return this.#sessionId
|
|
82
|
+
}
|
|
83
|
+
/** Byte offset just after the last remembered record. */
|
|
84
|
+
get end(): number {
|
|
85
|
+
return this.#end
|
|
86
|
+
}
|
|
87
|
+
/** The turn state at the head, as its own copy. */
|
|
88
|
+
turns(): SessionTurnState {
|
|
89
|
+
return this.#turns.clone()
|
|
90
|
+
}
|
|
91
|
+
/** Lengths and digests as they stand now; entries below `count` never change. */
|
|
92
|
+
get lengths(): Uint32Array {
|
|
93
|
+
return this.#lengths
|
|
94
|
+
}
|
|
95
|
+
get digests(): Buffer {
|
|
96
|
+
return this.#digests
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether the remembered record `seq` is exactly `pointer` (length and digest). */
|
|
100
|
+
holds(pointer: RecordPointer): boolean {
|
|
101
|
+
const at = pointer.seq - 1
|
|
102
|
+
if (at < 0 || at >= this.#count) return false
|
|
103
|
+
if (this.#lengths[at] !== pointer.length) return false
|
|
104
|
+
return (
|
|
105
|
+
this.#digests.toString('hex', at * DIGEST_BYTES, (at + 1) * DIGEST_BYTES) === pointer.sha256
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Remember a record that was verified in full. Ignored unless it continues
|
|
111
|
+
* what is held: the next seq, naming the head by its pointer.
|
|
112
|
+
*/
|
|
113
|
+
append(entry: SessionLogEntry): void {
|
|
114
|
+
if (this.#dead) return
|
|
115
|
+
const { record, pointer } = entry
|
|
116
|
+
if (pointer.seq !== this.#count + 1 || pointer.offset !== this.#end) return
|
|
117
|
+
const prev = record.prev
|
|
118
|
+
if (this.#head === null ? prev !== null : !samePointer(prev, this.#head)) return
|
|
119
|
+
if (this.#sessionId !== undefined && record.sessionId !== this.#sessionId) return
|
|
120
|
+
if (this.#count >= MAX_RECORDS) {
|
|
121
|
+
this.#dead = true
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
if (this.#count === this.#lengths.length) {
|
|
125
|
+
const lengths = new Uint32Array(this.#lengths.length * 2)
|
|
126
|
+
lengths.set(this.#lengths)
|
|
127
|
+
this.#lengths = lengths
|
|
128
|
+
const digests = Buffer.allocUnsafe(this.#digests.length * 2)
|
|
129
|
+
this.#digests.copy(digests)
|
|
130
|
+
this.#digests = digests
|
|
131
|
+
}
|
|
132
|
+
this.#lengths[this.#count] = pointer.length
|
|
133
|
+
this.#digests.write(pointer.sha256, this.#count * DIGEST_BYTES, DIGEST_BYTES, 'hex')
|
|
134
|
+
this.#count += 1
|
|
135
|
+
this.#head = pointer
|
|
136
|
+
this.#gen = record.gen
|
|
137
|
+
this.#end = pointer.offset + pointer.length
|
|
138
|
+
this.#sessionId ??= record.sessionId
|
|
139
|
+
this.#turns.apply(record)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function samePointer(a: RecordPointer | null | undefined, b: RecordPointer): boolean {
|
|
144
|
+
return (
|
|
145
|
+
a !== null &&
|
|
146
|
+
a !== undefined &&
|
|
147
|
+
a.seq === b.seq &&
|
|
148
|
+
a.offset === b.offset &&
|
|
149
|
+
a.length === b.length &&
|
|
150
|
+
a.sha256 === b.sha256
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const ledgers = new Map<string, VerifiedPrefix>()
|
|
155
|
+
|
|
156
|
+
/** The ledger of a log, if this process has one; marks it recently used. */
|
|
157
|
+
export function verifiedPrefixOf(key: string): VerifiedPrefix | undefined {
|
|
158
|
+
const found = ledgers.get(key)
|
|
159
|
+
if (found !== undefined) {
|
|
160
|
+
ledgers.delete(key)
|
|
161
|
+
ledgers.set(key, found)
|
|
162
|
+
}
|
|
163
|
+
return found
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Discard what is remembered of a log and start an empty ledger for it. */
|
|
167
|
+
export function startVerifiedPrefix(key: string): VerifiedPrefix {
|
|
168
|
+
ledgers.delete(key)
|
|
169
|
+
if (ledgers.size >= LEDGERS_KEPT) {
|
|
170
|
+
const oldest = ledgers.keys().next().value
|
|
171
|
+
if (oldest !== undefined) ledgers.delete(oldest)
|
|
172
|
+
}
|
|
173
|
+
const fresh = new VerifiedPrefix()
|
|
174
|
+
ledgers.set(key, fresh)
|
|
175
|
+
return fresh
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Forget every remembered log: what a process that has not read them yet looks like. For tests. */
|
|
179
|
+
export function clearVerifiedPrefixes(): void {
|
|
180
|
+
ledgers.clear()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** How many logs are remembered. For tests. */
|
|
184
|
+
export function verifiedPrefixCount(): number {
|
|
185
|
+
return ledgers.size
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Read the first `count` lines of `bytes` and compare each one's length and
|
|
190
|
+
* SHA-256 with the ledger's. Resolves `true` only when every one is equal.
|
|
191
|
+
* Parses nothing and keeps nothing. Any difference, a log that ends sooner,
|
|
192
|
+
* or a fragment no record could be, resolves `false`: the caller then verifies
|
|
193
|
+
* in full, which says what is wrong.
|
|
194
|
+
*/
|
|
195
|
+
export async function rehashPrefix(
|
|
196
|
+
bytes: Pick<LogBytes, 'size'> & {
|
|
197
|
+
stream(offset: number): AsyncIterable<Uint8Array>
|
|
198
|
+
scan?(offset: number, visit: (chunk: Uint8Array) => boolean): Promise<void>
|
|
199
|
+
},
|
|
200
|
+
ledger: VerifiedPrefix,
|
|
201
|
+
count: number,
|
|
202
|
+
): Promise<boolean> {
|
|
203
|
+
if (count === 0) return true
|
|
204
|
+
if (count > ledger.count) return false
|
|
205
|
+
const lengths = ledger.lengths
|
|
206
|
+
const digests = ledger.digests
|
|
207
|
+
if ((await bytes.size()) < endOfLine(lengths, count)) return false
|
|
208
|
+
const splitter = new LineSplitter(0)
|
|
209
|
+
let index = 0
|
|
210
|
+
let matched = false
|
|
211
|
+
// True once the answer is known. A chunk is read from a buffer the caller of `visit`
|
|
212
|
+
// may reuse, so nothing here keeps a slice of it: a line is hashed where it stands.
|
|
213
|
+
const visit = (chunk: Uint8Array): boolean => {
|
|
214
|
+
for (const { line } of splitter.push(chunk)) {
|
|
215
|
+
if (line.byteLength !== lengths[index]) return true
|
|
216
|
+
if (!hexEqualsBytes(hexDigestOf(line), digests, index * DIGEST_BYTES)) return true
|
|
217
|
+
index += 1
|
|
218
|
+
if (index === count) {
|
|
219
|
+
matched = true
|
|
220
|
+
return true
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return false
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
if (bytes.scan !== undefined) {
|
|
227
|
+
await bytes.scan(0, visit)
|
|
228
|
+
} else {
|
|
229
|
+
for await (const chunk of bytes.stream(0)) if (visit(chunk)) break
|
|
230
|
+
}
|
|
231
|
+
} catch (error) {
|
|
232
|
+
// A fragment longer than any record can be: not the log that was verified.
|
|
233
|
+
if ((error as { name?: string }).name === 'SessionLogIntegrityError') return false
|
|
234
|
+
throw error
|
|
235
|
+
}
|
|
236
|
+
return matched
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function endOfLine(lengths: Uint32Array, count: number): number {
|
|
240
|
+
let total = 0
|
|
241
|
+
for (let i = 0; i < count; i++) total += lengths[i] as number
|
|
242
|
+
return total
|
|
243
|
+
}
|
|
@@ -522,6 +522,25 @@ export interface MCPClientConfig {
|
|
|
522
522
|
* forever — no error, no failure, just a turn that stopped.
|
|
523
523
|
*/
|
|
524
524
|
requestTimeoutMs?: number
|
|
525
|
+
/**
|
|
526
|
+
* Treat the deadline as an idle deadline for a tool call that reports
|
|
527
|
+
* progress: each accepted `notifications/progress` (one whose value rose)
|
|
528
|
+
* re-arms `requestTimeoutMs` for that call. Off by default, which keeps
|
|
529
|
+
* the single fixed deadline.
|
|
530
|
+
*
|
|
531
|
+
* Only a `tools/call` made with `onProgress` carries a progress token, so
|
|
532
|
+
* only such a call can be extended. The call is still bounded in total by
|
|
533
|
+
* `maxTotalTimeoutMs`, which progress never extends.
|
|
534
|
+
*/
|
|
535
|
+
resetTimeoutOnProgress?: boolean
|
|
536
|
+
/**
|
|
537
|
+
* The most a progress-extended call may take in total, counted from when
|
|
538
|
+
* the request was issued, in milliseconds. An integer no smaller than
|
|
539
|
+
* `requestTimeoutMs`. Defaults to ten times `requestTimeoutMs`
|
|
540
|
+
* (`DEFAULT_MCP_TOTAL_TIMEOUT_FACTOR`). Applies only with
|
|
541
|
+
* `resetTimeoutOnProgress`.
|
|
542
|
+
*/
|
|
543
|
+
maxTotalTimeoutMs?: number
|
|
525
544
|
/**
|
|
526
545
|
* How long `connect()`'s era probe waits for an answer before deciding
|
|
527
546
|
* the peer speaks a legacy revision. Defaults to
|
|
@@ -457,6 +457,28 @@ export const MessageReplacedRecordSchema = recordSchema('message_replaced', {
|
|
|
457
457
|
]),
|
|
458
458
|
})
|
|
459
459
|
|
|
460
|
+
/**
|
|
461
|
+
* A compaction pass changed one message's text in place (a long narration cut
|
|
462
|
+
* to its first sentence, a large tool result cleared) and the message kept its
|
|
463
|
+
* id. The fold shows `content` for `targetMessageId` from this record on, in
|
|
464
|
+
* the message's own position, exactly as it does for `message_replaced`; the
|
|
465
|
+
* `message` record it targets still holds the original text.
|
|
466
|
+
*
|
|
467
|
+
* A record of its own, flagged `ignorable`, and not another `message_replaced`
|
|
468
|
+
* reason: that record's `reason` is a closed list an earlier release validates
|
|
469
|
+
* strictly, so a new value would make it refuse the whole log. An earlier
|
|
470
|
+
* release skips this one and folds the unedited text of the message.
|
|
471
|
+
* `reason` is free text for the same reason: a later release may name more
|
|
472
|
+
* kinds of edit without this one refusing its log.
|
|
473
|
+
*/
|
|
474
|
+
export const MessageEditedRecordSchema = inTurn('message_edited', {
|
|
475
|
+
ignorable: z.literal(true),
|
|
476
|
+
targetMessageId: messageId,
|
|
477
|
+
content: messageBody,
|
|
478
|
+
spill: spill.optional(),
|
|
479
|
+
reason: text.min(1),
|
|
480
|
+
})
|
|
481
|
+
|
|
460
482
|
// Checkpoints and human decisions (record-only)
|
|
461
483
|
|
|
462
484
|
/** `SessionRecordSchema` also holds `throughSeq` below the record's own `seq`. */
|
|
@@ -665,6 +687,7 @@ const RECORD_ONLY_SCHEMAS = {
|
|
|
665
687
|
session_updated: SessionUpdatedRecordSchema,
|
|
666
688
|
message: MessageRecordSchema,
|
|
667
689
|
message_replaced: MessageReplacedRecordSchema,
|
|
690
|
+
message_edited: MessageEditedRecordSchema,
|
|
668
691
|
checkpoint_written: CheckpointWrittenRecordSchema,
|
|
669
692
|
checkpoint_pruned: CheckpointPrunedRecordSchema,
|
|
670
693
|
decision_requested: DecisionRequestedRecordSchema,
|
|
@@ -1085,6 +1108,7 @@ export type SessionStartedRecord = z.infer<typeof SessionStartedRecordSchema>
|
|
|
1085
1108
|
export type SessionUpdatedRecord = z.infer<typeof SessionUpdatedRecordSchema>
|
|
1086
1109
|
export type MessageRecord = z.infer<typeof MessageRecordSchema>
|
|
1087
1110
|
export type MessageReplacedRecord = z.infer<typeof MessageReplacedRecordSchema>
|
|
1111
|
+
export type MessageEditedRecord = z.infer<typeof MessageEditedRecordSchema>
|
|
1088
1112
|
export type CheckpointWrittenRecord = z.infer<typeof CheckpointWrittenRecordSchema>
|
|
1089
1113
|
export type CheckpointPrunedRecord = z.infer<typeof CheckpointPrunedRecordSchema>
|
|
1090
1114
|
export type DecisionRequestedRecord = z.infer<typeof DecisionRequestedRecordSchema>
|
|
@@ -1105,6 +1129,7 @@ export type SessionRecordOnly =
|
|
|
1105
1129
|
| SessionUpdatedRecord
|
|
1106
1130
|
| MessageRecord
|
|
1107
1131
|
| MessageReplacedRecord
|
|
1132
|
+
| MessageEditedRecord
|
|
1108
1133
|
| CheckpointWrittenRecord
|
|
1109
1134
|
| CheckpointPrunedRecord
|
|
1110
1135
|
| DecisionRequestedRecord
|