create-walle 0.9.26 → 0.9.28

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 (45) hide show
  1. package/README.md +1 -0
  2. package/package.json +1 -1
  3. package/template/claude-task-manager/api-prompts.js +11 -6
  4. package/template/claude-task-manager/docs/session-status-redesign.html +554 -0
  5. package/template/claude-task-manager/docs/terminal-rendering-redesign.html +529 -0
  6. package/template/claude-task-manager/lib/flush-redraw-markers.js +72 -0
  7. package/template/claude-task-manager/lib/macos-capabilities.js +190 -0
  8. package/template/claude-task-manager/lib/session-messages-projection.js +224 -3
  9. package/template/claude-task-manager/lib/ttl-memo.js +61 -0
  10. package/template/claude-task-manager/public/index.html +892 -11
  11. package/template/claude-task-manager/public/js/activation-render-check.js +40 -2
  12. package/template/claude-task-manager/public/js/session-phase.js +370 -0
  13. package/template/claude-task-manager/public/js/setup.js +74 -1
  14. package/template/claude-task-manager/public/js/stream-view.js +56 -2
  15. package/template/claude-task-manager/server.js +643 -68
  16. package/template/claude-task-manager/workers/read-pool-worker.js +10 -0
  17. package/template/package.json +1 -1
  18. package/template/wall-e/agent.js +130 -24
  19. package/template/wall-e/api-walle.js +12 -1
  20. package/template/wall-e/brain.js +290 -4
  21. package/template/wall-e/chat.js +30 -25
  22. package/template/wall-e/coding/session-plan.js +79 -0
  23. package/template/wall-e/coding-orchestrator.js +9 -3
  24. package/template/wall-e/coding-prompts.js +10 -3
  25. package/template/wall-e/embeddings.js +192 -17
  26. package/template/wall-e/http/model-admin.js +109 -0
  27. package/template/wall-e/lib/event-loop-monitor.js +2 -2
  28. package/template/wall-e/lib/scheduler-worker-jobs.js +156 -121
  29. package/template/wall-e/lib/scheduler.js +226 -13
  30. package/template/wall-e/lib/worker-thread-pool.js +58 -4
  31. package/template/wall-e/llm/ollama-library.js +126 -0
  32. package/template/wall-e/llm/ollama.js +13 -0
  33. package/template/wall-e/llm/provider-backpressure.js +134 -0
  34. package/template/wall-e/llm/provider-health-state.js +24 -0
  35. package/template/wall-e/loops/backfill.js +43 -16
  36. package/template/wall-e/loops/initiative.js +1 -0
  37. package/template/wall-e/loops/think.js +38 -5
  38. package/template/wall-e/mcp-server.js +20 -4
  39. package/template/wall-e/skills/skill-fallback.js +34 -1
  40. package/template/wall-e/skills/skill-planner.js +60 -2
  41. package/template/wall-e/sources/jsonl-utils.js +84 -11
  42. package/template/wall-e/telemetry.js +42 -7
  43. package/template/wall-e/tools/local-tools.js +16 -0
  44. package/template/wall-e/workers/runtime-worker.js +33 -1
  45. package/template/website/index.html +5 -0
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ // macOS capability helpers — Full Disk Access (FDA) detection + non-local volume
4
+ // classification. Used to stop the repeating "Coding Task Manager would like to access
5
+ // files on a network volume" TCC prompt:
6
+ // - FDA is a superset of the network-volume / app-data / removable TCC services, so one
7
+ // FDA grant (persisting under the stable Dev-ID identity) ends all of those prompts.
8
+ // - isPathOnNonLocalVolume() lets the startup restore SKIP statting a Dropbox/NFS cwd at
9
+ // cold boot (no session is active yet), which is what fires the prompt today.
10
+ //
11
+ // Pure-with-injectable-deps so the logic is unit-testable without a real macOS; thin cached
12
+ // wrappers (default exports) read the real `mount` table / filesystem once.
13
+
14
+ const fs = require('fs');
15
+ const os = require('os');
16
+ const path = require('path');
17
+ const { execFileSync } = require('child_process');
18
+
19
+ const FDA_SETTINGS_URL = 'x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles';
20
+
21
+ // Filesystem types that are NOT the local boot volume — accessing files on these can trip the
22
+ // macOS "network volume" / "removable volume" TCC services.
23
+ const NONLOCAL_FS_TYPES = new Set([
24
+ 'nfs', 'smbfs', 'afpfs', 'webdav', 'ftp', 'fusefs', 'macfuse', 'osxfuse',
25
+ ]);
26
+
27
+ function _isDarwin() {
28
+ return process.platform === 'darwin';
29
+ }
30
+
31
+ // ---- non-local volume classification ------------------------------------------------
32
+
33
+ // Parse `mount` output into the list of mountpoints whose fs type is non-local.
34
+ // A macOS line looks like:
35
+ // OrbStack:/OrbStack on /Users/example/OrbStack (nfs, nodev, nosuid, noatime, mounted by example)
36
+ // PURE: takes the raw text, returns mountpoints.
37
+ function parseNonLocalMountpoints(mountOutput) {
38
+ const out = [];
39
+ for (const line of String(mountOutput || '').split('\n')) {
40
+ const m = line.match(/ on (.+?) \(([a-z0-9_]+)[,)]/i);
41
+ if (!m) continue;
42
+ const mountpoint = m[1];
43
+ const fstype = (m[2] || '').toLowerCase();
44
+ if (NONLOCAL_FS_TYPES.has(fstype)) out.push(mountpoint);
45
+ }
46
+ return out;
47
+ }
48
+
49
+ // Build the set of path prefixes that count as "non-local" (defer FS touch / FDA-covered).
50
+ // PURE given { mountOutput, home }:
51
+ // - ~/Library/CloudStorage/ (Dropbox/iCloud/OneDrive File Provider — the actual Dropbox case)
52
+ // - every non-local mountpoint parsed from `mount` (NFS/SMB/AFP/WebDAV, e.g. ~/OrbStack)
53
+ // - /Volumes/ (external / network / disk-image mounts)
54
+ function nonLocalVolumePrefixes({ mountOutput = '', home = os.homedir() } = {}) {
55
+ const prefixes = new Set();
56
+ prefixes.add(path.join(home, 'Library', 'CloudStorage') + path.sep);
57
+ // Legacy Dropbox folder (`~/Dropbox`) — older Dropbox installs and hand-made symlinks
58
+ // (e.g. `~/.codex -> ~/Dropbox/work/codex`) live here rather than under CloudStorage, but
59
+ // macOS still treats it as a File Provider and gates reads behind TCC.
60
+ prefixes.add(path.join(home, 'Dropbox') + path.sep);
61
+ prefixes.add('/Volumes' + path.sep);
62
+ for (const mp of parseNonLocalMountpoints(mountOutput)) {
63
+ if (mp && mp !== '/') prefixes.add(mp.endsWith(path.sep) ? mp : mp + path.sep);
64
+ }
65
+ return [...prefixes];
66
+ }
67
+
68
+ // PURE prefix match — NEVER touches the filesystem (so it cannot itself trigger a TCC prompt).
69
+ function pathHasNonLocalPrefix(p, prefixes) {
70
+ if (!p) return false;
71
+ const s = String(p);
72
+ for (const pre of prefixes) {
73
+ if (s === pre.slice(0, -1) || s.startsWith(pre)) return true;
74
+ }
75
+ return false;
76
+ }
77
+
78
+ // ---- cached real-system wrappers ----------------------------------------------------
79
+
80
+ let _cachedPrefixes = null;
81
+ function _defaultPrefixes() {
82
+ if (_cachedPrefixes) return _cachedPrefixes;
83
+ let mountOutput = '';
84
+ if (_isDarwin()) {
85
+ try { mountOutput = execFileSync('/sbin/mount', [], { encoding: 'utf8', timeout: 3000 }); }
86
+ catch { mountOutput = ''; }
87
+ }
88
+ _cachedPrefixes = nonLocalVolumePrefixes({ mountOutput, home: os.homedir() });
89
+ return _cachedPrefixes;
90
+ }
91
+
92
+ // Convenience used by the startup restore guard: is this cwd/worktree on a non-local volume?
93
+ function isPathOnNonLocalVolume(p, prefixes = _defaultPrefixes()) {
94
+ return pathHasNonLocalPrefix(p, prefixes);
95
+ }
96
+
97
+ // ---- Full Disk Access detection -----------------------------------------------------
98
+
99
+ // Files that require Full Disk Access to read. Reading one of these is the canonical SILENT
100
+ // FDA probe — a denial returns EPERM/EACCES with no dialog (unlike a network-volume access).
101
+ function _fdaProbePaths(home = os.homedir()) {
102
+ return [
103
+ path.join(home, 'Library', 'Application Support', 'com.apple.TCC', 'TCC.db'),
104
+ path.join(home, 'Library', 'Safari', 'Bookmarks.plist'),
105
+ path.join(home, 'Library', 'Safari', 'CloudTabs.db'),
106
+ ];
107
+ }
108
+
109
+ // Returns true (granted), false (denied), or null (undeterminable → caller should NOT nag).
110
+ // DI: accessSync + existsSync injectable for tests; defaults probe the real filesystem.
111
+ function fullDiskAccessGranted({
112
+ platform = process.platform,
113
+ probePaths = _fdaProbePaths(),
114
+ accessSync = fs.accessSync,
115
+ existsSync = fs.existsSync,
116
+ } = {}) {
117
+ if (platform !== 'darwin') return true; // n/a off macOS — never nag
118
+ let sawProbe = false;
119
+ for (const p of probePaths) {
120
+ if (!existsSync(p)) continue; // file absent on this machine — not a usable probe
121
+ sawProbe = true;
122
+ try {
123
+ accessSync(p, fs.constants.R_OK);
124
+ return true; // could read an FDA-gated file ⇒ FDA granted
125
+ } catch (e) {
126
+ if (e && (e.code === 'EPERM' || e.code === 'EACCES')) return false; // gated ⇒ no FDA
127
+ // ENOENT/other: not a reliable signal — try the next probe
128
+ }
129
+ }
130
+ return sawProbe ? false : null; // probes existed but all errored ⇒ denied; none existed ⇒ unknown
131
+ }
132
+
133
+ // ---- authoritative read-access probe ------------------------------------------------
134
+
135
+ // Does reading this path FAIL with a permission denial (EPERM/EACCES)? Unlike the pure
136
+ // prefix heuristics above, this is the GROUND TRUTH: it follows symlinks (so `~/.codex`
137
+ // pointing into Dropbox is seen as the real target) and reports the actual OS verdict,
138
+ // independent of where the file lives or how the volume is classified. EPERM is the
139
+ // signature of a macOS File-Provider / TCC denial (the file is owner-readable yet the
140
+ // process lacks Full Disk Access); EACCES is a plain permission denial. ENOENT / no path
141
+ // ⇒ null (not a denial — nothing to ask for). DI for tests.
142
+ function probePathReadDenied(p, {
143
+ platform = process.platform,
144
+ accessSync = fs.accessSync,
145
+ realpathSync = fs.realpathSync,
146
+ } = {}) {
147
+ if (!p || platform !== 'darwin') return null;
148
+ let target = String(p);
149
+ try { target = realpathSync(target); } catch { /* broken symlink / unreadable parent — probe as-is */ }
150
+ try {
151
+ accessSync(target, fs.constants.R_OK);
152
+ return null; // readable ⇒ not denied
153
+ } catch (e) {
154
+ if (e && (e.code === 'EPERM' || e.code === 'EACCES')) return e.code;
155
+ return null; // ENOENT/other ⇒ not an access denial we can fix by granting FDA
156
+ }
157
+ }
158
+
159
+ // ---- FDA guidance (paths + deep link shown to the user) -----------------------------
160
+
161
+ const BUNDLE_ROOT = process.env.WALLE_BUNDLE_DIR || path.join(os.homedir(), '.walle', 'bundles');
162
+
163
+ function ctmBundleAppPath() {
164
+ return path.join(BUNDLE_ROOT, 'Coding Task Manager.app');
165
+ }
166
+
167
+ function fdaGuidance({ bundlePath = ctmBundleAppPath() } = {}) {
168
+ return {
169
+ bundlePath,
170
+ settingsUrl: FDA_SETTINGS_URL,
171
+ hint:
172
+ 'Open System Settings → Privacy & Security → Full Disk Access, click +, press ⌘⇧G, ' +
173
+ `paste ${bundlePath}, add "Coding Task Manager", and turn it on. Then restart CTM once — ` +
174
+ 'macOS will stop asking for network-volume / file access on every restart.',
175
+ };
176
+ }
177
+
178
+ module.exports = {
179
+ FDA_SETTINGS_URL,
180
+ NONLOCAL_FS_TYPES,
181
+ parseNonLocalMountpoints,
182
+ nonLocalVolumePrefixes,
183
+ pathHasNonLocalPrefix,
184
+ isPathOnNonLocalVolume,
185
+ probePathReadDenied,
186
+ fullDiskAccessGranted,
187
+ fdaGuidance,
188
+ ctmBundleAppPath,
189
+ _fdaProbePaths,
190
+ };
@@ -90,6 +90,189 @@ function _attachImageRefsIfNeeded(messages, manifests) {
90
90
  return attachSessionImageRefs(messages, manifests);
91
91
  }
