devchain-cli 0.17.0 → 0.17.2

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 (152) hide show
  1. package/dist/node_modules/@devchain/codebase-overview/tsconfig.tsbuildinfo +1 -1
  2. package/dist/node_modules/@devchain/shared/tsconfig.tsbuildinfo +1 -1
  3. package/dist/server/app.main.module.js +2 -0
  4. package/dist/server/app.main.module.js.map +1 -1
  5. package/dist/server/app.normal.module.js +2 -0
  6. package/dist/server/app.normal.module.js.map +1 -1
  7. package/dist/server/modules/events/catalog/broadcast-registry.js +19 -12
  8. package/dist/server/modules/events/catalog/broadcast-registry.js.map +1 -1
  9. package/dist/server/modules/events/catalog/index.d.ts +23 -3
  10. package/dist/server/modules/events/catalog/session.transcript.updated.d.ts +23 -3
  11. package/dist/server/modules/events/catalog/session.transcript.updated.js +41 -19
  12. package/dist/server/modules/events/catalog/session.transcript.updated.js.map +1 -1
  13. package/dist/server/modules/metrics/controllers/metrics.controller.d.ts +6 -0
  14. package/dist/server/modules/metrics/controllers/metrics.controller.js +38 -0
  15. package/dist/server/modules/metrics/controllers/metrics.controller.js.map +1 -0
  16. package/dist/server/modules/metrics/helpers/byte-accounting.helper.d.ts +17 -0
  17. package/dist/server/modules/metrics/helpers/byte-accounting.helper.js +88 -0
  18. package/dist/server/modules/metrics/helpers/byte-accounting.helper.js.map +1 -0
  19. package/dist/server/modules/metrics/metrics.module.d.ts +2 -0
  20. package/dist/server/modules/metrics/metrics.module.js +23 -0
  21. package/dist/server/modules/metrics/metrics.module.js.map +1 -0
  22. package/dist/server/modules/metrics/services/metrics.service.d.ts +20 -0
  23. package/dist/server/modules/metrics/services/metrics.service.js +241 -0
  24. package/dist/server/modules/metrics/services/metrics.service.js.map +1 -0
  25. package/dist/server/modules/metrics/types/metrics.types.d.ts +100 -0
  26. package/dist/server/modules/metrics/types/metrics.types.js +3 -0
  27. package/dist/server/modules/metrics/types/metrics.types.js.map +1 -0
  28. package/dist/server/modules/projects/helpers/profile-mapping.helpers.d.ts +5 -3
  29. package/dist/server/modules/projects/helpers/profile-mapping.helpers.js +25 -13
  30. package/dist/server/modules/projects/helpers/profile-mapping.helpers.js.map +1 -1
  31. package/dist/server/modules/projects/helpers/project-import.js +9 -7
  32. package/dist/server/modules/projects/helpers/project-import.js.map +1 -1
  33. package/dist/server/modules/projects/helpers/template-loader.js +11 -9
  34. package/dist/server/modules/projects/helpers/template-loader.js.map +1 -1
  35. package/dist/server/modules/projects/template-codec/codec-registry.js.map +1 -1
  36. package/dist/server/modules/projects/template-codec/codecs/agents.codec.d.ts +1 -1
  37. package/dist/server/modules/projects/template-codec/codecs/agents.codec.js +4 -4
  38. package/dist/server/modules/projects/template-codec/codecs/agents.codec.js.map +1 -1
  39. package/dist/server/modules/projects/template-codec/codecs/profiles.codec.d.ts +1 -1
  40. package/dist/server/modules/projects/template-codec/codecs/profiles.codec.js +27 -8
  41. package/dist/server/modules/projects/template-codec/codecs/profiles.codec.js.map +1 -1
  42. package/dist/server/modules/projects/template-codec/codecs/watchers.codec.js +1 -1
  43. package/dist/server/modules/projects/template-codec/codecs/watchers.codec.js.map +1 -1
  44. package/dist/server/modules/projects/template-codec/import-context.d.ts +6 -1
  45. package/dist/server/modules/projects/template-codec/import-context.js.map +1 -1
  46. package/dist/server/modules/projects/template-codec/template-section-codec.d.ts +2 -3
  47. package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.d.ts +6 -0
  48. package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.js +66 -0
  49. package/dist/server/modules/session-reader/adapters/antigravity-session-reader.adapter.js.map +1 -1
  50. package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.d.ts +5 -0
  51. package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.js +12 -0
  52. package/dist/server/modules/session-reader/adapters/claude-session-reader.adapter.js.map +1 -1
  53. package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.d.ts +5 -0
  54. package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.js +12 -0
  55. package/dist/server/modules/session-reader/adapters/codex-session-reader.adapter.js.map +1 -1
  56. package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.d.ts +5 -0
  57. package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.js +12 -0
  58. package/dist/server/modules/session-reader/adapters/copilot-session-reader.adapter.js.map +1 -1
  59. package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.d.ts +5 -0
  60. package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.js +15 -0
  61. package/dist/server/modules/session-reader/adapters/opencode-session-reader.adapter.js.map +1 -1
  62. package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.d.ts +10 -0
  63. package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.js +28 -1
  64. package/dist/server/modules/session-reader/adapters/session-reader-adapter.interface.js.map +1 -1
  65. package/dist/server/modules/session-reader/controllers/session-reader.controller.d.ts +10 -4
  66. package/dist/server/modules/session-reader/controllers/session-reader.controller.js +18 -19
  67. package/dist/server/modules/session-reader/controllers/session-reader.controller.js.map +1 -1
  68. package/dist/server/modules/session-reader/data/pricing.json +206 -0
  69. package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.d.ts +1 -0
  70. package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.js +7 -3
  71. package/dist/server/modules/session-reader/parsers/claude-jsonl.parser.js.map +1 -1
  72. package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.d.ts +1 -0
  73. package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.js +22 -11
  74. package/dist/server/modules/session-reader/parsers/codex-jsonl.parser.js.map +1 -1
  75. package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.d.ts +1 -0
  76. package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.js +21 -9
  77. package/dist/server/modules/session-reader/parsers/copilot-jsonl.parser.js.map +1 -1
  78. package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.d.ts +5 -1
  79. package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.js +102 -0
  80. package/dist/server/modules/session-reader/readers/opencode-sqlite.reader.js.map +1 -1
  81. package/dist/server/modules/session-reader/services/session-cache.service.d.ts +71 -4
  82. package/dist/server/modules/session-reader/services/session-cache.service.js +427 -68
  83. package/dist/server/modules/session-reader/services/session-cache.service.js.map +1 -1
  84. package/dist/server/modules/session-reader/services/session-reader.service.d.ts +21 -4
  85. package/dist/server/modules/session-reader/services/session-reader.service.js +126 -40
  86. package/dist/server/modules/session-reader/services/session-reader.service.js.map +1 -1
  87. package/dist/server/modules/session-reader/services/transcript-cursor.d.ts +1 -1
  88. package/dist/server/modules/session-reader/services/transcript-cursor.js +2 -2
  89. package/dist/server/modules/session-reader/services/transcript-cursor.js.map +1 -1
  90. package/dist/server/modules/session-reader/services/transcript-watcher.service.d.ts +2 -0
  91. package/dist/server/modules/session-reader/services/transcript-watcher.service.js +61 -11
  92. package/dist/server/modules/session-reader/services/transcript-watcher.service.js.map +1 -1
  93. package/dist/server/modules/session-reader/session-reader.module.js +3 -1
  94. package/dist/server/modules/session-reader/session-reader.module.js.map +1 -1
  95. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.d.ts +4 -4
  96. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js +11 -5
  97. package/dist/server/modules/sessions/services/session-runtime/__test-utils__/pipeline-harness.js.map +1 -1
  98. package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.d.ts +3 -1
  99. package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.js +11 -2
  100. package/dist/server/modules/sessions/services/session-runtime/session-restore-pipeline.service.js.map +1 -1
  101. package/dist/server/modules/terminal/dtos/ws-envelope.dto.d.ts +140 -1
  102. package/dist/server/modules/terminal/dtos/ws-envelope.dto.js +59 -2
  103. package/dist/server/modules/terminal/dtos/ws-envelope.dto.js.map +1 -1
  104. package/dist/server/modules/terminal/gateways/terminal.gateway.d.ts +33 -1
  105. package/dist/server/modules/terminal/gateways/terminal.gateway.js +471 -61
  106. package/dist/server/modules/terminal/gateways/terminal.gateway.js.map +1 -1
  107. package/dist/server/modules/terminal/services/process-executor/fake-process-executor.d.ts +1 -0
  108. package/dist/server/modules/terminal/services/process-executor/fake-process-executor.js +1 -0
  109. package/dist/server/modules/terminal/services/process-executor/fake-process-executor.js.map +1 -1
  110. package/dist/server/modules/terminal/services/pty.service.d.ts +8 -3
  111. package/dist/server/modules/terminal/services/pty.service.js +11 -2
  112. package/dist/server/modules/terminal/services/pty.service.js.map +1 -1
  113. package/dist/server/modules/terminal/services/terminal-io/capture.js +2 -0
  114. package/dist/server/modules/terminal/services/terminal-io/capture.js.map +1 -1
  115. package/dist/server/modules/terminal/services/terminal-io/delivery.d.ts +1 -0
  116. package/dist/server/modules/terminal/services/terminal-io/delivery.js.map +1 -1
  117. package/dist/server/modules/terminal/services/terminal-io/terminal-io.service.js +28 -1
  118. package/dist/server/modules/terminal/services/terminal-io/terminal-io.service.js.map +1 -1
  119. package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.d.ts +6 -0
  120. package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.js +26 -1
  121. package/dist/server/modules/terminal/services/terminal-key-input/terminal-key-input.facade.js.map +1 -1
  122. package/dist/server/modules/terminal/services/terminal-seed.service.d.ts +27 -3
  123. package/dist/server/modules/terminal/services/terminal-seed.service.js +75 -19
  124. package/dist/server/modules/terminal/services/terminal-seed.service.js.map +1 -1
  125. package/dist/server/modules/terminal/services/terminal-send-scheduler.service.d.ts +73 -0
  126. package/dist/server/modules/terminal/services/terminal-send-scheduler.service.js +387 -0
  127. package/dist/server/modules/terminal/services/terminal-send-scheduler.service.js.map +1 -0
  128. package/dist/server/modules/terminal/services/terminal-session/terminal-session.d.ts +4 -10
  129. package/dist/server/modules/terminal/services/terminal-session/terminal-session.js +20 -48
  130. package/dist/server/modules/terminal/services/terminal-session/terminal-session.js.map +1 -1
  131. package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.d.ts +10 -0
  132. package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.js +75 -0
  133. package/dist/server/modules/terminal/services/terminal-socket-drain.adapter.js.map +1 -0
  134. package/dist/server/modules/terminal/services/terminal-stream.service.d.ts +53 -6
  135. package/dist/server/modules/terminal/services/terminal-stream.service.js +190 -35
  136. package/dist/server/modules/terminal/services/terminal-stream.service.js.map +1 -1
  137. package/dist/server/modules/terminal/terminal.module.js +7 -0
  138. package/dist/server/modules/terminal/terminal.module.js.map +1 -1
  139. package/dist/server/templates/3-agents-dev.json +97 -93
  140. package/dist/server/templates/teams-dev.json +174 -160
  141. package/dist/server/tsconfig.tsbuildinfo +1 -1
  142. package/dist/server/ui/assets/{ReviewDetailPage-Dd40lXMW.js → ReviewDetailPage-C5BI5FSx.js} +1 -1
  143. package/dist/server/ui/assets/{ReviewsPage-CBSbsimJ.js → ReviewsPage-C4V7MJQc.js} +1 -1
  144. package/dist/server/ui/assets/index-BSx7QxmM.css +32 -0
  145. package/dist/server/ui/assets/index-nwIXVn_B.js +1142 -0
  146. package/dist/server/ui/assets/{useReviewSubscription-CpDyx3vJ.js → useReviewSubscription-Bd1R13V5.js} +1 -1
  147. package/dist/server/ui/index.html +2 -2
  148. package/dist/templates/3-agents-dev.json +97 -93
  149. package/dist/templates/teams-dev.json +174 -160
  150. package/package.json +16 -5
  151. package/dist/server/ui/assets/index-COvyD_66.css +0 -32
  152. package/dist/server/ui/assets/index-DtIjl-Lt.js +0 -1113
