typeclaw 0.36.8 → 0.37.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 (111) hide show
  1. package/README.md +2 -2
  2. package/package.json +3 -2
  3. package/src/agent/index.ts +31 -11
  4. package/src/agent/live-sessions.ts +12 -0
  5. package/src/agent/model-fallback.ts +17 -15
  6. package/src/agent/model-overrides.ts +2 -2
  7. package/src/agent/session-meta.ts +10 -0
  8. package/src/agent/subagents.ts +11 -2
  9. package/src/agent/system-prompt.ts +9 -3
  10. package/src/agent/todo/continuation-policy.ts +6 -3
  11. package/src/agent/todo/continuation-wiring.ts +4 -2
  12. package/src/agent/todo/continuation.ts +3 -3
  13. package/src/agent/tools/todo/index.ts +27 -4
  14. package/src/bundled-plugins/agent-browser/index.ts +33 -108
  15. package/src/bundled-plugins/agent-browser/shim.ts +3 -94
  16. package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +8 -33
  17. package/src/bundled-plugins/doc-render/skills/typeclaw-render-pdf/SKILL.md +2 -2
  18. package/src/bundled-plugins/guard/policies/memory-retrieval-cache-write.ts +7 -1
  19. package/src/bundled-plugins/memory/README.md +80 -23
  20. package/src/bundled-plugins/memory/append-tool.ts +74 -53
  21. package/src/bundled-plugins/memory/citation-superset.ts +4 -0
  22. package/src/bundled-plugins/memory/citations.ts +54 -0
  23. package/src/bundled-plugins/memory/dreaming-metrics.ts +30 -0
  24. package/src/bundled-plugins/memory/dreaming.ts +444 -21
  25. package/src/bundled-plugins/memory/index.ts +544 -400
  26. package/src/bundled-plugins/memory/load-memory.ts +87 -10
  27. package/src/bundled-plugins/memory/load-shards.ts +48 -22
  28. package/src/bundled-plugins/memory/memory-logger.ts +95 -106
  29. package/src/bundled-plugins/memory/memory-retrieval.ts +3 -3
  30. package/src/bundled-plugins/memory/parent-link.ts +33 -0
  31. package/src/bundled-plugins/memory/paths.ts +12 -0
  32. package/src/bundled-plugins/memory/references/frontmatter.ts +197 -0
  33. package/src/bundled-plugins/memory/references/load-references.ts +212 -0
  34. package/src/bundled-plugins/memory/references/store-reference-tool.ts +59 -0
  35. package/src/bundled-plugins/memory/search-tool.ts +282 -45
  36. package/src/bundled-plugins/memory/stream-events.ts +1 -0
  37. package/src/bundled-plugins/memory/stream-io.ts +28 -3
  38. package/src/bundled-plugins/memory/turn-dedup.ts +40 -0
  39. package/src/bundled-plugins/memory/vector/cache-write.ts +19 -0
  40. package/src/bundled-plugins/memory/vector/config.ts +28 -0
  41. package/src/bundled-plugins/memory/vector/doctor.ts +124 -0
  42. package/src/bundled-plugins/memory/vector/embedder.ts +246 -0
  43. package/src/bundled-plugins/memory/vector/hybrid.ts +439 -0
  44. package/src/bundled-plugins/memory/vector/index-on-write.ts +34 -0
  45. package/src/bundled-plugins/memory/vector/inspect.ts +111 -0
  46. package/src/bundled-plugins/memory/vector/passages.ts +125 -0
  47. package/src/bundled-plugins/memory/vector/reference-index-on-write.ts +50 -0
  48. package/src/bundled-plugins/memory/vector/relevance-gate.ts +93 -0
  49. package/src/bundled-plugins/memory/vector/startup.ts +71 -0
  50. package/src/bundled-plugins/memory/vector/store.ts +203 -0
  51. package/src/bundled-plugins/memory/vector/truncation.ts +124 -0
  52. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +2 -0
  53. package/src/channels/router.ts +239 -40
  54. package/src/cli/incomplete-init.ts +57 -0
  55. package/src/cli/init.ts +143 -12
  56. package/src/cli/inspect.ts +11 -5
  57. package/src/cli/model.ts +112 -34
  58. package/src/cli/restart.ts +24 -0
  59. package/src/cli/start.ts +24 -0
  60. package/src/cli/tunnel.ts +53 -8
  61. package/src/config/config.ts +110 -19
  62. package/src/config/index.ts +5 -1
  63. package/src/config/models-mutation.ts +29 -11
  64. package/src/config/providers-mutation.ts +2 -2
  65. package/src/config/providers.ts +146 -12
  66. package/src/container/shared.ts +9 -0
  67. package/src/container/start.ts +87 -4
  68. package/src/cron/consumer.ts +13 -7
  69. package/src/hostd/models.ts +64 -0
  70. package/src/hostd/paths.ts +6 -0
  71. package/src/hostd/portbroker-manager.ts +2 -2
  72. package/src/init/checkpoint.ts +201 -0
  73. package/src/init/dockerfile.ts +121 -34
  74. package/src/init/gitignore.ts +7 -7
  75. package/src/init/index.ts +41 -9
  76. package/src/init/models-dev.ts +96 -21
  77. package/src/init/oauth-login.ts +3 -3
  78. package/src/init/progress.ts +29 -0
  79. package/src/init/validate-api-key.ts +4 -0
  80. package/src/inspect/index.ts +13 -6
  81. package/src/inspect/item-list.ts +11 -2
  82. package/src/inspect/live-list.ts +65 -0
  83. package/src/inspect/open-item.ts +22 -1
  84. package/src/inspect/session-list.ts +29 -0
  85. package/src/models/embedding-model.ts +114 -0
  86. package/src/models/transformers-version.ts +55 -0
  87. package/src/plugin/types.ts +3 -0
  88. package/src/portbroker/container-server.ts +23 -0
  89. package/src/portbroker/forward-request-bus.ts +35 -0
  90. package/src/portbroker/forward-result-bus.ts +2 -3
  91. package/src/portbroker/hostd-client.ts +182 -36
  92. package/src/portbroker/index.ts +6 -1
  93. package/src/portbroker/protocol.ts +9 -2
  94. package/src/run/channel-session-factory.ts +11 -1
  95. package/src/run/index.ts +41 -7
  96. package/src/server/command-runner.ts +24 -1
  97. package/src/server/index.ts +42 -8
  98. package/src/shared/index.ts +2 -0
  99. package/src/shared/protocol.ts +31 -0
  100. package/src/skills/typeclaw-channels/SKILL.md +4 -4
  101. package/src/skills/typeclaw-config/SKILL.md +2 -2
  102. package/src/skills/typeclaw-memory/SKILL.md +3 -1
  103. package/src/skills/typeclaw-permissions/SKILL.md +3 -3
  104. package/src/skills/typeclaw-skills/SKILL.md +1 -1
  105. package/src/skills/typeclaw-tunnels/SKILL.md +22 -1
  106. package/src/tunnels/providers/cloudflare-quick.ts +65 -7
  107. package/src/tunnels/upstream-probe.ts +25 -0
  108. package/typeclaw.schema.json +156 -67
  109. package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +0 -170
  110. package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +0 -421
  111. package/src/portbroker/bind-with-forward.ts +0 -102