92
92
 
93
+ const COMPACT_ACTIVITY_KINDS = new Set([
94
+ 'reasoning',
95
+ 'tool_call',
96
+ 'tool_result',
97
+ 'patch',
98
+ 'plan',
99
+ 'activity',
100
+ 'command',
101
+ ]);
102
+ const COMPACT_ACTIVITY_PREFIX_RE = /^\s*(?:\[(?:Tool|Tool result|Reasoning|Patch)[^\]]*\]|(?:Exit code|Chunk ID|Plan updated|Success\. Updated|Would you like to run|Reason:|Bash\(|Read \d+ file|Listed \d+ director|Ran |Running |Waiting\.{0,3}|Process (?:exited|running)|Original token count:))/i;
103
+ const COMPACT_PREVIEW_LIMIT = 180;
104
+ const COMPACT_SUMMARY_PREVIEWS = 3;
105
+
106
+ function _compactRole(message) {
107
+ return String(message?.role || '').trim().toLowerCase();
108
+ }
109
+
110
+ function _compactMetadata(message) {
111
+ return message && message.metadata && typeof message.metadata === 'object' ? message.metadata : {};
112
+ }
113
+
114
+ function _compactPreview(text) {
115
+ const clean = String(text || '').replace(/\s+/g, ' ').trim();
116
+ if (!clean) return '';
117
+ return clean.length > COMPACT_PREVIEW_LIMIT ? `${clean.slice(0, COMPACT_PREVIEW_LIMIT - 1)}…` : clean;
118
+ }
119
+
120
+ function _compactActivityLabel(message) {
121
+ const meta = _compactMetadata(message);
122
+ const role = _compactRole(message);
123
+ const kind = String(meta.kind || meta.sourceKind || '').trim();
124
+ if (kind === 'tool_call' && meta.tool) return `tool:${meta.tool}`;
125
+ if (kind) return kind;
126
+ const text = sessionMessageText(message);
127
+ const prefix = String(text || '').match(/^\s*\[([^\]]+)\]/);
128
+ if (prefix) return prefix[1].toLowerCase();
129
+ return role || 'message';
130
+ }
131
+
132
+ function _assistantKeepIndexes(messages) {
133
+ const keep = new Set();
134
+ let lastAssistant = -1;
135
+ const flush = () => {
136
+ if (lastAssistant >= 0) keep.add(lastAssistant);
137
+ lastAssistant = -1;
138
+ };
139
+ for (let i = 0; i < messages.length; i += 1) {
140
+ const message = messages[i];
141
+ const role = _compactRole(message);
142
+ if (role === 'user') {
143
+ flush();
144
+ continue;
145
+ }
146
+ if (role === 'assistant' && sessionMessageText(message).trim()) lastAssistant = i;
147
+ }
148
+ flush();
149
+ return keep;
150
+ }
151
+
152
+ function _isConversationActivityDetail(message, options = {}) {
153
+ if (!message || typeof message !== 'object') return false;
154
+ const role = _compactRole(message);
155
+ if (role === 'user') return false;
156
+ if (role === 'assistant' && options.keepAssistant) return false;
157
+ const meta = _compactMetadata(message);
158
+ if (meta.sourceKind === 'subagent' && meta.originalRole === 'user') return false;
159
+ const kind = String(meta.kind || '').trim();
160
+ if (kind && COMPACT_ACTIVITY_KINDS.has(kind)) return true;
161
+ const text = sessionMessageText(message);
162
+ if (!String(text || '').trim()) return role !== 'assistant';
163
+ if (COMPACT_ACTIVITY_PREFIX_RE.test(text)) return true;
164
+ if (role === 'system' || role === 'tool' || role === 'function') return true;
165
+ // Non-final assistant updates inside a turn are progress chatter; keep the last assistant answer.
166
+ if (role === 'assistant' && !options.keepAssistant) return true;
167
+ return false;
168
+ }
169
+
170
+ function _emptyCompactGroup() {
171
+ return {
172
+ count: 0,
173
+ roleCounts: {},
174
+ labels: {},
175
+ previews: [],
176
+ firstTimestamp: '',
177
+ lastTimestamp: '',
178
+ };
179
+ }
180
+
181
+ function _trackCompactMessage(group, message) {
182
+ const role = _compactRole(message) || 'message';
183
+ const label = _compactActivityLabel(message);
184
+ group.count += 1;
185
+ group.roleCounts[role] = (group.roleCounts[role] || 0) + 1;
186
+ group.labels[label] = (group.labels[label] || 0) + 1;
187
+ const ts = String(message?.timestamp || message?.created_at || message?.createdAt || '').trim();
188
+ if (ts && !group.firstTimestamp) group.firstTimestamp = ts;
189
+ if (ts) group.lastTimestamp = ts;
190
+ if (group.previews.length < COMPACT_SUMMARY_PREVIEWS) {
191
+ const preview = _compactPreview(sessionMessageText(message));
192
+ if (preview) group.previews.push(preview);
193
+ }
194
+ }
195
+
196
+ function _formatCompactPairs(obj) {
197
+ return Object.entries(obj || {})
198
+ .sort((a, b) => b[1] - a[1] || String(a[0]).localeCompare(String(b[0])))
199
+ .slice(0, 5)
200
+ .map(([key, value]) => `${value} ${key}`)
201
+ .join(', ');
202
+ }
203
+
204
+ function _compactSummaryMessage(group) {
205
+ const roles = _formatCompactPairs(group.roleCounts);
206
+ const labels = _formatCompactPairs(group.labels);
207
+ const preview = group.previews.length ? ` Highlights: ${group.previews.join(' | ')}` : '';
208
+ const text = `Collapsed ${group.count} activity record${group.count === 1 ? '' : 's'}${roles ? ` (${roles})` : ''}${labels ? `: ${labels}.` : '.'}${preview}`;
209
+ return {
210
+ role: 'system',
211
+ text,
212
+ timestamp: group.lastTimestamp || group.firstTimestamp || '',
213
+ metadata: {
214
+ sourceKind: 'ctm-activity-summary',
215
+ compacted: true,
216
+ collapsedCount: group.count,
217
+ collapsedRoles: group.roleCounts,
218
+ collapsedLabels: group.labels,
219
+ previews: group.previews,
220
+ },
221
+ };
222
+ }
223
+
224
+ function compactConversationMessages(messages) {
225
+ const input = Array.isArray(messages) ? messages : [];
226
+ if (input.length === 0) {
227
+ return { messages: input, original_count: 0, compacted_count: 0, collapsed_count: 0, summary_count: 0 };
228
+ }
229
+ const keepAssistant = _assistantKeepIndexes(input);
230
+ const out = [];
231
+ let group = _emptyCompactGroup();
232
+ let collapsed = 0;
233
+ let summaries = 0;
234
+ const flush = () => {
235
+ if (!group.count) return;
236
+ out.push(_compactSummaryMessage(group));
237
+ collapsed += group.count;
238
+ summaries += 1;
239
+ group = _emptyCompactGroup();
240
+ };
241
+ for (let i = 0; i < input.length; i += 1) {
242
+ const message = input[i];
243
+ if (_isConversationActivityDetail(message, { keepAssistant: keepAssistant.has(i) })) {
244
+ _trackCompactMessage(group, message);
245
+ } else {
246
+ flush();
247
+ out.push(message);
248
+ }
249
+ }
250
+ flush();
251
+ return {
252
+ messages: out,
253
+ original_count: input.length,
254
+ compacted_count: out.length,
255
+ collapsed_count: collapsed,
256
+ summary_count: summaries,
257
+ };
258
+ }
259
+
260
+ function applyConversationCompactionToPage(page, options = {}) {
261
+ if (!options.compactConversation) return page;
262
+ const compacted = compactConversationMessages(page?.messages || []);
263
+ return {
264
+ ...(page || {}),
265
+ messages: compacted.messages,
266
+ conversationCompact: {
267
+ mode: 'activity-summary',
268
+ originalMessages: compacted.original_count,
269
+ renderedMessages: compacted.compacted_count,
270
+ collapsedMessages: compacted.collapsed_count,
271
+ summaryMessages: compacted.summary_count,
272
+ },
273
+ };
274
+ }
275
+
93
276
  // The whole projection, off-thread-safe. Inputs are resolved + snapshotted on main:
