@namzu/sdk 60.1.0 → 60.2.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 +22 -0
- package/dist/bridge/a2a/mapper.d.ts.map +1 -1
- package/dist/bridge/a2a/mapper.js +3 -0
- package/dist/bridge/a2a/mapper.js.map +1 -1
- package/dist/bridge/sse/mapper.d.ts.map +1 -1
- package/dist/bridge/sse/mapper.js +16 -0
- package/dist/bridge/sse/mapper.js.map +1 -1
- package/dist/contracts/session/api.d.ts +1 -1
- package/dist/contracts/session/api.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.d.ts +7 -1
- package/dist/manager/session/turn-recorder.d.ts.map +1 -1
- package/dist/manager/session/turn-recorder.js +19 -0
- package/dist/manager/session/turn-recorder.js.map +1 -1
- package/dist/peers/dir.d.ts.map +1 -1
- package/dist/peers/dir.js +3 -0
- package/dist/peers/dir.js.map +1 -1
- package/dist/public-runtime.d.ts +1 -0
- package/dist/public-runtime.d.ts.map +1 -1
- package/dist/public-runtime.js +1 -0
- package/dist/public-runtime.js.map +1 -1
- package/dist/runtime/query/events.d.ts.map +1 -1
- package/dist/runtime/query/events.js +17 -0
- package/dist/runtime/query/events.js.map +1 -1
- package/dist/runtime/query/executor.d.ts +23 -0
- package/dist/runtime/query/executor.d.ts.map +1 -1
- package/dist/runtime/query/executor.js +88 -9
- package/dist/runtime/query/executor.js.map +1 -1
- package/dist/runtime/query/index.d.ts.map +1 -1
- package/dist/runtime/query/index.js +13 -0
- package/dist/runtime/query/index.js.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.d.ts.map +1 -1
- package/dist/runtime/query/iteration/phases/tool-review.js +6 -0
- package/dist/runtime/query/iteration/phases/tool-review.js.map +1 -1
- package/dist/runtime/query/review-policy.d.ts.map +1 -1
- package/dist/runtime/query/review-policy.js +12 -2
- package/dist/runtime/query/review-policy.js.map +1 -1
- package/dist/runtime/query/tooling.d.ts +2 -0
- package/dist/runtime/query/tooling.d.ts.map +1 -1
- package/dist/runtime/query/tooling.js +1 -0
- package/dist/runtime/query/tooling.js.map +1 -1
- package/dist/sandbox/provider/local.d.ts.map +1 -1
- package/dist/sandbox/provider/local.js +6 -5
- package/dist/sandbox/provider/local.js.map +1 -1
- package/dist/session/paths.d.ts +15 -22
- package/dist/session/paths.d.ts.map +1 -1
- package/dist/session/paths.js +19 -46
- package/dist/session/paths.js.map +1 -1
- package/dist/store/session-log/core.d.ts +43 -0
- package/dist/store/session-log/core.d.ts.map +1 -1
- package/dist/store/session-log/core.js +190 -3
- package/dist/store/session-log/core.js.map +1 -1
- package/dist/store/session-log/disk.d.ts +1 -1
- package/dist/store/session-log/disk.d.ts.map +1 -1
- package/dist/store/session-log/disk.js +1 -0
- package/dist/store/session-log/disk.js.map +1 -1
- package/dist/store/session-log/index.d.ts +1 -1
- package/dist/store/session-log/index.d.ts.map +1 -1
- package/dist/store/session-log/index.js +1 -1
- package/dist/store/session-log/index.js.map +1 -1
- package/dist/tools/builtins/bash-cwd.d.ts.map +1 -1
- package/dist/tools/builtins/bash-cwd.js +2 -3
- package/dist/tools/builtins/bash-cwd.js.map +1 -1
- package/dist/tools/builtins/read-media.js +3 -3
- package/dist/tools/builtins/read-media.js.map +1 -1
- package/dist/turn/reporter.d.ts.map +1 -1
- package/dist/turn/reporter.js +2 -0
- package/dist/turn/reporter.js.map +1 -1
- package/dist/types/hitl/index.d.ts +8 -0
- package/dist/types/hitl/index.d.ts.map +1 -1
- package/dist/types/hitl/index.js.map +1 -1
- package/dist/types/session/events.d.ts +38 -2
- package/dist/types/session/events.d.ts.map +1 -1
- package/dist/types/session/events.js +4 -0
- package/dist/types/session/events.js.map +1 -1
- package/dist/types/session/records.d.ts +10 -10
- package/dist/types/session/records.d.ts.map +1 -1
- package/dist/types/session/records.js +2 -0
- package/dist/types/session/records.js.map +1 -1
- package/dist/utils/temp.d.ts +82 -0
- package/dist/utils/temp.d.ts.map +1 -0
- package/dist/utils/temp.js +196 -0
- package/dist/utils/temp.js.map +1 -0
- package/package.json +1 -1
- package/src/bridge/a2a/mapper.ts +3 -0
- package/src/bridge/sse/mapper.ts +16 -0
- package/src/contracts/session/api.ts +2 -0
- package/src/manager/session/turn-recorder.ts +21 -0
- package/src/peers/dir.ts +3 -0
- package/src/public-runtime.ts +13 -0
- package/src/runtime/query/events.ts +19 -0
- package/src/runtime/query/executor.ts +92 -8
- package/src/runtime/query/index.ts +12 -0
- package/src/runtime/query/iteration/phases/tool-review.ts +6 -0
- package/src/runtime/query/review-policy.ts +13 -2
- package/src/runtime/query/tooling.ts +3 -0
- package/src/sandbox/provider/local.ts +8 -5
- package/src/session/paths.ts +32 -57
- package/src/store/session-log/core.ts +235 -3
- package/src/store/session-log/disk.ts +7 -1
- package/src/store/session-log/index.ts +4 -0
- package/src/tools/builtins/bash-cwd.ts +2 -3
- package/src/tools/builtins/read-media.ts +3 -3
- package/src/turn/reporter.ts +2 -0
- package/src/types/hitl/index.ts +8 -0
- package/src/types/session/events.ts +41 -1
- package/src/types/session/records.ts +2 -0
- package/src/utils/temp.ts +250 -0
|
@@ -211,8 +211,59 @@ export interface SessionLog {
|
|
|
211
211
|
/** The folded conversation (spec §4.5), spills read back. */
|
|
212
212
|
messages(options?: { readonly throughSeq?: number }): Promise<Message[]>
|
|
213
213
|
readSpill(ref: SpillRef, options?: SpillReadOptions): Promise<string>
|
|
214
|
+
/**
|
|
215
|
+
* Be told when the log stops being able to write (the disk is full) and
|
|
216
|
+
* when it can write again. Returns the function that stops the
|
|
217
|
+
* notifications. A log that cannot degrade does not offer this.
|
|
218
|
+
*/
|
|
219
|
+
onWriteHealth?(listener: (change: SessionLogWriteHealth) => void): () => void
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Why a log's writes are failing; each is a full or unwritable volume. */
|
|
223
|
+
export type SessionLogDegradedCause = 'disk_full' | 'quota_exceeded' | 'read_only'
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* A change in whether a log can write. While `degraded` the session goes on
|
|
227
|
+
* in memory and the records not yet written wait in a bounded buffer.
|
|
228
|
+
*/
|
|
229
|
+
export type SessionLogWriteHealth =
|
|
230
|
+
| {
|
|
231
|
+
readonly state: 'degraded'
|
|
232
|
+
readonly cause: SessionLogDegradedCause
|
|
233
|
+
/** Records waiting to be written, including the one that failed. */
|
|
234
|
+
readonly bufferedRecords: number
|
|
235
|
+
readonly bufferedBytes: number
|
|
236
|
+
/**
|
|
237
|
+
* Set on the one notification sent when the buffer first overflowed
|
|
238
|
+
* and the oldest waiting records were discarded.
|
|
239
|
+
*/
|
|
240
|
+
readonly dropping?: true
|
|
241
|
+
}
|
|
242
|
+
| {
|
|
243
|
+
readonly state: 'recovered'
|
|
244
|
+
/** Records written from the buffer, in order. */
|
|
245
|
+
readonly flushedRecords: number
|
|
246
|
+
/** Records discarded while the buffer was full; the log has a gap where they were. */
|
|
247
|
+
readonly droppedRecords: number
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The cause when `error` is the volume refusing writes, else `undefined`. */
|
|
251
|
+
export function sessionLogDegradedCause(error: unknown): SessionLogDegradedCause | undefined {
|
|
252
|
+
switch ((error as NodeJS.ErrnoException | undefined)?.code) {
|
|
253
|
+
case 'ENOSPC':
|
|
254
|
+
return 'disk_full'
|
|
255
|
+
case 'EDQUOT':
|
|
256
|
+
return 'quota_exceeded'
|
|
257
|
+
case 'EROFS':
|
|
258
|
+
return 'read_only'
|
|
259
|
+
default:
|
|
260
|
+
return undefined
|
|
261
|
+
}
|
|
214
262
|
}
|
|
215
263
|
|
|
264
|
+
/** Default bound on records held in memory while a log cannot write. */
|
|
265
|
+
export const DEFAULT_DEGRADED_BUFFER_BYTES = 8 * 1024 * 1024
|
|
266
|
+
|
|
216
267
|
/** A draft that is not a valid session record once enveloped. */
|
|
217
268
|
export class InvalidSessionRecordError extends Error {
|
|
218
269
|
override readonly name = 'InvalidSessionRecordError'
|
|
@@ -271,6 +322,12 @@ export interface SessionLogCoreOptions {
|
|
|
271
322
|
* and decisions. `all` or `none` as named.
|
|
272
323
|
*/
|
|
273
324
|
readonly sync?: 'boundaries' | 'all' | 'none'
|
|
325
|
+
/**
|
|
326
|
+
* Bytes of records kept in memory while the volume refuses writes (full,
|
|
327
|
+
* over quota, read-only). Past it the oldest are discarded. Default
|
|
328
|
+
* {@link DEFAULT_DEGRADED_BUFFER_BYTES}.
|
|
329
|
+
*/
|
|
330
|
+
readonly degradedBufferBytes?: number
|
|
274
331
|
}
|
|
275
332
|
|
|
276
333
|
const BOUNDARY_TYPES: ReadonlySet<string> = new Set([
|
|
@@ -289,6 +346,8 @@ const BOUNDARY_TYPES: ReadonlySet<string> = new Set([
|
|
|
289
346
|
|
|
290
347
|
/** Characters of a spilled message kept inline as its preview. */
|
|
291
348
|
const PREVIEW_CHARS = 2000
|
|
349
|
+
/** Added to a preview whose full body could not be written. */
|
|
350
|
+
const NOT_SAVED_NOTE = '[… the rest of this output could not be saved: the disk is full]'
|
|
292
351
|
/** Default byte bound above which a `compaction` record's summary is spilled. */
|
|
293
352
|
const COMPACTION_SPILL_ABOVE_BYTES = 256 * 1024
|
|
294
353
|
|
|
@@ -339,6 +398,17 @@ export class SessionLogCore implements SessionLog {
|
|
|
339
398
|
/** Bytes of the log this instance has verified and applied. */
|
|
340
399
|
#synced = 0
|
|
341
400
|
#torn = 0
|
|
401
|
+
readonly #degradedBufferBytes: number
|
|
402
|
+
/** Records accepted into the chain that the medium has not taken yet, oldest first. */
|
|
403
|
+
#pending: { readonly bytes: Buffer; readonly sync: boolean }[] = []
|
|
404
|
+
#pendingBytes = 0
|
|
405
|
+
/** Bytes the medium holds while records are pending. */
|
|
406
|
+
#diskBytes = 0
|
|
407
|
+
#degradedCause: SessionLogDegradedCause | undefined
|
|
408
|
+
#droppedRecords = 0
|
|
409
|
+
#droppedNoticeSent = false
|
|
410
|
+
#flushedWhileDegraded = 0
|
|
411
|
+
readonly #healthListeners = new Set<(change: SessionLogWriteHealth) => void>()
|
|
342
412
|
|
|
343
413
|
constructor(options: SessionLogCoreOptions) {
|
|
344
414
|
this.sessionId = options.sessionId
|
|
@@ -362,12 +432,40 @@ export class SessionLogCore implements SessionLog {
|
|
|
362
432
|
this.#spillAbove,
|
|
363
433
|
)
|
|
364
434
|
this.#sync = options.sync ?? 'boundaries'
|
|
435
|
+
this.#degradedBufferBytes = Math.max(
|
|
436
|
+
0,
|
|
437
|
+
options.degradedBufferBytes ?? DEFAULT_DEGRADED_BUFFER_BYTES,
|
|
438
|
+
)
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
onWriteHealth(listener: (change: SessionLogWriteHealth) => void): () => void {
|
|
442
|
+
this.#healthListeners.add(listener)
|
|
443
|
+
return () => {
|
|
444
|
+
this.#healthListeners.delete(listener)
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
#notify(change: SessionLogWriteHealth): void {
|
|
449
|
+
for (const listener of this.#healthListeners) {
|
|
450
|
+
try {
|
|
451
|
+
listener(change)
|
|
452
|
+
} catch {
|
|
453
|
+
// A listener that throws must not turn a survived write into a failed one.
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/** True while records wait in memory because the volume refuses writes. */
|
|
459
|
+
get degraded(): boolean {
|
|
460
|
+
return this.#degradedCause !== undefined
|
|
365
461
|
}
|
|
366
462
|
|
|
367
463
|
// ── state ──
|
|
368
464
|
|
|
369
465
|
/** Catch up with the medium: verify and apply whatever was appended since the last sync. */
|
|
370
466
|
async #catchUp(): Promise<void> {
|
|
467
|
+
// Records waiting for the disk are ahead of it; the lease keeps others out.
|
|
468
|
+
if (this.#pending.length > 0) return
|
|
371
469
|
const size = await this.#medium.size()
|
|
372
470
|
if (size === this.#synced + this.#torn && size >= this.#synced) return
|
|
373
471
|
if (size < this.#synced) this.#reset()
|
|
@@ -395,6 +493,8 @@ export class SessionLogCore implements SessionLog {
|
|
|
395
493
|
this.#turns = new SessionTurnState()
|
|
396
494
|
this.#synced = 0
|
|
397
495
|
this.#torn = 0
|
|
496
|
+
this.#pending = []
|
|
497
|
+
this.#pendingBytes = 0
|
|
398
498
|
}
|
|
399
499
|
|
|
400
500
|
async #requireCurrent(lease: SessionLease): Promise<void> {
|
|
@@ -461,6 +561,8 @@ export class SessionLogCore implements SessionLog {
|
|
|
461
561
|
*/
|
|
462
562
|
release(lease: SessionLease): Promise<void> {
|
|
463
563
|
return this.#mutex.run(async () => {
|
|
564
|
+
// One last try to save what the disk refused earlier.
|
|
565
|
+
if (this.#pending.length > 0) await this.#flushPending().catch(() => undefined)
|
|
464
566
|
if (this.#held?.fence === lease.fence) {
|
|
465
567
|
this.#held = undefined
|
|
466
568
|
untrackHeldSessionLease(this.#holding)
|
|
@@ -655,13 +757,126 @@ export class SessionLogCore implements SessionLog {
|
|
|
655
757
|
}
|
|
656
758
|
const bytes = Buffer.from(line, 'utf8')
|
|
657
759
|
const sync = this.#sync === 'all' || (this.#sync === 'boundaries' && BOUNDARY_TYPES.has(type))
|
|
658
|
-
await this.#
|
|
760
|
+
await this.#persist(bytes, sync)
|
|
659
761
|
const entry = this.#chain.accept(bytes, this.#synced)
|
|
660
762
|
this.#turns.apply(entry.record)
|
|
661
763
|
this.#synced += bytes.byteLength
|
|
662
764
|
return entry
|
|
663
765
|
}
|
|
664
766
|
|
|
767
|
+
/**
|
|
768
|
+
* Hand a record's bytes to the medium. A volume that refuses writes (full,
|
|
769
|
+
* over quota, read-only) does not fail the record: it waits in a bounded
|
|
770
|
+
* buffer and goes out, in order, with the next record that can be written.
|
|
771
|
+
* Any other error is thrown as before. Caller holds the mutex.
|
|
772
|
+
*/
|
|
773
|
+
async #persist(bytes: Buffer, sync: boolean): Promise<void> {
|
|
774
|
+
if (this.#pending.length === 0) {
|
|
775
|
+
try {
|
|
776
|
+
await this.#medium.append(bytes, this.#synced, sync)
|
|
777
|
+
return
|
|
778
|
+
} catch (error) {
|
|
779
|
+
const cause = sessionLogDegradedCause(error)
|
|
780
|
+
if (cause === undefined) throw error
|
|
781
|
+
this.#diskBytes = this.#synced
|
|
782
|
+
this.#degradedCause = cause
|
|
783
|
+
this.#bufferRecord(bytes, sync)
|
|
784
|
+
this.#notify({
|
|
785
|
+
state: 'degraded',
|
|
786
|
+
cause,
|
|
787
|
+
bufferedRecords: this.#pending.length,
|
|
788
|
+
bufferedBytes: this.#pendingBytes,
|
|
789
|
+
})
|
|
790
|
+
return
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const queued = this.#bufferRecord(bytes, sync)
|
|
794
|
+
try {
|
|
795
|
+
await this.#flushPending()
|
|
796
|
+
} catch (error) {
|
|
797
|
+
// Only the volume refusing writes keeps the session going; anything else
|
|
798
|
+
// fails this record, which must then not stay queued.
|
|
799
|
+
if (sessionLogDegradedCause(error) === undefined) {
|
|
800
|
+
const at = this.#pending.indexOf(queued)
|
|
801
|
+
if (at !== -1) {
|
|
802
|
+
this.#pending.splice(at, 1)
|
|
803
|
+
this.#pendingBytes -= queued.bytes.byteLength
|
|
804
|
+
}
|
|
805
|
+
throw error
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
#bufferRecord(bytes: Buffer, sync: boolean): { readonly bytes: Buffer; readonly sync: boolean } {
|
|
811
|
+
const queued = { bytes, sync }
|
|
812
|
+
this.#pending.push(queued)
|
|
813
|
+
this.#pendingBytes += bytes.byteLength
|
|
814
|
+
let dropped = false
|
|
815
|
+
// The newest record always stays, whatever the bound.
|
|
816
|
+
while (this.#pendingBytes > this.#degradedBufferBytes && this.#pending.length > 1) {
|
|
817
|
+
const oldest = this.#pending.shift()
|
|
818
|
+
if (oldest === undefined) break
|
|
819
|
+
this.#pendingBytes -= oldest.bytes.byteLength
|
|
820
|
+
this.#droppedRecords += 1
|
|
821
|
+
dropped = true
|
|
822
|
+
}
|
|
823
|
+
if (dropped && !this.#droppedNoticeSent && this.#degradedCause !== undefined) {
|
|
824
|
+
this.#droppedNoticeSent = true
|
|
825
|
+
this.#notify({
|
|
826
|
+
state: 'degraded',
|
|
827
|
+
cause: this.#degradedCause,
|
|
828
|
+
bufferedRecords: this.#pending.length,
|
|
829
|
+
bufferedBytes: this.#pendingBytes,
|
|
830
|
+
dropping: true,
|
|
831
|
+
})
|
|
832
|
+
}
|
|
833
|
+
return queued
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
/** Write the buffer in order. Throws the error that stopped it; clears the degraded mark when it empties. */
|
|
837
|
+
async #flushPending(): Promise<void> {
|
|
838
|
+
// A refused write may have left part of a record behind; cut it back.
|
|
839
|
+
const size = await this.#medium.size()
|
|
840
|
+
if (size > this.#diskBytes) await this.#medium.truncate(this.#diskBytes, size)
|
|
841
|
+
while (this.#pending.length > 0) {
|
|
842
|
+
const next = this.#pending[0]
|
|
843
|
+
if (next === undefined) break
|
|
844
|
+
await this.#medium.append(next.bytes, this.#diskBytes, next.sync)
|
|
845
|
+
this.#diskBytes += next.bytes.byteLength
|
|
846
|
+
this.#pendingBytes -= next.bytes.byteLength
|
|
847
|
+
this.#pending.shift()
|
|
848
|
+
this.#flushedWhileDegraded += 1
|
|
849
|
+
}
|
|
850
|
+
const flushed = this.#flushedWhileDegraded
|
|
851
|
+
this.#flushedWhileDegraded = 0
|
|
852
|
+
const dropped = this.#droppedRecords
|
|
853
|
+
this.#degradedCause = undefined
|
|
854
|
+
this.#droppedRecords = 0
|
|
855
|
+
this.#droppedNoticeSent = false
|
|
856
|
+
this.#pendingBytes = 0
|
|
857
|
+
this.#notify({ state: 'recovered', flushedRecords: flushed, droppedRecords: dropped })
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
/**
|
|
861
|
+
* Write a message body beside the log. When the volume refuses it the body
|
|
862
|
+
* is given up (`undefined`) rather than the record,; the record's own write
|
|
863
|
+
* then meets the same refusal and degrades the log.
|
|
864
|
+
*/
|
|
865
|
+
async #spillOrLose(
|
|
866
|
+
key: string,
|
|
867
|
+
content: Parameters<SpillStore['write']>[1],
|
|
868
|
+
text: string,
|
|
869
|
+
): Promise<SpillRef | undefined> {
|
|
870
|
+
try {
|
|
871
|
+
return await this.#spills.write(key, content, text)
|
|
872
|
+
} catch (error) {
|
|
873
|
+
const cause = sessionLogDegradedCause(error)
|
|
874
|
+
if (cause === undefined) throw error
|
|
875
|
+
// The record's own write, next, finds the volume refusing and degrades the log.
|
|
876
|
+
return undefined
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
665
880
|
async #spillIfLarge(original: Record<string, unknown>): Promise<Record<string, unknown>> {
|
|
666
881
|
const candidate = this.#structuredResultSpilling
|
|
667
882
|
? await this.#spillStructuredResults(original)
|
|
@@ -684,11 +899,17 @@ export class SessionLogCore implements SessionLog {
|
|
|
684
899
|
if (size <= spillAbove) return candidate
|
|
685
900
|
if (type === 'turn_completed') {
|
|
686
901
|
if (typeof candidate.result !== 'string') return candidate
|
|
687
|
-
const ref = await this.#
|
|
902
|
+
const ref = await this.#spillOrLose(
|
|
688
903
|
`record:${String(candidate.id)}`,
|
|
689
904
|
'message',
|
|
690
905
|
candidate.result,
|
|
691
906
|
)
|
|
907
|
+
if (ref === undefined) {
|
|
908
|
+
return {
|
|
909
|
+
...candidate,
|
|
910
|
+
result: `${candidate.result.slice(0, PREVIEW_CHARS)}\n${NOT_SAVED_NOTE}`,
|
|
911
|
+
}
|
|
912
|
+
}
|
|
692
913
|
const preview = `${candidate.result.slice(0, PREVIEW_CHARS)}\n[… ${ref.bytes} bytes spilled to ${ref.path}]`
|
|
693
914
|
return { ...candidate, result: preview, resultSpill: ref }
|
|
694
915
|
}
|
|
@@ -708,9 +929,20 @@ export class SessionLogCore implements SessionLog {
|
|
|
708
929
|
: type === 'message_replaced'
|
|
709
930
|
? `record:${String(candidate.id)}`
|
|
710
931
|
: `message:${String(candidate.messageId)}`
|
|
711
|
-
const ref = await this.#
|
|
932
|
+
const ref = await this.#spillOrLose(key, 'message', JSON.stringify(content))
|
|
712
933
|
const text =
|
|
713
934
|
typeof content.content === 'string' ? content.content : JSON.stringify(content.content)
|
|
935
|
+
if (ref === undefined) {
|
|
936
|
+
// The body cannot be kept: the record carries its preview alone.
|
|
937
|
+
return {
|
|
938
|
+
...candidate,
|
|
939
|
+
content: {
|
|
940
|
+
role: content.role,
|
|
941
|
+
content: `${text.slice(0, PREVIEW_CHARS)}\n${NOT_SAVED_NOTE}`,
|
|
942
|
+
...(typeof content.toolCallId === 'string' ? { toolCallId: content.toolCallId } : {}),
|
|
943
|
+
},
|
|
944
|
+
}
|
|
945
|
+
}
|
|
714
946
|
const preview = {
|
|
715
947
|
role: content.role,
|
|
716
948
|
content: `${text.slice(0, PREVIEW_CHARS)}\n[… ${ref.bytes} bytes spilled to ${ref.path}]`,
|
|
@@ -135,7 +135,12 @@ export class DiskLogMedium implements LogMedium {
|
|
|
135
135
|
export interface DiskSessionLogOptions
|
|
136
136
|
extends Pick<
|
|
137
137
|
SessionLogCoreOptions,
|
|
138
|
-
|
|
138
|
+
| 'now'
|
|
139
|
+
| 'spillAboveBytes'
|
|
140
|
+
| 'compactionSpillAboveBytes'
|
|
141
|
+
| 'structuredResultSpilling'
|
|
142
|
+
| 'sync'
|
|
143
|
+
| 'degradedBufferBytes'
|
|
139
144
|
> {
|
|
140
145
|
readonly sessionId: SessionId
|
|
141
146
|
/** `<session-id>.jsonl`. */
|
|
@@ -174,6 +179,7 @@ export class DiskSessionLog extends SessionLogCore {
|
|
|
174
179
|
compactionSpillAboveBytes: options.compactionSpillAboveBytes,
|
|
175
180
|
structuredResultSpilling: options.structuredResultSpilling,
|
|
176
181
|
sync: options.sync,
|
|
182
|
+
degradedBufferBytes: options.degradedBufferBytes,
|
|
177
183
|
})
|
|
178
184
|
this.file = options.file
|
|
179
185
|
this.sessionDir = options.sessionDir
|
|
@@ -21,6 +21,7 @@ export {
|
|
|
21
21
|
type ActiveTurnOptions,
|
|
22
22
|
type AbandonTurnLogOptions,
|
|
23
23
|
type BeginTurnOptions,
|
|
24
|
+
DEFAULT_DEGRADED_BUFFER_BYTES,
|
|
24
25
|
InvalidSessionRecordError,
|
|
25
26
|
type LogMedium,
|
|
26
27
|
type ReadSessionLogOptions,
|
|
@@ -29,10 +30,13 @@ export {
|
|
|
29
30
|
SessionLogConflictError,
|
|
30
31
|
SessionLogCore,
|
|
31
32
|
type SessionLogCoreOptions,
|
|
33
|
+
type SessionLogDegradedCause,
|
|
32
34
|
type SessionLogHead,
|
|
33
35
|
type SessionLogRead,
|
|
34
36
|
type SessionLogReadSummary,
|
|
37
|
+
type SessionLogWriteHealth,
|
|
35
38
|
type SessionRecordDraft,
|
|
39
|
+
sessionLogDegradedCause,
|
|
36
40
|
type TurnStartedDraft,
|
|
37
41
|
walkSessionLog,
|
|
38
42
|
} from './core.js'
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
import { randomUUID } from 'node:crypto'
|
|
19
19
|
import { statSync } from 'node:fs'
|
|
20
20
|
import { rm } from 'node:fs/promises'
|
|
21
|
-
import { tmpdir } from 'node:os'
|
|
22
|
-
import { join } from 'node:path'
|
|
23
21
|
|
|
22
|
+
import { tempFilePath } from '../../utils/temp.js'
|
|
24
23
|
import { type CommandShell, hostCommandShell } from '../command-shell.js'
|
|
25
24
|
import { pathOutsideRoots, toolRoots } from '../paths.js'
|
|
26
25
|
import { readBoundedFile } from './bounded-file-read.js'
|
|
@@ -55,7 +54,7 @@ export function withCwdCapture(command: string, file: string): string {
|
|
|
55
54
|
}
|
|
56
55
|
|
|
57
56
|
export function newCwdFile(): string {
|
|
58
|
-
return
|
|
57
|
+
return tempFilePath(`cwd-${randomUUID()}`)
|
|
59
58
|
}
|
|
60
59
|
|
|
61
60
|
/** Where this session's next foreground call starts. Synchronous: the spawn must not wait on it. */
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process'
|
|
2
|
-
import {
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
2
|
+
import { readdir, rm } from 'node:fs/promises'
|
|
4
3
|
import { join } from 'node:path'
|
|
4
|
+
import { createTempDir } from '../../utils/temp.js'
|
|
5
5
|
import { readBoundedFile } from './bounded-file-read.js'
|
|
6
6
|
import { screenshotTargetSize } from './computer-use-image.js'
|
|
7
7
|
import { describeRaster } from './view-image.js'
|
|
@@ -254,7 +254,7 @@ export async function readPdfPages(
|
|
|
254
254
|
}
|
|
255
255
|
last = total === null ? PDF_WHOLE_READ_MAX_PAGES : total
|
|
256
256
|
}
|
|
257
|
-
const directory = await
|
|
257
|
+
const directory = await createTempDir('pdf-')
|
|
258
258
|
try {
|
|
259
259
|
const rendered = await renderPages(bytes, first, last, directory, signal)
|
|
260
260
|
if (!rendered.ok) return { ok: false, message: rendered.message }
|
package/src/turn/reporter.ts
CHANGED
|
@@ -455,6 +455,8 @@ export function createTurnReporter(parentLogger?: Logger): TurnReporter {
|
|
|
455
455
|
case 'tool_calls_admitted':
|
|
456
456
|
case 'compaction_shed':
|
|
457
457
|
case 'compaction_started':
|
|
458
|
+
case 'session_log_degraded':
|
|
459
|
+
case 'session_log_recovered':
|
|
458
460
|
// Deliberately silent. The report is what a human reads about a
|
|
459
461
|
// run; replaying every shed message into it would bury the
|
|
460
462
|
// summary line that says the pass happened, in exactly the
|
package/src/types/hitl/index.ts
CHANGED
|
@@ -300,6 +300,14 @@ export interface ToolCallSummary {
|
|
|
300
300
|
* and `strict`. A host's own handler may ignore it.
|
|
301
301
|
*/
|
|
302
302
|
skillGrant?: { readonly skill: string }
|
|
303
|
+
/**
|
|
304
|
+
* Set when this call writes a file inside the session's own scratch folder
|
|
305
|
+
* and nothing else about it needs a person: the folder is the session's, so
|
|
306
|
+
* the review policy does not ask about it (outside `strict`, which asks about
|
|
307
|
+
* everything). A rule that asked for review, a protected path and a tool's
|
|
308
|
+
* own `requiresApproval` all keep the call in review whatever this says.
|
|
309
|
+
*/
|
|
310
|
+
scratchpadWrite?: true
|
|
303
311
|
/**
|
|
304
312
|
* What a `pre_tool_use` hook answered about approving this call, as the
|
|
305
313
|
* hooks reported it. `ask` is acted on by the kernel (it makes the call an
|
|
@@ -211,6 +211,40 @@ type CoreSessionEvent =
|
|
|
211
211
|
/** The window the trigger measured against. */
|
|
212
212
|
contextWindowTokens: number
|
|
213
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* The session log can no longer write: the volume holding it is full, over
|
|
216
|
+
* its quota or read-only. The session goes on in memory and the records not
|
|
217
|
+
* yet written wait in a bounded buffer, so a reply is not lost, but a
|
|
218
|
+
* restart before the log recovers would resume from an older point. Emitted
|
|
219
|
+
* once on entering that state, and once more with `dropping` when the
|
|
220
|
+
* buffer first overflowed and its oldest records were discarded.
|
|
221
|
+
*
|
|
222
|
+
* Live only, never recorded: the log is what cannot be written.
|
|
223
|
+
*/
|
|
224
|
+
| {
|
|
225
|
+
type: 'session_log_degraded'
|
|
226
|
+
sessionId: SessionId
|
|
227
|
+
turnId?: TurnId
|
|
228
|
+
cause: 'disk_full' | 'quota_exceeded' | 'read_only'
|
|
229
|
+
/** Records waiting to be written. */
|
|
230
|
+
bufferedRecords: number
|
|
231
|
+
bufferedBytes: number
|
|
232
|
+
/** The buffer overflowed and the oldest waiting records were discarded. */
|
|
233
|
+
dropping?: boolean
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The session log writes again after `session_log_degraded`: the waiting
|
|
237
|
+
* records were written, in order. `droppedRecords` is how many were
|
|
238
|
+
* discarded while the buffer was full; the log has a gap where they were.
|
|
239
|
+
* Live only, never recorded.
|
|
240
|
+
*/
|
|
241
|
+
| {
|
|
242
|
+
type: 'session_log_recovered'
|
|
243
|
+
sessionId: SessionId
|
|
244
|
+
turnId?: TurnId
|
|
245
|
+
flushedRecords: number
|
|
246
|
+
droppedRecords: number
|
|
247
|
+
}
|
|
214
248
|
| {
|
|
215
249
|
type: 'compaction_completed'
|
|
216
250
|
sessionId: SessionId
|
|
@@ -1370,6 +1404,8 @@ export type SessionEventListener = (event: SessionEvent) => void | Promise<void>
|
|
|
1370
1404
|
* runtime-context message or tool result that carried it, and a noisy watch
|
|
1371
1405
|
* would write tens of megabytes an hour of raw process output a second time.
|
|
1372
1406
|
* `background_job_exited.monitor` is the durable record of how a watch ended.
|
|
1407
|
+
* `session_log_degraded` and `session_log_recovered` are ephemeral because the
|
|
1408
|
+
* log is what cannot be written while they matter.
|
|
1373
1409
|
* `compaction_started` is ephemeral too: it only says a pass is about to run,
|
|
1374
1410
|
* and the `compaction` record or `compaction_failed` that ends it is the
|
|
1375
1411
|
* durable fact.
|
|
@@ -1381,13 +1417,15 @@ export const EPHEMERAL_EVENT_TYPES: ReadonlySet<SessionEventType> = new Set<Sess
|
|
|
1381
1417
|
'tool_progress',
|
|
1382
1418
|
'monitor_event',
|
|
1383
1419
|
'compaction_started',
|
|
1420
|
+
'session_log_degraded',
|
|
1421
|
+
'session_log_recovered',
|
|
1384
1422
|
])
|
|
1385
1423
|
|
|
1386
1424
|
export function isEphemeralEvent(event: { readonly type: string }): boolean {
|
|
1387
1425
|
return EPHEMERAL_EVENT_TYPES.has(event.type as SessionEventType)
|
|
1388
1426
|
}
|
|
1389
1427
|
|
|
1390
|
-
/** The event types a session log records: every literal except the ephemeral
|
|
1428
|
+
/** The event types a session log records: every literal except the ephemeral eight. */
|
|
1391
1429
|
export type PersistedSessionEventType = Exclude<
|
|
1392
1430
|
SessionEventType,
|
|
1393
1431
|
| 'text_delta'
|
|
@@ -1396,4 +1434,6 @@ export type PersistedSessionEventType = Exclude<
|
|
|
1396
1434
|
| 'tool_progress'
|
|
1397
1435
|
| 'monitor_event'
|
|
1398
1436
|
| 'compaction_started'
|
|
1437
|
+
| 'session_log_degraded'
|
|
1438
|
+
| 'session_log_recovered'
|
|
1399
1439
|
>
|
|
@@ -647,6 +647,8 @@ const SESSION_EVENT_TYPE_TABLE: Record<SessionEventType, true> = {
|
|
|
647
647
|
request_envelope: true,
|
|
648
648
|
iteration_completed: true,
|
|
649
649
|
compaction_started: true,
|
|
650
|
+
session_log_degraded: true,
|
|
651
|
+
session_log_recovered: true,
|
|
650
652
|
compaction_shed: true,
|
|
651
653
|
compaction_completed: true,
|
|
652
654
|
compaction_tool_results_cleared: true,
|