@@ -1,6 +1,7 @@
1
+ import { createHash } from 'node:crypto'
1
2
  import { existsSync } from 'node:fs'
2
- import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises'
3
- import { join } from 'node:path'
3
+ import { mkdir, readdir, readFile, unlink, writeFile } from 'node:fs/promises'
4
+ import { basename, join } from 'node:path'
4
5
 
5
6
  import { z } from 'zod'
6
7
 
@@ -11,6 +12,7 @@ import { formatLocalDate, formatLocalDateTime } from '@/shared'
11
12
  import { checkCitationSupersetAcrossShards, summarizeMissingCitations } from './citation-superset'
12
13
  import { parseCitations } from './citations'
13
14
  import { deleteTopicShardTool } from './delete-tool'
15
+ import { computeDreamingMetrics } from './dreaming-metrics'
14
16
  import {
15
17
  addDreamedIds,
16
18
  DREAMING_STATE_FILE,
@@ -20,13 +22,24 @@ import {
20
22
  saveDreamingState,
21
23
  } from './dreaming-state'
22
24
  import { parseShard, renderShard, type ShardFrontmatter } from './frontmatter'
23
- import { listShardSlugs, loadAllShards } from './load-shards'
24
- import { streamFilePath, streamsDir, topicShardPath, topicsDir } from './paths'
25
+ import { listShardSlugs, loadAllShards, loadShard, type TopicShard } from './load-shards'
26
+ import { referencesDir, streamFilePath, streamsDir, topicShardPath, topicsDir } from './paths'
27
+ import { renderReference } from './references/frontmatter'
28
+ import { loadAllReferences, type Reference } from './references/load-references'
25
29
  import { captureShardSnapshot, restoreShardSnapshot } from './shard-snapshot'
26
30
  import type { StreamEvent } from './stream-events'
27
31
  import { readEvents, writeEventsAtomic } from './stream-io'
32
+ import { embed, EMBEDDING_MODEL_ID } from './vector/embedder'
33
+ import type { EmbedFn } from './vector/hybrid'
34
+ import { topicPassage } from './vector/passages'
35
+ import { VectorStore } from './vector/store'
36
+ import { estimateTokens, TEXT_TOKEN_BUDGET } from './vector/truncation'
28
37
 
29
38
  const STREAM_FILE_PATTERN = /^(\d{4}-\d{2}-\d{2})\.jsonl$/
39
+ const REFERENCE_HALF_LIFE_DAYS = 14
40
+ const REFERENCE_DEMOTE_SCORE_THRESHOLD = 0.1
41
+ const REFERENCE_DELETE_DORMANCY_DAYS = 30
42
+ const MS_PER_DAY = 86_400_000
30
43
 
31
44
  export const dreamingPayloadSchema = z.object({
32
45
  agentDir: z.string().min(1),
@@ -55,6 +68,12 @@ type ShardStrength = {
55
68
  daysSinceLastReinforced: number | null
56
69
  }
57
70
 
71
+ type OverBudgetShard = {
72
+ slug: string
73
+ heading: string
74
+ estimatedTokens: number
75
+ }
76
+
58
77
  const consoleLogger: DreamingLogger = {
59
78
  info: (m) => console.warn(m),
60
79
  warn: (m) => console.warn(m),
@@ -137,6 +156,7 @@ export type CompactionStats = {
137
156
  filesCompacted: number
138
157
  watermarksDropped: number
139
158
  fragmentsDropped: number
159
+ droppedFragmentIds: string[]
140
160
  }
141
161
 
142
162
  export type CompactionOptions = {
@@ -179,7 +199,12 @@ export async function compactDailyStreams(
179
199
  touchedDates: readonly string[],
180
200
  options: CompactionOptions,
181
201
  ): Promise<CompactionStats> {
182
- const stats: CompactionStats = { filesCompacted: 0, watermarksDropped: 0, fragmentsDropped: 0 }
202
+ const stats: CompactionStats = {
203
+ filesCompacted: 0,
204
+ watermarksDropped: 0,
205
+ fragmentsDropped: 0,
206
+ droppedFragmentIds: [],
207
+ }
183
208
  const useLegacyFlatStreams = !existsSync(streamsDir(agentDir))
184
209
 
185
210
  for (const date of touchedDates) {
@@ -212,6 +237,7 @@ export async function compactDailyStreams(
212
237
  if (event.type === 'fragment') {
213
238
  if (options.applyFragmentGc && dreamedIds.has(event.id) && !citedIds.has(event.id)) {
214
239
  fragmentsDropped++
240
+ stats.droppedFragmentIds.push(`${date}#${event.id}`)
215
241
  continue
216
242
  }
217
243
  kept.push(event)
@@ -231,6 +257,260 @@ export async function compactDailyStreams(
231
257
  return stats
232
258
  }
233
259
 
260
+ export async function syncTopicVectorsFromSnapshotDiff(
261
+ agentDir: string,
262
+ snapshotBefore: ReadonlyMap<string, Buffer>,
263
+ snapshotAfter: ReadonlyMap<string, Buffer>,
264
+ embedFn: EmbedFn = embed,
265
+ ): Promise<void> {
266
+ const dbPath = join(agentDir, 'memory', '.vectors', 'index.db')
267
+ if (!existsSync(dbPath)) return
268
+
269
+ const store = VectorStore.open(dbPath)
270
+ try {
271
+ for (const [path, afterBuf] of snapshotAfter) {
272
+ const beforeBuf = snapshotBefore.get(path)
273
+ if (beforeBuf !== undefined && beforeBuf.equals(afterBuf)) continue
274
+
275
+ const slug = slugFromSnapshotPath(path)
276
+ const shard = await loadShard(agentDir, slug)
277
+ if (shard === null) continue
278
+ const passage = topicPassage(slug, shard.frontmatter.heading, shard.body)
279
+ const [embedding] = await embedFn([passage.text], 'passage')
280
+ if (embedding === undefined) continue
281
+ store.upsert({
282
+ id: passage.id,
283
+ source: passage.source,
284
+ key: passage.key,
285
+ model: EMBEDDING_MODEL_ID,
286
+ dims: embedding.length,
287
+ embedding,
288
+ contentHash: passage.contentHash,
289
+ })
290
+ }
291
+
292
+ for (const path of snapshotBefore.keys()) {
293
+ if (!snapshotAfter.has(path)) store.delete(`topic:${slugFromSnapshotPath(path)}`)
294
+ }
295
+ } finally {
296
+ store.close()
297
+ }
298
+ }
299
+
300
+ function slugFromSnapshotPath(path: string): string {
301
+ return basename(path, '.md')
302
+ }
303
+
304
+ function deleteStreamVectorsForDroppedFragments(agentDir: string, droppedFragmentIds: readonly string[]): void {
305
+ if (droppedFragmentIds.length === 0) return
306
+ const dbPath = join(agentDir, 'memory', '.vectors', 'index.db')
307
+ if (!existsSync(dbPath)) return
308
+
309
+ const store = VectorStore.open(dbPath)
310
+ try {
311
+ store.deleteMany(droppedFragmentIds.map((fragmentId) => `stream:${fragmentId}`))
312
+ } finally {
313
+ store.close()
314
+ }
315
+ }
316
+
317
+ type ReferenceSaturationStats = {
318
+ referencesDemoted: number
319
+ referencesEvicted: number
320
+ }
321
+
322
+ async function runReferenceSaturationPass(agentDir: string, logger: DreamingLogger): Promise<ReferenceSaturationStats> {
323
+ const references = await loadAllReferences(agentDir, { logger })
324
+ const nowMs = Date.now()
325
+ const evictedSlugs: string[] = []
326
+ const demotedSlugs: string[] = []
327
+ let referencesDemoted = 0
328
+ let referencesEvicted = 0
329
+
330
+ for (const ref of references) {
331
+ if (isReferenceDecayExempt(ref)) continue
332
+
333
+ if (ref.frontmatter.demoted && referenceDormancyDays(ref, nowMs) > REFERENCE_DELETE_DORMANCY_DAYS) {
334
+ await unlink(ref.path)
335
+ evictedSlugs.push(ref.slug)
336
+ referencesEvicted += 1
337
+ continue
338
+ }
339
+
340
+ if (!ref.frontmatter.demoted && referenceScore(ref, nowMs) < REFERENCE_DEMOTE_SCORE_THRESHOLD) {
341
+ await writeFile(ref.path, renderReference({ ...ref.frontmatter, demoted: true }, ref.body))
342
+ demotedSlugs.push(ref.slug)
343
+ referencesDemoted += 1
344
+ }
345
+ }
346
+
347
+ // Demotion excludes a reference from the embed surface (passages.ts skips
348
+ // demoted refs at startup), but the on-write hook indexed it while it was
349
+ // demoted:false. Demoting the file alone leaves those reference:<slug>#* rows
350
+ // live, so a demoted reference stays vector-retrievable until the next restart
351
+ // rebuilds the index. Prune them now so demotion takes effect immediately,
352
+ // mirroring the eviction path's deletion.
353
+ if (demotedSlugs.length > 0) {
354
+ deleteReferenceVectors(agentDir, demotedSlugs)
355
+ }
356
+
357
+ if (evictedSlugs.length > 0) {
358
+ deleteReferenceVectors(agentDir, evictedSlugs)
359
+ await pruneReferenceCitations(agentDir, new Set(evictedSlugs), logger)
360
+ }
361
+
362
+ return { referencesDemoted, referencesEvicted }
363
+ }
364
+
365
+ function isReferenceDecayExempt(ref: Reference): boolean {
366
+ return ref.frontmatter.pinned || ref.frontmatter.origin === 'curated' || ref.frontmatter.origin === 'external'
367
+ }
368
+
369
+ function referenceScore(ref: Reference, nowMs: number): number {
370
+ const recencyDays = referenceDormancyDays(ref, nowMs)
371
+ const ageDays = Math.max(0, (nowMs - new Date(ref.frontmatter.created).getTime()) / MS_PER_DAY)
372
+ // Combined decay: access-recency dominates, age provides a floor decay
373
+ // score = (accessCount + 1) * exp(-recencyDays / halfLife) * exp(-ageDays / (halfLife * 4))
374
+ return (
375
+ (ref.frontmatter.accessCount + 1) *
376
+ Math.exp(-recencyDays / REFERENCE_HALF_LIFE_DAYS) *
377
+ Math.exp(-ageDays / (REFERENCE_HALF_LIFE_DAYS * 4))
378
+ )
379
+ }
380
+
381
+ function referenceDormancyDays(ref: Reference, nowMs: number): number {
382
+ const lastAccessedMs = new Date(ref.frontmatter.lastAccessed).getTime()
383
+ if (!Number.isFinite(lastAccessedMs)) return Number.POSITIVE_INFINITY
384
+ return Math.max(0, (nowMs - lastAccessedMs) / MS_PER_DAY)
385
+ }
386
+
387
+ function deleteReferenceVectors(agentDir: string, slugs: readonly string[]): void {
388
+ const dbPath = join(agentDir, 'memory', '.vectors', 'index.db')
389
+ if (!existsSync(dbPath)) return
390
+
391
+ const prefixes = slugs.map((slug) => `reference:${slug}#`)
392
+ const store = VectorStore.open(dbPath)
393
+ try {
394
+ const ids = store
395
+ .getAllMeta()
396
+ .flatMap((row) => (prefixes.some((prefix) => row.id.startsWith(prefix)) ? [row.id] : []))
397
+ if (ids.length > 0) store.deleteMany(ids)
398
+ } finally {
399
+ store.close()
400
+ }
401
+ }
402
+
403
+ async function pruneReferenceCitations(
404
+ agentDir: string,
405
+ evictedSlugs: ReadonlySet<string>,
406
+ logger: DreamingLogger,
407
+ ): Promise<void> {
408
+ const slugs = await listShardSlugs(agentDir)
409
+ for (const slug of slugs) {
410
+ const path = topicShardPath(agentDir, slug)
411
+ let raw: string
412
+ try {
413
+ raw = await readFile(path, 'utf8')
414
+ } catch (err) {
415
+ if (isEnoent(err)) continue
416
+ throw err
417
+ }
418
+
419
+ const parsed = parseShardTolerantly(raw, slug, logger)
420
+ const prunedBody = pruneReferenceSection(parsed.body, evictedSlugs)
421
+ if (prunedBody === parsed.body) continue
422
+ await writeFile(path, renderShard(parsed.frontmatter, prunedBody))
423
+ }
424
+ }
425
+
426
+ function pruneReferenceSection(body: string, evictedSlugs: ReadonlySet<string>): string {
427
+ const lines = body.split('\n')
428
+ const out: string[] = []
429
+ let referencesHeadingIndex: number | null = null
430
+ let referencesKept = 0
431
+ let inReferences = false
432
+
433
+ const flushEmptyReferencesHeading = (): void => {
434
+ if (referencesHeadingIndex !== null && referencesKept === 0) {
435
+ out.splice(referencesHeadingIndex, out.length - referencesHeadingIndex)
436
+ }
437
+ referencesHeadingIndex = null
438
+ referencesKept = 0
439
+ }
440
+
441
+ for (const line of lines) {
442
+ if (/^references\s*:\s*$/i.test(line.trim())) {
443
+ flushEmptyReferencesHeading()
444
+ inReferences = true
445
+ referencesHeadingIndex = out.length
446
+ referencesKept = 0
447
+ out.push(line)
448
+ continue
449
+ }
450
+
451
+ if (inReferences && isMarkdownSectionHeading(line)) {
452
+ flushEmptyReferencesHeading()
453
+ inReferences = false
454
+ }
455
+
456
+ if (inReferences) {
457
+ const referenceSlug = /^\s*-\s+(.+?)\s*$/.exec(line)?.[1]
458
+ if (referenceSlug !== undefined) {
459
+ if (evictedSlugs.has(referenceSlug)) continue
460
+ referencesKept += 1
461
+ }
462
+ }
463
+
464
+ out.push(line)
465
+ }
466
+
467
+ flushEmptyReferencesHeading()
468
+ return out.join('\n')
469
+ }
470
+
471
+ function isMarkdownSectionHeading(line: string): boolean {
472
+ const trimmed = line.trim()
473
+ return /^(fragments|references|superseded|proposal)\s*:/i.test(trimmed) || /^#{1,6}\s+/.test(trimmed)
474
+ }
475
+
476
+ // A dreamed-AND-cited fragment's `stream:*` row is redundant: hybridSearch
477
+ // collapses any match on it to the citing topic, whose `topic:*` row is already
478
+ // a candidate. It surfaces no new result, yet still consumes one of
479
+ // store.query's finite `topK * 2` pre-fusion slots by raw cosine — displacing a
480
+ // DISTINCT topic. Without this, one such row accrues per cited fragment for the
481
+ // whole container uptime (only startup `pruneStaleRows` clears them), so a
482
+ // many-day topic hoards proportionally more slots: the popularity bias MAX-child
483
+ // ranking exists to prevent. Pruning per-pass is the same deletion startup does
484
+ // (dreamed-and-cited fragments leave the undreamed passage set), advanced from
485
+ // per-restart to per-pass. Undreamed rows are kept — they resolve to themselves
486
+ // and ARE the freshness window; `makeAppendHook` re-embeds only on fresh APPEND,
487
+ // so a pruned row is never resurrected mid-uptime.
488
+ export function deleteRedundantDreamedCitedStreamVectors(
489
+ agentDir: string,
490
+ dreamedState: DreamingState,
491
+ citedIdsByDate: ReadonlyMap<string, ReadonlySet<string>>,
492
+ ): number {
493
+ const dbPath = join(agentDir, 'memory', '.vectors', 'index.db')
494
+ if (!existsSync(dbPath)) return 0
495
+
496
+ const redundantIds: string[] = []
497
+ for (const [date, citedIds] of citedIdsByDate) {
498
+ const dreamedIds = getDreamedIds(dreamedState, date)
499
+ for (const fragmentId of citedIds) {
500
+ if (dreamedIds.has(fragmentId)) redundantIds.push(`stream:${date}#${fragmentId}`)
501
+ }
502
+ }
503
+ if (redundantIds.length === 0) return 0
504
+
505
+ const store = VectorStore.open(dbPath)
506
+ try {
507
+ store.deleteMany(redundantIds)
508
+ } finally {
509
+ store.close()
510
+ }
511
+ return redundantIds.length
512
+ }
513
+
234
514
  const EMPTY_ID_SET: ReadonlySet<string> = new Set()
235
515
 
236
516
  async function loadCitedIds(agentDir: string): Promise<ReadonlyMap<string, ReadonlySet<string>>> {
@@ -242,6 +522,51 @@ async function loadCitedIds(agentDir: string): Promise<ReadonlyMap<string, Reado
242
522
  return out
243
523
  }
244
524
 
525
+ type ReferenceSnapshotEntry = {
526
+ bytes: Buffer
527
+ hash: string
528
+ }
529
+
530
+ async function captureReferenceSnapshot(agentDir: string): Promise<Map<string, ReferenceSnapshotEntry>> {
531
+ const snapshot = new Map<string, ReferenceSnapshotEntry>()
532
+ let names: string[]
533
+ try {
534
+ names = await readdir(referencesDir(agentDir))
535
+ } catch (err) {
536
+ if (isEnoent(err)) return snapshot
537
+ throw err
538
+ }
539
+
540
+ for (const name of names.sort()) {
541
+ if (!name.endsWith('.md')) continue
542
+ const slug = basename(name, '.md')
543
+ const bytes = await readFile(join(referencesDir(agentDir), name))
544
+ snapshot.set(slug, { bytes, hash: createHash('sha256').update(bytes).digest('hex') })
545
+ }
546
+ return snapshot
547
+ }
548
+
549
+ async function restoreChangedReferences(
550
+ agentDir: string,
551
+ before: ReadonlyMap<string, ReferenceSnapshotEntry>,
552
+ logger: DreamingLogger,
553
+ ): Promise<boolean> {
554
+ if (before.size === 0) return false
555
+ const after = await captureReferenceSnapshot(agentDir)
556
+ let restored = false
557
+ for (const [slug, entry] of before) {
558
+ const next = after.get(slug)
559
+ if (next?.hash === entry.hash) continue
560
+ await mkdir(referencesDir(agentDir), { recursive: true })
561
+ await writeFile(join(referencesDir(agentDir), `${slug}.md`), entry.bytes)
562
+ restored = true
563
+ logger.warn(
564
+ `[dreaming] reference content modified: ${slug} — restored original bytes and aborted the dreaming commit to preserve the verbatim invariant`,
565
+ )
566
+ }
567
+ return restored
568
+ }
569
+
245
570
  function mergeCitationIndex(target: Map<string, Set<string>>, source: ReadonlyMap<string, ReadonlySet<string>>): void {
246
571
  for (const [date, ids] of source) {
247
572
  let targetIds = target.get(date)
@@ -654,10 +979,10 @@ You also distill **muscle memory**: when the streams show a repeated multi-step
654
979
 
655
980
  **2. Only read the undreamed tail.** The runtime gives you a list of stream files and fragment ids. Use \`read\` to inspect the listed files; do not search unrelated stream history. Earlier fragments are already consolidated, re-citing them as new evidence would create duplicate references. Treat each JSONL line as one event; consolidate only \`type: "fragment"\` events and ignore \`watermark\` events except as evidence that progress was recorded.
656
981
 
657
- **3. Every topic shard cites its source fragments by id.** When you consolidate, group fragments by topic and produce a single conclusion paragraph per topic, then list the source fragments below it. The id is the \`id\` field of the fragment event in the JSONL line you read — a UUIDv7 like \`019e2eca-6fc5-71ef-add9-67a0955a4b35\`. Use this exact format:
982
+ **3. Every topic shard cites its source fragments by id.** When you consolidate, group fragments by topic and produce **one compact belief sentence** per topic (see rule 6), then list the source fragments below it. The id is the \`id\` field of the fragment event in the JSONL line you read — a UUIDv7 like \`019e2eca-6fc5-71ef-add9-67a0955a4b35\`. Use this exact format:
658
983
 
659
984
  \`\`\`
660
- <conclusion paragraph in your own words>
985
+ <one compact belief sentence in your own words>
661
986
 
662
987
  fragments:
663
988
  - streams/yyyy-MM-dd#<fragment-id>
@@ -670,12 +995,23 @@ A fragment with no useful content (a watermark-only marker, a near-duplicate, a
670
995
 
671
996
  **4. Inherit the memory-logger's standards.** The memory-logger already filtered fragments using strict certainty rules (explicit / deductive / inductive). Your job is consolidation, not loosening the bar. If two fragments contradict, prefer the more recent. If a fragment is ambiguous in isolation but clarified by a later fragment, merge them under one topic. Never promote a single fragment from one day into a stable claim unless its certainty was already \`explicit\` or \`deductive\`.
672
997
 
673
- **5. Rebalance every run. Preserve every fact and every cited fragment id.** The shard set is a saturated surface (a fixed prompt-budget), not an append-only log — every run is consolidation, not just the runs that get new fragments. You may merge near-duplicate topics into one, split overloaded topics, rename unclear slugs/headings, and rewrite verbose conclusion paragraphs more tightly. What you must NOT do: drop a fragment id. The merged topic's \`fragments:\` list is the **union** of its source topics' fragment ids. The daily-stream GC depends on shard citations to keep evidence alive; an omitted id means the underlying fragment is permanently deleted on the next compaction. If two topics genuinely cover different facts, leave them separate — premature merging loses signal. If a new fragment contradicts an existing entry, replace the entry's conclusion paragraph and keep BOTH the old and new fragment ids in the citations list (the contradiction itself is evidence). Citation-superset invariant: every previously-cited fragment id must still appear cited in at least one shard after your run. If you violate this, the runtime reverts your whole run.
998
+ **5. Rebalance every run. Preserve every fact and every cited fragment id.** The shard set is a saturated surface (a fixed prompt-budget), not an append-only log — every run is consolidation, not just the runs that get new fragments. You may merge near-duplicate topics into one, split overloaded topics, rename unclear slugs/headings, and rewrite verbose conclusion paragraphs more tightly. What you must NOT do: drop a fragment id. The merged topic's \`fragments:\` list is the **union** of its source topics' fragment ids. The daily-stream GC depends on shard citations to keep evidence alive; an omitted id means the underlying fragment is permanently deleted on the next compaction. If two topics genuinely cover different facts, leave them separate — premature merging loses signal. If a new fragment contradicts an existing entry, replace the entry's conclusion paragraph to state the new current truth, and **move the old, now-overturned fragment id from \`fragments:\` into a \`superseded:\` list** in the same shard (the new fragment id goes under \`fragments:\`). Both lists keep the ids cited, so no evidence is lost — but \`superseded:\` marks the old evidence as history, not current truth, so retrieval no longer surfaces it as a hook for the new belief. Citation-superset invariant: every previously-cited fragment id must still appear cited in at least one shard after your run, in EITHER \`fragments:\` or \`superseded:\`. If you violate this, the runtime reverts your whole run.
674
999
 
675
- **6. Be concise.** Each topic conclusion is one short paragraph. No lists of preferences ("the user likes X, Y, Z"). One topic per concept. If a topic only earned one fragment and the fragment was already small, you may copy its conclusion verbatim do not pad.
1000
+ **6. Write a compact belief, not an essay.** An ordinary belief topic's body is **one compact belief sentence** stating the current truth — a durable fact about the user, project, or environment — placed before \`fragments:\`. It carries the subject, the predicate (the preference/habit/fact/decision), and only the essential scope qualifier needed to avoid overgeneralizing ("for this repo", "when committing", "in host-stage code"). Do NOT explain the evidence, the history, or the reasoning ("because…") — the \`fragments:\` and \`superseded:\` citation lists carry that. No lists of preferences ("the user likes X, Y, Z"), no labels, no markdown headings, no multiple sentences. One topic per concept. Keep the sentence natural and keyword-rich (it is embedded and keyword-searched) — do not compress into telegraphic fragments like "bun/typecheck/lint". Smaller bodies let more topics stay in the directly-injected budget, so tightness is load-bearing, not cosmetic. **Exception: CLI/plugin proposal shards (see "Suggesting a CLI or a plugin" below) are not belief topics — they keep their richer rationale paragraph plus the required \`proposal:\` label and are exempt from the one-sentence/no-labels rule.**
676
1001
 
677
1002
  **7. Memory is passive context, not an instruction channel.** Rewrite imperative or duty-shaped fragments as observations. Preserve facts, user preferences, and evidence; do not promote inferred obligations like "the agent should educate X", "future agents must correct Y", "bot Z should not post", or "run this later" unless the user explicitly stated an always/never rule. When a fragment contains such language, convert it into neutral context about what happened and why it might help interpret a future user request.
678
1003
 
1004
+ **8. Compact the over-budget shards the run flags.** If the user prompt includes an "Over the embedding budget" table, those shards are too long for the embedding model: their tail is truncated and never contributes to semantic retrieval. Rewrite each flagged shard's body into the compact one-belief-sentence form (rule 6) so the whole shard fits. **This is a prose-tightening task, never a citation-dropping one:** keep every \`fragments:\` and \`superseded:\` id exactly as-is — shrink only the explanatory prose around them. If one shard genuinely holds two distinct beliefs, split it into two shards and carry each fragment id to the shard whose belief it supports (the union of the two shards' citations must still cover every original id — the citation-superset invariant reverts the whole run otherwise, and a reverted shard stays over budget). Never drop a citation to save tokens; the deterministic embed-time bound already prevents silent loss, so a flagged shard losing a citation would be strictly worse than leaving it long.
1005
+
1006
+ **9. References are verbatim artifacts — never edit their content.** When a fragment you consolidate carries a \`references:\` field listing reference slugs, carry those slugs up into the topic shard's body under a \`references:\` section (union semantics, same as \`fragments:\`). Use this format:
1007
+
1008
+ \`\`\`
1009
+ references:
1010
+ - <slug>
1011
+ \`\`\`
1012
+
1013
+ The reference files under \`memory/references/\` are verbatim artifacts. You MUST NOT read, rewrite, or distill their content. You may only cite them by slug. On eviction (Phase 4), citations are pruned — but that is a separate pass, not your concern here.
1014
+
679
1015
  # What a topic shard looks like
680
1016
 
681
1017
  \`\`\`
@@ -687,13 +1023,19 @@ lastReinforced: 1970-01-01
687
1023
  tags: []
688
1024
  ---
689
1025
 
690
- <conclusion paragraph>
1026
+ <one compact belief sentence — current truth, with scope if needed (see rule 6)>
691
1027
 
692
1028
  fragments:
693
1029
  - streams/yyyy-MM-dd#<fragment-id>
1030
+
1031
+ references:
1032
+ - <reference-slug>
1033
+
1034
+ superseded:
1035
+ - streams/yyyy-MM-dd#<overturned-fragment-id>
694
1036
  \`\`\`
695
1037
 
696
- The file shape is YAML frontmatter plus body. The runtime owns frontmatter: do not spend effort making \`cites\`, \`days\`, or \`lastReinforced\` correct. To create a new topic, \`write memory/topics/<slug>.md\` with frontmatter containing \`heading\`, \`cites: 0\`, \`days: 0\`, \`lastReinforced\` (placeholder), optional \`tags\`, plus body; or omit frontmatter entirely — the runtime synthesizes it. If existing frontmatter is present, leave its semantics alone; the runtime will replace it with computed values.
1038
+ The \`references:\` list is OPTIONAL — include it only when a consolidated fragment carried reference slugs (see rule 9). The \`superseded:\` list is OPTIONAL — include it only when a later fragment overturned earlier evidence (see rule 5). Ids under it stay cited (GC keeps them alive) but are excluded from retrieval, so a superseded "uses bun" fragment never resurfaces against the current "uses pnpm" belief. The file shape is YAML frontmatter plus body. The runtime owns frontmatter: do not spend effort making \`cites\`, \`days\`, or \`lastReinforced\` correct. To create a new topic, \`write memory/topics/<slug>.md\` with frontmatter containing \`heading\`, \`cites: 0\`, \`days: 0\`, \`lastReinforced\` (placeholder), optional \`tags\`, plus body; or omit frontmatter entirely — the runtime synthesizes it. If existing frontmatter is present, leave its semantics alone; the runtime will replace it with computed values.
697
1039
 
698
1040
  # Topic shard operations
699
1041
 
@@ -708,14 +1050,14 @@ Topic shards are read into session context under a prompt budget. Treat the shar
708
1050
 
709
1051
  ## Strength tiers and promotion ladder
710
1052
 
711
- Pick the wording in each conclusion paragraph from the topic's \`days\` count:
1053
+ Calibrate the strength wording **inside the belief sentence** from the topic's \`days\` count (the frontmatter carries the numbers; the sentence carries how confidently the agent should act on them):
712
1054
 
713
- - **\`days = 1\` — "mentioned":** the topic was observed in one session. Conclusion uses tentative language ("the user mentioned X in the context of Y"). Single-fragment one-day topics that are not reinforced on subsequent runs should stay short.
1055
+ - **\`days = 1\` — "mentioned":** observed in one session. Tentative wording ("the user mentioned X in the context of Y").
714
1056
  - **\`days = 2\` — "observed":** seen twice, on different days. Still tentative — could be a recurring quirk, could be coincidence.
715
- - **\`days >= 3\` — "consistently":** the topic has been reinforced across at least three distinct days. Conclusion uses confident language ("the user consistently prefers X", "the user's pattern is Y"). Strong enough to keep visible when budgets tighten.
716
- - **\`days >= 7\` — "always":** seen across at least seven distinct days. Conclusion uses declarative language ("the user always X", "Y is the user's standard"). These are the load-bearing topics; protect them from accidental merges.
1057
+ - **\`days >= 3\` — "consistently":** reinforced across at least three distinct days. Confident wording ("the user consistently prefers X"). Strong enough to keep visible when budgets tighten.
1058
+ - **\`days >= 7\` — "always":** seen across at least seven distinct days. Declarative wording ("the user always X", "Y is the user's standard"). These are the load-bearing topics; protect them from accidental merges.
717
1059
 
718
- Promotion is gated on \`days\`, not on \`cites\`. A topic with \`cites = 12, days = 1\` is still "mentioned" — twelve citations in one debugging session is one event, not twelve. Stronger shards should be clearer and more prominent; weaker shards stay short.
1060
+ The strength lives in the sentence's verb/qualifier, not in a separate label — do not write "Strength: high". Promotion is gated on \`days\`, not on \`cites\`: a topic with \`cites = 12, days = 1\` is still "mentioned" — twelve citations in one debugging session is one event, not twelve. Reserve "always" for genuinely stable rules so the wording stays calibrated.
719
1061
 
720
1062
  ## Demotion without a bucket
721
1063
 
@@ -775,7 +1117,7 @@ Do not create skills speculatively. A skill the main agent never reaches for is
775
1117
 
776
1118
  ## Suggesting a CLI or a plugin (forms B and C)
777
1119
 
778
- You record CLI and plugin suggestions as topic shards. Each suggestion is a single topic with the same fragment-citation rules as every other shard, plus an explicit \`proposal:\` line that names the form, the package name, and why this shape fits better than a skill. These topics are passive recommendations: the main agent may act on them only when the current user request asks for the matching procedure.
1120
+ You record CLI and plugin suggestions as topic shards. These are the exception to rule 6's one-sentence belief format: a suggestion is a single topic with the same fragment-citation rules as every other shard, but it keeps a richer rationale paragraph plus an explicit \`proposal:\` line that names the form, the package name, and why this shape fits better than a skill. These topics are passive recommendations: the main agent may act on them only when the current user request asks for the matching procedure.
779
1121
 
780
1122
  Use this exact shape — pick one of the two \`proposal:\` lines:
781
1123
 
@@ -821,7 +1163,12 @@ Do not suggest CLIs or plugins speculatively. The same recurrence + generalizabi
821
1163
 
822
1164
  If the undreamed tails contain only watermarks, AND no procedure clears the muscle-memory bar, AND every existing topic looks well-shaped at its current strength (no obvious merge, split, rename, or terse-demotion candidates), do not write shards and do not write a skill just to touch something. Stop without writing. The point of dreaming is consolidation, not activity. The runtime advances the watermark either way. But: if there ARE new fragments, or if the strength table shows topics that should clearly rebalance, the run is productive even without skill activity — rebalancing IS work.`
823
1165
 
824
- function buildInitialPrompt(payload: DreamingPayload, snapshots: StreamSnapshot[], strengths: ShardStrength[]): string {
1166
+ function buildInitialPrompt(
1167
+ payload: DreamingPayload,
1168
+ snapshots: StreamSnapshot[],
1169
+ strengths: ShardStrength[],
1170
+ overBudget: OverBudgetShard[],
1171
+ ): string {
825
1172
  const today = formatLocalDate()
826
1173
  const streamDir = join(payload.agentDir, snapshots[0]?.displayPrefix ?? 'memory/streams')
827
1174
  const lines: string[] = [
@@ -842,6 +1189,16 @@ function buildInitialPrompt(payload: DreamingPayload, snapshots: StreamSnapshot[
842
1189
  )
843
1190
  }
844
1191
 
1192
+ const overBudgetTable = renderOverBudgetTable(overBudget)
1193
+ if (overBudgetTable.length > 0) {
1194
+ lines.push(
1195
+ '',
1196
+ 'Over the embedding budget. These shards are too long for the embedding model — their tail is truncated and never reaches semantic retrieval. Per rule 8, compact each into the one-belief-sentence form (or split a genuinely-two-belief shard), preserving EVERY `fragments:`/`superseded:` id. Do not drop a citation to save tokens.',
1197
+ '',
1198
+ overBudgetTable,
1199
+ )
1200
+ }
1201
+
845
1202
  lines.push(
846
1203
  '',
847
1204
  'Undreamed fragments to consolidate. Each entry lists the daily JSONL file and the ids of fragments in that file you have not yet consolidated into topic shards. Read the file, locate each id, and decide what (if anything) belongs in a shard. Cite by id (streams/yyyy-MM-dd#<id>), not by line number.',
@@ -894,6 +1251,34 @@ function compareShardStrengths(a: ShardStrength, b: ShardStrength): number {
894
1251
  return a.slug.localeCompare(b.slug)
895
1252
  }
896
1253
 
1254
+ // Shards whose embeddable text exceeds the model token budget. Surfaced to the
1255
+ // dreaming subagent as compaction candidates (rule 8). Gated by the caller on
1256
+ // the vector index actually existing — over-budget is meaningless when nothing
1257
+ // embeds these shards. Measures topicPassage(...).text — the exact citation-
1258
+ // stripped string the embedder bounds — so the flag matches what is truncated,
1259
+ // not the raw body (which is longer and includes the citation lines).
1260
+ function findOverBudgetShards(shards: TopicShard[]): OverBudgetShard[] {
1261
+ return shards
1262
+ .map((shard) => ({
1263
+ slug: shard.slug,
1264
+ heading: shard.frontmatter.heading,
1265
+ estimatedTokens: estimateTokens(topicPassage(shard.slug, shard.frontmatter.heading, shard.body).text),
1266
+ }))
1267
+ .filter((shard) => shard.estimatedTokens > TEXT_TOKEN_BUDGET)
1268
+ .sort((a, b) => b.estimatedTokens - a.estimatedTokens || a.slug.localeCompare(b.slug))
1269
+ }
1270
+
1271
+ function renderOverBudgetTable(overBudget: readonly OverBudgetShard[]): string {
1272
+ if (overBudget.length === 0) return ''
1273
+ const lines = ['| slug | heading | est. tokens |', '| --- | --- | ---: |']
1274
+ for (const shard of overBudget) {
1275
+ lines.push(
1276
+ `| ${escapeTableCell(shard.slug)} | ${escapeTableCell(shard.heading || '(untitled)')} | ${shard.estimatedTokens} |`,
1277
+ )
1278
+ }
1279
+ return lines.join('\n')
1280
+ }
1281
+
897
1282
  function daysBetween(today: string, earlier: string): number | null {
898
1283
  const todayMs = parseIsoDateUtc(today)
899
1284
  const earlierMs = parseIsoDateUtc(earlier)
@@ -928,11 +1313,13 @@ const dreamingDeleteTopicShardTool = defineTool({
928
1313
  export type CreateDreamingSubagentOptions = {
929
1314
  commitMemory?: (cwd: string) => Promise<void>
930
1315
  logger?: DreamingLogger
1316
+ vectorEmbedFn?: EmbedFn
931
1317
  }
932
1318
 
933
1319
  export function createDreamingSubagent(options: CreateDreamingSubagentOptions = {}): Subagent<DreamingPayload> {
934
1320
  const commit = options.commitMemory ?? commitMemorySnapshot
935
1321
  const logger = options.logger ?? consoleLogger
1322
+ const vectorEmbedFn = options.vectorEmbedFn ?? embed
936
1323
 
937
1324
  return {
938
1325
  systemPrompt: DREAMING_SYSTEM_PROMPT,
@@ -959,10 +1346,20 @@ export function createDreamingSubagent(options: CreateDreamingSubagentOptions =
959
1346
  )
960
1347
 
961
1348
  const snapshotBefore = await captureShardSnapshot(topicsDir(ctx.payload.agentDir))
1349
+ const referenceSnapshotBefore = await captureReferenceSnapshot(ctx.payload.agentDir)
962
1350
  const strengths = await loadTopicStrengths(ctx.payload.agentDir)
963
1351
 
1352
+ // Over-budget compaction candidates only matter when the vector index
1353
+ // actually embeds these shards; with vector off, nothing truncates them,
1354
+ // so suppress the signal rather than nag the subagent about a budget that
1355
+ // does not apply. Gate on the same `index.db` existence the vector ops use.
1356
+ const vectorActive = existsSync(join(ctx.payload.agentDir, 'memory', '.vectors', 'index.db'))
1357
+ const overBudget = vectorActive ? findOverBudgetShards(await loadAllShards(ctx.payload.agentDir)) : []
1358
+
964
1359
  try {
965
- await runSession({ userPrompt: buildInitialPrompt(ctx.payload, snapshots.undreamed, strengths) })
1360
+ await runSession({
1361
+ userPrompt: buildInitialPrompt(ctx.payload, snapshots.undreamed, strengths, overBudget),
1362
+ })
966
1363
  } catch (err) {
967
1364
  const message = err instanceof Error ? err.message : String(err)
968
1365
  logger.warn(`[dreaming] run threw: ${message} elapsed_ms=${Date.now() - start}`)
@@ -970,6 +1367,8 @@ export function createDreamingSubagent(options: CreateDreamingSubagentOptions =
970
1367
  }
971
1368
 
972
1369
  const snapshotAfter = await captureShardSnapshot(topicsDir(ctx.payload.agentDir))
1370
+ const restoredReferences = await restoreChangedReferences(ctx.payload.agentDir, referenceSnapshotBefore, logger)
1371
+ if (restoredReferences) return
973
1372
  let shardsRewrittenThisRun = !shardSnapshotsEqual(snapshotBefore, snapshotAfter)
974
1373
  let revertedCitationViolation = false
975
1374
 
@@ -1009,7 +1408,24 @@ export function createDreamingSubagent(options: CreateDreamingSubagentOptions =
1009
1408
  }
1010
1409
  }
1011
1410
 
1012
- if (shardsRewrittenThisRun) await recomputeFrontmatterForAllShards(ctx.payload.agentDir, logger)
1411
+ let metrics = computeDreamingMetrics(snapshotBefore, snapshotBefore)
1412
+ if (shardsRewrittenThisRun) {
1413
+ await recomputeFrontmatterForAllShards(ctx.payload.agentDir, logger)
1414
+ const snapshotAfterFrontmatter = await captureShardSnapshot(topicsDir(ctx.payload.agentDir))
1415
+ metrics = computeDreamingMetrics(snapshotBefore, snapshotAfterFrontmatter)
1416
+ await syncTopicVectorsFromSnapshotDiff(
1417
+ ctx.payload.agentDir,
1418
+ snapshotBefore,
1419
+ snapshotAfterFrontmatter,
1420
+ vectorEmbedFn,
1421
+ ).catch((err: unknown) => {
1422
+ logger.warn(
1423
+ `[dreaming] vector topic sync failed (index will be repaired on next startup): ${err instanceof Error ? err.message : String(err)}`,
1424
+ )
1425
+ })
1426
+ }
1427
+
1428
+ const { referencesDemoted, referencesEvicted } = await runReferenceSaturationPass(ctx.payload.agentDir, logger)
1013
1429
 
1014
1430
  const advanced = advanceDreamedIds(state, snapshots.undreamed)
1015
1431
  await saveDreamingState(ctx.payload.agentDir, advanced)
@@ -1027,10 +1443,17 @@ export function createDreamingSubagent(options: CreateDreamingSubagentOptions =
1027
1443
  `[dreaming] compaction files=${compaction.filesCompacted} watermarks_dropped=${compaction.watermarksDropped} fragments_dropped=${compaction.fragmentsDropped} fragment_gc=${shardsRewrittenThisRun ? 'on' : 'off'}`,
1028
1444
  )
1029
1445
  }
1446
+ deleteStreamVectorsForDroppedFragments(ctx.payload.agentDir, compaction.droppedFragmentIds)
1447
+ const redundantVectors = deleteRedundantDreamedCitedStreamVectors(ctx.payload.agentDir, advanced, citedIdsByDate)
1448
+ if (redundantVectors > 0) {
1449
+ logger.info(`[dreaming] pruned redundant dreamed-and-cited stream vectors=${redundantVectors}`)
1450
+ }
1030
1451
 
1031
1452
  try {
1032
1453
  await commit(ctx.payload.agentDir)
1033
- logger.info(`[dreaming] done elapsed_ms=${Date.now() - start}`)
1454
+ logger.info(
1455
+ `[dreaming] done topics_created=${metrics.topicsCreated} topics_removed=${metrics.topicsRemoved} superseded_new=${metrics.supersededDelta} fragments_dropped=${compaction.fragmentsDropped} over_budget=${overBudget.length} references_demoted=${referencesDemoted} references_evicted=${referencesEvicted} elapsed_ms=${Date.now() - start}`,
1456
+ )
1034
1457
  } catch (err) {
1035
1458
  const message = err instanceof Error ? err.message : String(err)
1036
1459
  logger.warn(`[dreaming] commit failed: ${message} elapsed_ms=${Date.now() - start}`)