94
277
  // baseMessages - the fetched page (getMessagesPage/TurnPage)
95
278
  // streamEvents - snapshot of the live stream ring (collectTimelineStreamEvents on main)
@@ -148,12 +331,42 @@ function _sourceWithStreamTail(source, added) {
148
331
  // skipStreamTail); publicIdentity = _publicTimelineIdentity(identity) (null for offset>0).
149
332
  function buildPaginatedPageResponse({
150
333
  baseMessages, streamEvents = [], skipStreamTail = false, exclusionRows = [], filterCodexSynthetic = false,
151
- imageManifests = null, pageMeta = {}, extra = {}, publicIdentity = null,
334
+ imageManifests = null, pageMeta = {}, extra = {}, publicIdentity = null, compactConversation = false,
335
+ } = {}) {
336
+ const proj = projectSessionMessagesPage({
337
+ baseMessages, streamEvents, skipStreamTail, exclusionRows, filterCodexSynthetic, imageManifests, allowEmptyBase: true,
338
+ });
339
+ const visibleMessages = proj.messages;
340
+ const visibleTailAdded = Math.max(0, (proj.visibleMergedCount || 0) - (proj.visibleBaseCount || 0));
341
+ const finalExtra = { ...extra };
342
+ delete finalExtra.skipStreamTail;
343
+ finalExtra.timelineIdentity = publicIdentity || null;
344
+ finalExtra.timelineFreshness = visibleTailAdded > 0 ? 'hot-tail' : 'durable';
345
+ finalExtra.streamEventCount = proj.streamEventCount || 0;
346
+ if (visibleTailAdded > 0) {
347
+ finalExtra.source = _sourceWithStreamTail(finalExtra.source, visibleTailAdded);
348
+ finalExtra.streamTailAdded = visibleTailAdded;
349
+ }
350
+ const response = { ...pageMeta, messages: visibleMessages, ...finalExtra };
351
+ return applyConversationCompactionToPage(response, { compactConversation: compactConversation === true });
352
+ }
353
+
354
+ // Stage C: the NON-paginated full read. Mirrors server.js sendMessages' projection + response
355
+ // assembly EXACTLY so the worker can project + serialize the full conversation off the main loop
356
+ // (the on-main merge/dedupText + exclusions sha256/msg + image-refs + stringify over a 3k-16k-msg
357
+ // array was the profiled "reading conversational logs" freeze). Two response shapes, matching
358
+ // sendMessages: a BARE ARRAY when the caller passed no extra (the common conversation read), else
359
+ // { messages, ...finalExtra } with the same timeline fields sendMessages adds when hasCallerExtra.
360
+ function buildFullMessagesResponse({
361
+ baseMessages, streamEvents = [], skipStreamTail = false, exclusionRows = [], filterCodexSynthetic = false,
362
+ imageManifests = null, extra = {}, publicIdentity = null, bareArray = false,
152
363
  } = {}) {
153
364
  const proj = projectSessionMessagesPage({
154
365
  baseMessages, streamEvents, skipStreamTail, exclusionRows, filterCodexSynthetic, imageManifests, allowEmptyBase: true,
155
366
  });
156
367
  const visibleMessages = proj.messages;
368
+ // Bare array: non-paginated read with no caller extra (sendMessages returns JSON.stringify(arr)).
369
+ if (bareArray) return visibleMessages;
157
370
  const visibleTailAdded = Math.max(0, (proj.visibleMergedCount || 0) - (proj.visibleBaseCount || 0));
158
371
  const finalExtra = { ...extra };
159
372
  delete finalExtra.skipStreamTail;
@@ -164,7 +377,15 @@ function buildPaginatedPageResponse({
164
377
  finalExtra.source = _sourceWithStreamTail(finalExtra.source, visibleTailAdded);
165
378
  finalExtra.streamTailAdded = visibleTailAdded;
166
379
  }
167
- return { ...pageMeta, messages: visibleMessages, ...finalExtra };
380
+ return { messages: visibleMessages, ...finalExtra };
168
381
  }
169
382
 
170
- module.exports = { applyExclusions, sessionMessageTextHash, projectSessionMessagesPage, buildPaginatedPageResponse };
383
+ module.exports = {
384
+ applyExclusions,
385
+ sessionMessageTextHash,
386
+ projectSessionMessagesPage,
387
+ buildPaginatedPageResponse,
388
+ buildFullMessagesResponse,
389
+ compactConversationMessages,
390
+ applyConversationCompactionToPage,
391
+ };
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ // Tiny single-value-per-key TTL memo with bounded LRU eviction.
4
+ //
5
+ // WHY: several hot read paths (e.g. the per-poll /api/session/prompts freshness
6
+ // probe and the per-miss codex-source probe) run a synchronous SQLite read on the
7
+ // MAIN event loop on every call. Under many sessions polling concurrently those
8
+ // reads stack into multi-hundred-ms event-loop blocks (observed up to 790ms on a
9
+ // dev storm, multi-second on the primary with cold pages). The values they read
10
+ // change rarely (a user-prompt count, a session's codex-ness), so collapsing
11
+ // repeated identical reads behind a short TTL removes the main-thread work without
12
+ // changing the answer beyond a sub-second/short staleness window — strictly cheaper.
13
+ //
14
+ // compute() runs only on a miss or after the TTL expires. now() is injectable so
15
+ // the behavior is deterministically unit-testable. ttlMs<=0 disables caching
16
+ // entirely (every get() recomputes) so the cache can be turned off via env.
17
+
18
+ function createTtlMemo({ ttlMs, max = 512, now = Date.now } = {}) {
19
+ const store = new Map(); // key -> { value, at }
20
+ const ttl = Number(ttlMs) || 0;
21
+ const cap = Math.max(1, Number(max) || 512);
22
+
23
+ // Return the cached value for `key` if still fresh, else run compute(), store the
24
+ // result (unless cacheIf(value) === false), and return it. cacheIf lets callers
25
+ // skip caching sentinel/error values (e.g. a null "do-not-trust" freshness key)
26
+ // so a transient failure isn't pinned for the whole TTL.
27
+ function get(key, compute, cacheIf) {
28
+ const t = now();
29
+ if (ttl > 0) {
30
+ const hit = store.get(key);
31
+ if (hit && (t - hit.at) < ttl) return hit.value;
32
+ }
33
+ const value = compute();
34
+ if (ttl > 0 && (typeof cacheIf !== 'function' || cacheIf(value))) {
35
+ set(key, value, t);
36
+ }
37
+ return value;
38
+ }
39
+
40
+ function set(key, value, at) {
41
+ if (ttl <= 0) return value;
42
+ store.delete(key); // refresh LRU recency
43
+ store.set(key, { value, at: at == null ? now() : at });
44
+ while (store.size > cap) {
45
+ const oldest = store.keys().next().value;
46
+ if (oldest === undefined) break;
47
+ store.delete(oldest);
48
+ }
49
+ return value;
50
+ }
51
+
52
+ return {
53
+ get,
54
+ set,
55
+ delete: (key) => store.delete(key),
56
+ clear: () => store.clear(),
57
+ get size() { return store.size; },
58
+ };
59
+ }
60
+
61
+ module.exports = { createTtlMemo };