@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/compaction/plan.d.ts +12 -0
  3. package/dist/compaction/plan.d.ts.map +1 -1
  4. package/dist/compaction/plan.js +39 -11
  5. package/dist/compaction/plan.js.map +1 -1
  6. package/dist/compaction/salience/minhash.d.ts +9 -0
  7. package/dist/compaction/salience/minhash.d.ts.map +1 -1
  8. package/dist/compaction/salience/minhash.js +20 -0
  9. package/dist/compaction/salience/minhash.js.map +1 -1
  10. package/dist/compaction/salience/score.d.ts.map +1 -1
  11. package/dist/compaction/salience/score.js +87 -45
  12. package/dist/compaction/salience/score.js.map +1 -1
  13. package/dist/compaction/salience/working-set.d.ts +14 -0
  14. package/dist/compaction/salience/working-set.d.ts.map +1 -1
  15. package/dist/compaction/salience/working-set.js +84 -33
  16. package/dist/compaction/salience/working-set.js.map +1 -1
  17. package/dist/config/runtime.d.ts +2 -0
  18. package/dist/config/runtime.d.ts.map +1 -1
  19. package/dist/config/runtime.js +14 -0
  20. package/dist/config/runtime.js.map +1 -1
  21. package/dist/connector/http-operation.d.ts +1 -0
  22. package/dist/connector/http-operation.d.ts.map +1 -1
  23. package/dist/connector/http-operation.js +1 -1
  24. package/dist/connector/http-operation.js.map +1 -1
  25. package/dist/connector/mcp/adapter.d.ts.map +1 -1
  26. package/dist/connector/mcp/adapter.js +10 -0
  27. package/dist/connector/mcp/adapter.js.map +1 -1
  28. package/dist/connector/mcp/client.d.ts +12 -0
  29. package/dist/connector/mcp/client.d.ts.map +1 -1
  30. package/dist/connector/mcp/client.js +59 -10
  31. package/dist/connector/mcp/client.js.map +1 -1
  32. package/dist/constants/mcp/index.d.ts +15 -0
  33. package/dist/constants/mcp/index.d.ts.map +1 -1
  34. package/dist/constants/mcp/index.js +15 -0
  35. package/dist/constants/mcp/index.js.map +1 -1
  36. package/dist/manager/session/turn-recorder.d.ts +25 -0
  37. package/dist/manager/session/turn-recorder.d.ts.map +1 -1
  38. package/dist/manager/session/turn-recorder.js +193 -17
  39. package/dist/manager/session/turn-recorder.js.map +1 -1
  40. package/dist/public-runtime.d.ts +1 -1
  41. package/dist/public-runtime.d.ts.map +1 -1
  42. package/dist/public-runtime.js +1 -1
  43. package/dist/public-runtime.js.map +1 -1
  44. package/dist/runtime/query/cancelled-before-start.js +1 -1
  45. package/dist/runtime/query/cancelled-before-start.js.map +1 -1
  46. package/dist/runtime/query/checkpoint.js +2 -1
  47. package/dist/runtime/query/checkpoint.js.map +1 -1
  48. package/dist/runtime/query/index.d.ts.map +1 -1
  49. package/dist/runtime/query/index.js +4 -1
  50. package/dist/runtime/query/index.js.map +1 -1
  51. package/dist/runtime/query/iteration/phases/compaction.d.ts.map +1 -1
  52. package/dist/runtime/query/iteration/phases/compaction.js +15 -1
  53. package/dist/runtime/query/iteration/phases/compaction.js.map +1 -1
  54. package/dist/runtime/query/prepare-turn.js +10 -4
  55. package/dist/runtime/query/prepare-turn.js.map +1 -1
  56. package/dist/store/session-log/core.d.ts +31 -1
  57. package/dist/store/session-log/core.d.ts.map +1 -1
  58. package/dist/store/session-log/core.js +95 -9
  59. package/dist/store/session-log/core.js.map +1 -1
  60. package/dist/store/session-log/disk.d.ts +2 -0
  61. package/dist/store/session-log/disk.d.ts.map +1 -1
  62. package/dist/store/session-log/disk.js +37 -0
  63. package/dist/store/session-log/disk.js.map +1 -1
  64. package/dist/store/session-log/fold.d.ts +9 -1
  65. package/dist/store/session-log/fold.d.ts.map +1 -1
  66. package/dist/store/session-log/fold.js +18 -2
  67. package/dist/store/session-log/fold.js.map +1 -1
  68. package/dist/store/session-log/incremental.d.ts +8 -0
  69. package/dist/store/session-log/incremental.d.ts.map +1 -1
  70. package/dist/store/session-log/incremental.js +7 -2
  71. package/dist/store/session-log/incremental.js.map +1 -1
  72. package/dist/store/session-log/index.d.ts +1 -0
  73. package/dist/store/session-log/index.d.ts.map +1 -1
  74. package/dist/store/session-log/index.js +1 -0
  75. package/dist/store/session-log/index.js.map +1 -1
  76. package/dist/store/session-log/verified-prefix.d.ts +46 -0
  77. package/dist/store/session-log/verified-prefix.d.ts.map +1 -0
  78. package/dist/store/session-log/verified-prefix.js +233 -0
  79. package/dist/store/session-log/verified-prefix.js.map +1 -0
  80. package/dist/types/connector/mcp.d.ts +19 -0
  81. package/dist/types/connector/mcp.d.ts.map +1 -1
  82. package/dist/types/session/records.d.ts +80 -1
  83. package/dist/types/session/records.d.ts.map +1 -1
  84. package/dist/types/session/records.js +22 -0
  85. package/dist/types/session/records.js.map +1 -1
  86. package/package.json +1 -1
  87. package/src/compaction/plan.ts +53 -11
  88. package/src/compaction/salience/minhash.ts +20 -0
  89. package/src/compaction/salience/score.ts +112 -43
  90. package/src/compaction/salience/working-set.ts +102 -31
  91. package/src/config/runtime.ts +14 -0
  92. package/src/connector/http-operation.ts +1 -1
  93. package/src/connector/mcp/adapter.ts +11 -0
  94. package/src/connector/mcp/client.ts +75 -12
  95. package/src/constants/mcp/index.ts +17 -0
  96. package/src/manager/session/turn-recorder.ts +218 -15
  97. package/src/public-runtime.ts +1 -0
  98. package/src/runtime/query/cancelled-before-start.ts +1 -1
  99. package/src/runtime/query/checkpoint.ts +2 -1
  100. package/src/runtime/query/index.ts +4 -1
  101. package/src/runtime/query/iteration/phases/compaction.ts +18 -0
  102. package/src/runtime/query/prepare-turn.ts +10 -3
  103. package/src/store/session-log/core.ts +129 -10
  104. package/src/store/session-log/disk.ts +32 -0
  105. package/src/store/session-log/fold.ts +19 -2
  106. package/src/store/session-log/incremental.ts +15 -2
  107. package/src/store/session-log/index.ts +1 -0
  108. package/src/store/session-log/verified-prefix.ts +243 -0
  109. package/src/types/connector/mcp.ts +19 -0
  110. 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
- * Only `message`, `message_replaced` and `compaction` records move the fold.
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, so it grows with the log and not with the live context, and it
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({ mode: fold.mode, ...(anchor ? { after: anchor } : {}) })
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()
@@ -61,6 +61,7 @@ export {
61
61
  withMessageId,
62
62
  } from './fold.js'
63
63
  export { type TornTailRepair, repairRecordDraft } from './heal.js'
64
+ export { type IncrementalFold, foldSessionLog } from './incremental.js'
64
65
  export {
65
66
  foldChildOutcomes,
66
67
  foldTurnOutcomes,
@@ -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