@@ -38,76 +38,228 @@ var __importStar = (this && this.__importStar) || (function () {
38
38
  return result;
39
39
  };
40
40
  })();
41
+ var __metadata = (this && this.__metadata) || function (k, v) {
42
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
43
+ };
44
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
45
+ return function (target, key) { decorator(target, key, paramIndex); }
46
+ };
41
47
  var SessionCacheService_1;
42
48
  Object.defineProperty(exports, "__esModule", { value: true });
43
- exports.SessionCacheService = void 0;
49
+ exports.SessionCacheService = exports.TRANSCRIPT_CACHE_CONFIG = void 0;
50
+ exports.getTranscriptCacheConfig = getTranscriptCacheConfig;
44
51
  const common_1 = require("@nestjs/common");
52
+ const node_crypto_1 = require("node:crypto");
45
53
  const fs = __importStar(require("node:fs/promises"));
46
54
  const estimate_content_tokens_1 = require("../adapters/utils/estimate-content-tokens");
47
55
  const tool_result_fold_1 = require("../adapters/utils/tool-result-fold");
48
56
  const coalesce_turns_1 = require("../adapters/utils/coalesce-turns");
49
- const CACHE_TTL_MS = 10 * 60 * 1_000;
50
- const CACHE_MAX_ENTRIES = 20;
57
+ const metrics_service_1 = require("../../metrics/services/metrics.service");
58
+ const DEFAULT_CACHE_IDLE_TTL_MS = 10 * 60 * 1_000;
59
+ const DEFAULT_CACHE_SWEEP_INTERVAL_MS = 60_000;
60
+ const DEFAULT_CACHE_BUDGET_BYTES = 64 * 1024 * 1024;
61
+ const SAFE_INTEGER_LOW_BITS = 0x20_0000;
62
+ const FILE_HASH_CHUNK_BYTES = 64 * 1024;
63
+ function fileSourceVersion(stat) {
64
+ const digest = (0, node_crypto_1.createHash)('sha256')
65
+ .update(`${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeMs}`)
66
+ .digest();
67
+ return digest.readUInt32BE(0) * SAFE_INTEGER_LOW_BITS + (digest.readUInt32BE(4) >>> 11);
68
+ }
69
+ exports.TRANSCRIPT_CACHE_CONFIG = Symbol('TRANSCRIPT_CACHE_CONFIG');
70
+ function getTranscriptCacheConfig() {
71
+ return {
72
+ budgetBytes: positiveIntegerFromEnv(process.env.TRANSCRIPT_CACHE_MAX_BYTES, DEFAULT_CACHE_BUDGET_BYTES),
73
+ idleTtlMs: positiveIntegerFromEnv(process.env.TRANSCRIPT_CACHE_IDLE_TTL_MS, DEFAULT_CACHE_IDLE_TTL_MS),
74
+ sweepIntervalMs: positiveIntegerFromEnv(process.env.TRANSCRIPT_CACHE_SWEEP_INTERVAL_MS, DEFAULT_CACHE_SWEEP_INTERVAL_MS),
75
+ };
76
+ }
51
77
  let SessionCacheService = SessionCacheService_1 = class SessionCacheService {
52
- constructor() {
78
+ constructor(metricsService, config) {
79
+ this.metricsService = metricsService;
53
80
  this.logger = new common_1.Logger(SessionCacheService_1.name);
54
81
  this.cache = new Map();
82
+ this.hits = 0;
83
+ this.misses = 0;
84
+ this.chunksHits = 0;
85
+ this.chunksMisses = 0;
86
+ this.dtoHits = 0;
87
+ this.dtoMisses = 0;
88
+ this.budgetUsedBytes = 0;
89
+ this.evictions = 0;
90
+ this.config = config ?? getTranscriptCacheConfig();
91
+ }
92
+ onModuleInit() {
93
+ this.metricsService.registerCacheStatsProvider('parsed', () => this.getCacheStats(), () => Array.from(this.cache.values(), (entry) => entry.session));
94
+ this.metricsService.registerCacheStatsProvider('chunks', () => this.getChunksCacheStats(), () => this.getChunksRetainedRoots());
95
+ this.idleSweepTimer = setInterval(() => this.sweepIdleEntries(), this.config.sweepIntervalMs);
96
+ this.idleSweepTimer.unref();
55
97
  }
56
98
  onModuleDestroy() {
57
- this.cache.clear();
99
+ if (this.idleSweepTimer) {
100
+ clearInterval(this.idleSweepTimer);
101
+ this.idleSweepTimer = undefined;
102
+ }
103
+ this.clear();
104
+ }
105
+ getCacheStats() {
106
+ const total = this.hits + this.misses;
107
+ return {
108
+ entries: this.cache.size,
109
+ bytesEstimated: 0,
110
+ hits: this.hits,
111
+ misses: this.misses,
112
+ hitRate: total > 0 ? this.hits / total : 0,
113
+ bytesMethod: 'deferred-to-aggregate',
114
+ budgetUsedBytes: this.budgetUsedBytes,
115
+ budgetBytes: this.config.budgetBytes,
116
+ evictions: this.evictions,
117
+ };
118
+ }
119
+ getChunksCacheStats() {
120
+ const total = this.chunksHits + this.chunksMisses;
121
+ let entries = 0;
122
+ for (const entry of this.cache.values()) {
123
+ if (entry.chunks)
124
+ entries += 1;
125
+ }
126
+ return {
127
+ entries,
128
+ bytesEstimated: 0,
129
+ hits: this.chunksHits,
130
+ misses: this.chunksMisses,
131
+ hitRate: total > 0 ? this.chunksHits / total : 0,
132
+ bytesMethod: 'deferred-to-aggregate',
133
+ };
134
+ }
135
+ getDtoCacheStats() {
136
+ let entries = 0;
137
+ let bytesEstimated = 0;
138
+ for (const entry of this.cache.values()) {
139
+ if (!entry.dto)
140
+ continue;
141
+ entries += 1;
142
+ bytesEstimated += entry.dto.responseBytes;
143
+ }
144
+ const total = this.dtoHits + this.dtoMisses;
145
+ return {
146
+ entries,
147
+ bytesEstimated,
148
+ hits: this.dtoHits,
149
+ misses: this.dtoMisses,
150
+ hitRate: total > 0 ? this.dtoHits / total : 0,
151
+ bytesMethod: 'json-stringify-length',
152
+ };
153
+ }
154
+ async getFreshSession(sessionId, source, adapter) {
155
+ const cached = this.cache.get(sessionId);
156
+ if (!cached)
157
+ return undefined;
158
+ const ref = this.toSourceRef(source, adapter);
159
+ const freshness = await this.computeFreshness(ref, adapter);
160
+ return this.takeFreshCachedSession(sessionId, cached, freshness.token, Date.now());
58
161
  }
59
162
  async getOrParse(sessionId, source, adapter) {
163
+ return (await this.getOrParseResult(sessionId, source, adapter)).session;
164
+ }
165
+ async getOrParseWithMeta(sessionId, source, adapter) {
166
+ return this.getOrParseResult(sessionId, source, adapter);
167
+ }
168
+ async getOrParseResult(sessionId, source, adapter) {
60
169
  const now = Date.now();
61
170
  const ref = this.toSourceRef(source, adapter);
62
171
  const threadRef = typeof source !== 'string' ? ref : undefined;
63
- const freshness = await this.computeFreshness(ref, adapter);
172
+ let freshness = await this.computeFreshness(ref, adapter);
64
173
  const cached = this.cache.get(sessionId);
65
- if (cached &&
66
- now - cached.cachedAt < CACHE_TTL_MS &&
67
- JSON.stringify(cached.freshnessToken) === JSON.stringify(freshness.token)) {
68
- this.logger.debug({ sessionId }, 'Session cache hit');
69
- this.touchLru(sessionId, cached);
70
- return cached.session;
174
+ const freshSession = this.takeFreshCachedSession(sessionId, cached, freshness.token, now);
175
+ if (freshSession && cached) {
176
+ return this.toGetOrParseResult(freshSession, cached, true, 'cache-hit');
71
177
  }
178
+ this.misses += 1;
72
179
  let session;
73
180
  let lastOffset;
74
181
  let boundaryFold = false;
75
- if (cached && freshness.size > cached.lastSize) {
182
+ let acceptedFileDigests;
183
+ const sameFileIdentity = ref.kind !== 'file' ||
184
+ (cached?.fileIdentity !== undefined && cached.fileIdentity === freshness.fileIdentity);
185
+ const fileReplaced = cached !== undefined && ref.kind === 'file' && !sameFileIdentity;
186
+ const sameIdentityGrowth = cached !== undefined &&
187
+ ref.kind === 'file' &&
188
+ sameFileIdentity &&
189
+ freshness.size > cached.lastSize;
190
+ const growthDigests = sameIdentityGrowth && cached.fileContentDigest !== undefined
191
+ ? await this.tryHashFileContent(ref.filePath, freshness, cached.lastOffset)
192
+ : undefined;
193
+ const provenSameFileAppend = sameIdentityGrowth &&
194
+ cached.fileContentDigest !== undefined &&
195
+ growthDigests?.prefixDigest === cached.fileContentDigest;
196
+ let sourceChangeKind = !cached
197
+ ? 'unknown-full-parse'
198
+ : ref.kind === 'db'
199
+ ? 'db-update'
200
+ : fileReplaced
201
+ ? 'file-replacement'
202
+ : freshness.size > cached.lastSize
203
+ ? provenSameFileAppend
204
+ ? 'same-file-append'
205
+ : 'same-file-rewrite'
206
+ : freshness.size < cached.lastSize
207
+ ? 'file-truncation'
208
+ : 'same-file-rewrite';
209
+ if (cached && provenSameFileAppend && growthDigests) {
76
210
  this.logger.debug({ sessionId, lastOffset: cached.lastOffset, currentSize: freshness.size }, 'Incremental parse (source grew)');
77
211
  const incOptions = { byteOffset: cached.lastOffset, includeToolCalls: true };
78
- const result = threadRef
212
+ let tentativeResult = threadRef
79
213
  ? await adapter.parseIncremental(ref.filePath, incOptions, threadRef)
80
214
  : await adapter.parseIncremental(ref.filePath, incOptions);
81
- const newMessages = result.entries;
82
- if (adapter.incrementalMode === 'snapshot') {
83
- const snapshotMetrics = result.metrics ?? cached.session.metrics;
84
- session = {
85
- ...cached.session,
86
- messages: newMessages,
87
- metrics: snapshotMetrics,
88
- isOngoing: snapshotMetrics.isOngoing,
89
- warnings: this.mergeWarnings(cached.session.warnings, result.warnings),
90
- };
215
+ const postParseDigests = await this.tryHashFileContent(ref.filePath, freshness, freshness.size);
216
+ if (postParseDigests?.fullDigest !== growthDigests.fullDigest) {
217
+ this.logger.debug({ sessionId }, 'Discarding incremental parse because its proven file revision drifted');
218
+ tentativeResult = undefined;
219
+ freshness = await this.computeFreshness(ref, adapter);
220
+ sourceChangeKind = this.classifyUnsafeFileChange(cached, freshness);
221
+ session = threadRef
222
+ ? await adapter.parseFullSession(ref.filePath, threadRef)
223
+ : await adapter.parseFullSession(ref.filePath);
224
+ lastOffset = freshness.size;
91
225
  }
92
226
  else {
93
- const folded = this.foldLeadingContinuationIntoCachedTail(cached.session.messages, newMessages);
94
- boundaryFold = folded.tailMutatedWithoutNewMessage;
95
- const mergedMessages = folded.merged;
96
- const mergedMetrics = result.metrics
97
- ? this.mergeMetrics(cached.session.metrics, result.metrics, mergedMessages)
98
- : cached.session.metrics;
99
- session = {
100
- ...cached.session,
101
- messages: mergedMessages,
102
- metrics: mergedMetrics,
103
- isOngoing: mergedMetrics.isOngoing,
104
- warnings: this.mergeWarnings(cached.session.warnings, result.warnings),
105
- };
227
+ const acceptedResult = tentativeResult;
228
+ acceptedFileDigests = postParseDigests;
229
+ const newMessages = acceptedResult.entries;
230
+ if (adapter.incrementalMode === 'snapshot') {
231
+ const snapshotMetrics = acceptedResult.metrics ?? cached.session.metrics;
232
+ session = {
233
+ ...cached.session,
234
+ messages: newMessages,
235
+ metrics: snapshotMetrics,
236
+ isOngoing: snapshotMetrics.isOngoing,
237
+ warnings: this.mergeWarnings(cached.session.warnings, acceptedResult.warnings),
238
+ };
239
+ }
240
+ else {
241
+ const folded = this.foldLeadingContinuationIntoCachedTail(cached.session.messages, newMessages);
242
+ boundaryFold = folded.tailMutatedWithoutNewMessage;
243
+ const mergedMessages = folded.merged;
244
+ const mergedMetrics = acceptedResult.metrics
245
+ ? this.mergeMetrics(cached.session.metrics, acceptedResult.metrics, mergedMessages)
246
+ : cached.session.metrics;
247
+ session = {
248
+ ...cached.session,
249
+ messages: mergedMessages,
250
+ metrics: mergedMetrics,
251
+ isOngoing: mergedMetrics.isOngoing,
252
+ warnings: this.mergeWarnings(cached.session.warnings, acceptedResult.warnings),
253
+ };
254
+ }
255
+ lastOffset = acceptedResult.nextByteOffset;
106
256
  }
107
- lastOffset = result.nextByteOffset;
108
257
  }
109
258
  else {
110
- if (cached && freshness.size < cached.lastSize) {
259
+ if (fileReplaced) {
260
+ this.logger.debug({ sessionId }, 'Full reparse (source replaced)');
261
+ }
262
+ else if (cached && freshness.size < cached.lastSize) {
111
263
  this.logger.debug({ sessionId }, 'Full reparse (source truncated)');
112
264
  }
113
265
  session = threadRef
@@ -119,58 +271,175 @@ let SessionCacheService = SessionCacheService_1 = class SessionCacheService {
119
271
  if (coalesced.messages !== session.messages) {
120
272
  session = { ...session, messages: coalesced.messages, metrics: coalesced.metrics };
121
273
  }
122
- this.evictIfNeeded(sessionId);
123
- this.cache.delete(sessionId);
124
- this.cache.set(sessionId, {
274
+ session = this.withoutDerivedChunks(session);
275
+ const fileContentDigest = ref.kind === 'file'
276
+ ? lastOffset === freshness.size && acceptedFileDigests !== undefined
277
+ ? acceptedFileDigests.fullDigest
278
+ : (await this.tryHashFileContent(ref.filePath, freshness, lastOffset))?.prefixDigest
279
+ : undefined;
280
+ this.deleteEntry(sessionId);
281
+ const sourceWeightBytes = this.estimateSourceWeight(ref, session, freshness.size);
282
+ const entry = {
125
283
  session,
126
284
  lastOffset,
127
285
  lastSize: freshness.size,
128
286
  lastMtime: freshness.mtimeMs,
287
+ fileIdentity: freshness.fileIdentity,
288
+ fileContentDigest,
129
289
  sourceVersion: freshness.sourceVersion,
130
290
  freshnessToken: freshness.token,
131
- cachedAt: now,
291
+ lastAccessedAt: now,
292
+ sourceWeightBytes,
293
+ weights: { parsed: sourceWeightBytes * 2, chunks: 0, dto: 0 },
132
294
  boundaryFold,
133
- });
134
- return session;
135
- }
136
- async getOrParseWithMeta(sessionId, source, adapter) {
137
- const prevSession = this.cache.get(sessionId)?.session;
138
- const session = await this.getOrParse(sessionId, source, adapter);
139
- const entry = this.cache.get(sessionId);
295
+ };
296
+ this.cache.set(sessionId, entry);
297
+ this.budgetUsedBytes += entry.weights.parsed;
298
+ this.enforceBudget();
140
299
  return {
141
300
  session,
142
- cacheHit: session === prevSession,
143
- lastOffset: entry.lastOffset,
144
- lastSize: entry.lastSize,
145
- lastMtime: entry.lastMtime,
146
- sourceVersion: entry.sourceVersion,
147
- boundaryFold: entry.boundaryFold,
301
+ cacheHit: false,
302
+ sourceChangeKind,
303
+ lastOffset,
304
+ lastSize: freshness.size,
305
+ lastMtime: freshness.mtimeMs,
306
+ sourceVersion: freshness.sourceVersion,
307
+ boundaryFold,
148
308
  };
149
309
  }
150
310
  getEntry(sessionId) {
151
311
  return this.cache.get(sessionId);
152
312
  }
313
+ getChunks(sessionId, sourceVersion) {
314
+ const entry = this.cache.get(sessionId);
315
+ if (!entry?.chunks || entry.chunks.sourceVersion !== sourceVersion) {
316
+ this.chunksMisses += 1;
317
+ return undefined;
318
+ }
319
+ this.chunksHits += 1;
320
+ this.touchLru(sessionId, entry, Date.now());
321
+ return entry.chunks.chunks;
322
+ }
323
+ setChunks(sessionId, sourceVersion, chunks) {
324
+ const entry = this.cache.get(sessionId);
325
+ if (!entry || entry.sourceVersion !== sourceVersion)
326
+ return;
327
+ this.budgetUsedBytes -= entry.weights.chunks;
328
+ entry.chunks = { chunks, sourceVersion };
329
+ entry.weights.chunks = entry.sourceWeightBytes;
330
+ this.budgetUsedBytes += entry.weights.chunks;
331
+ this.touchLru(sessionId, entry, Date.now());
332
+ this.enforceBudget();
333
+ }
334
+ getDto(sessionId, maxToolResultLength, enrichmentFingerprint) {
335
+ const entry = this.cache.get(sessionId);
336
+ const dto = entry?.dto;
337
+ if (!entry ||
338
+ !dto ||
339
+ dto.maxToolResultLength !== maxToolResultLength ||
340
+ dto.enrichmentFingerprint !== enrichmentFingerprint) {
341
+ this.dtoMisses += 1;
342
+ return undefined;
343
+ }
344
+ this.dtoHits += 1;
345
+ this.touchLru(sessionId, entry, Date.now());
346
+ return dto;
347
+ }
348
+ setDto(sessionId, dto) {
349
+ const entry = this.cache.get(sessionId);
350
+ if (!entry)
351
+ return;
352
+ this.budgetUsedBytes -= entry.weights.dto;
353
+ entry.dto = dto;
354
+ entry.weights.dto = dto.responseBytes;
355
+ this.budgetUsedBytes += entry.weights.dto;
356
+ this.touchLru(sessionId, entry, Date.now());
357
+ this.enforceBudget();
358
+ }
359
+ getDtoRetainedRoots() {
360
+ return Array.from(this.cache.values(), (entry) => entry.dto?.result);
361
+ }
362
+ getChunksRetainedRoots() {
363
+ return Array.from(this.cache.values(), (entry) => entry.chunks?.chunks);
364
+ }
365
+ sweepIdleEntries(now = Date.now()) {
366
+ let swept = 0;
367
+ for (const [sessionId, entry] of this.cache) {
368
+ if (now - entry.lastAccessedAt < this.config.idleTtlMs)
369
+ continue;
370
+ this.deleteEntry(sessionId);
371
+ swept += 1;
372
+ }
373
+ return swept;
374
+ }
153
375
  invalidate(sessionId) {
154
- this.cache.delete(sessionId);
376
+ this.deleteEntry(sessionId);
155
377
  }
156
378
  clear() {
157
379
  this.cache.clear();
380
+ this.budgetUsedBytes = 0;
158
381
  }
159
382
  get size() {
160
383
  return this.cache.size;
161
384
  }
162
- touchLru(sessionId, entry) {
385
+ takeFreshCachedSession(sessionId, cached, freshnessToken, now) {
386
+ if (!cached || JSON.stringify(cached.freshnessToken) !== JSON.stringify(freshnessToken)) {
387
+ return undefined;
388
+ }
389
+ this.logger.debug({ sessionId }, 'Session cache hit');
390
+ this.touchLru(sessionId, cached, now);
391
+ this.hits += 1;
392
+ return cached.session;
393
+ }
394
+ toGetOrParseResult(session, entry, cacheHit, sourceChangeKind) {
395
+ return {
396
+ session,
397
+ cacheHit,
398
+ sourceChangeKind,
399
+ lastOffset: entry.lastOffset,
400
+ lastSize: entry.lastSize,
401
+ lastMtime: entry.lastMtime,
402
+ sourceVersion: entry.sourceVersion,
403
+ boundaryFold: entry.boundaryFold,
404
+ };
405
+ }
406
+ touchLru(sessionId, entry, now) {
407
+ entry.lastAccessedAt = now;
163
408
  this.cache.delete(sessionId);
164
409
  this.cache.set(sessionId, entry);
165
410
  }
166
- evictIfNeeded(sessionId) {
167
- if (this.cache.size >= CACHE_MAX_ENTRIES && !this.cache.has(sessionId)) {
411
+ enforceBudget() {
412
+ while (this.budgetUsedBytes > this.config.budgetBytes && this.cache.size > 0) {
168
413
  const oldestKey = this.cache.keys().next().value;
169
414
  if (oldestKey !== undefined) {
170
- this.cache.delete(oldestKey);
415
+ this.deleteEntry(oldestKey);
416
+ this.evictions += 1;
171
417
  }
172
418
  }
173
419
  }
420
+ deleteEntry(sessionId) {
421
+ const entry = this.cache.get(sessionId);
422
+ if (!entry)
423
+ return;
424
+ this.budgetUsedBytes -= this.entryWeight(entry);
425
+ this.cache.delete(sessionId);
426
+ }
427
+ entryWeight(entry) {
428
+ return entry.weights.parsed + entry.weights.chunks + entry.weights.dto;
429
+ }
430
+ estimateSourceWeight(ref, session, sourceSize) {
431
+ if (ref.kind === 'file')
432
+ return Math.max(1, sourceSize);
433
+ const tokenProxy = Math.max(session.metrics.totalContextConsumption, session.metrics.totalTokens, session.metrics.visibleContextTokens) * 4;
434
+ return Math.max(1_024, tokenProxy, session.messages.length * 256);
435
+ }
436
+ withoutDerivedChunks(session) {
437
+ if (session.chunks === undefined)
438
+ return session;
439
+ const parsedSession = { ...session };
440
+ delete parsedSession.chunks;
441
+ return parsedSession;
442
+ }
174
443
  foldLeadingContinuationIntoCachedTail(cachedMessages, newMessages) {
175
444
  const tail = cachedMessages[cachedMessages.length - 1];
176
445
  if (!tail || tail.role !== 'assistant' || newMessages.length === 0) {
@@ -266,7 +535,79 @@ let SessionCacheService = SessionCacheService_1 = class SessionCacheService {
266
535
  }
267
536
  async statFile(filePath) {
268
537
  const stat = await fs.stat(filePath);
269
- return { size: stat.size, mtimeMs: stat.mtime.getTime() };
538
+ return {
539
+ size: stat.size,
540
+ mtimeMs: stat.mtime.getTime(),
541
+ dev: stat.dev,
542
+ ino: stat.ino,
543
+ fileIdentity: `${stat.dev}:${stat.ino}`,
544
+ };
545
+ }
546
+ classifyUnsafeFileChange(cached, current) {
547
+ if (cached.fileIdentity === undefined ||
548
+ current.fileIdentity === undefined ||
549
+ cached.fileIdentity !== current.fileIdentity) {
550
+ return 'file-replacement';
551
+ }
552
+ if (current.size < cached.lastSize) {
553
+ return 'file-truncation';
554
+ }
555
+ return 'same-file-rewrite';
556
+ }
557
+ async tryHashFileContent(filePath, expected, prefixLength) {
558
+ try {
559
+ return await this.hashFileContent(filePath, expected, prefixLength);
560
+ }
561
+ catch (error) {
562
+ this.logger.debug({ error, filePath }, 'File prefix proof unavailable — append is unsafe');
563
+ return undefined;
564
+ }
565
+ }
566
+ async hashFileContent(filePath, expected, prefixLength) {
567
+ if (!Number.isSafeInteger(prefixLength) ||
568
+ prefixLength < 0 ||
569
+ prefixLength > expected.size ||
570
+ expected.fileIdentity === undefined) {
571
+ throw new Error('Invalid file prefix proof bounds');
572
+ }
573
+ const handle = await fs.open(filePath, 'r');
574
+ const assertStableSnapshot = async () => {
575
+ const current = await handle.stat();
576
+ if (current.size !== expected.size ||
577
+ current.mtime.getTime() !== expected.mtimeMs ||
578
+ `${current.dev}:${current.ino}` !== expected.fileIdentity) {
579
+ throw new Error('File changed while computing append proof');
580
+ }
581
+ };
582
+ try {
583
+ await assertStableSnapshot();
584
+ const prefixHash = (0, node_crypto_1.createHash)('sha256');
585
+ const fullHash = (0, node_crypto_1.createHash)('sha256');
586
+ const buffer = Buffer.allocUnsafe(Math.min(FILE_HASH_CHUNK_BYTES, Math.max(1, expected.size)));
587
+ let position = 0;
588
+ while (position < expected.size) {
589
+ const requested = Math.min(buffer.byteLength, expected.size - position);
590
+ const { bytesRead } = await handle.read(buffer, 0, requested, position);
591
+ if (bytesRead === 0) {
592
+ throw new Error('File ended while computing append proof');
593
+ }
594
+ const chunk = buffer.subarray(0, bytesRead);
595
+ fullHash.update(chunk);
596
+ const prefixBytes = Math.min(bytesRead, Math.max(0, prefixLength - position));
597
+ if (prefixBytes > 0) {
598
+ prefixHash.update(chunk.subarray(0, prefixBytes));
599
+ }
600
+ position += bytesRead;
601
+ }
602
+ await assertStableSnapshot();
603
+ return {
604
+ prefixDigest: prefixHash.digest('hex'),
605
+ fullDigest: fullHash.digest('hex'),
606
+ };
607
+ }
608
+ finally {
609
+ await handle.close();
610
+ }
270
611
  }
271
612
  toSourceRef(source, adapter) {
272
613
  if (typeof source === 'string') {
@@ -279,12 +620,21 @@ let SessionCacheService = SessionCacheService_1 = class SessionCacheService {
279
620
  return source;
280
621
  }
281
622
  async computeFreshness(ref, adapter) {
282
- const { size, mtimeMs } = await this.statFile(ref.filePath);
283
- const token = adapter.getFreshnessToken
623
+ const { size, mtimeMs, dev, ino, fileIdentity } = await this.statFile(ref.filePath);
624
+ const adapterToken = adapter.getFreshnessToken
284
625
  ? await adapter.getFreshnessToken(ref)
285
- : { mtimeMs, size };
286
- const sourceVersion = ref.kind === 'db' ? this.dbSourceVersion(token, size) : size;
287
- return { token, sourceVersion, size, mtimeMs };
626
+ : { mtimeMs, size, dev, ino };
627
+ const token = ref.kind === 'file' && adapter.getFreshnessToken ? { adapterToken, dev, ino } : adapterToken;
628
+ const sourceVersion = ref.kind === 'db'
629
+ ? this.dbSourceVersion(token, size)
630
+ : fileSourceVersion({ size, mtimeMs, dev, ino });
631
+ return {
632
+ token,
633
+ sourceVersion,
634
+ size,
635
+ mtimeMs,
636
+ fileIdentity: ref.kind === 'file' ? fileIdentity : undefined,
637
+ };
288
638
  }
289
639
  dbSourceVersion(token, fallbackSize) {
290
640
  if (token && typeof token === 'object') {
@@ -298,6 +648,15 @@ let SessionCacheService = SessionCacheService_1 = class SessionCacheService {
298
648
  };
299
649
  exports.SessionCacheService = SessionCacheService;
300
650
  exports.SessionCacheService = SessionCacheService = SessionCacheService_1 = __decorate([
301
- (0, common_1.Injectable)()
651
+ (0, common_1.Injectable)(),
652
+ __param(1, (0, common_1.Optional)()),
653
+ __param(1, (0, common_1.Inject)(exports.TRANSCRIPT_CACHE_CONFIG)),
654
+ __metadata("design:paramtypes", [metrics_service_1.MetricsService, Object])
302
655
  ], SessionCacheService);
656
+ function positiveIntegerFromEnv(raw, fallback) {
657
+ if (raw === undefined)
658
+ return fallback;
659
+ const parsed = Number(raw);
660
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
661
+ }
303
662
  //# sourceMappingURL=session-cache.service.js.map