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.
- package/README.md +1 -0
- package/package.json +1 -1
- package/template/claude-task-manager/api-prompts.js +11 -6
- package/template/claude-task-manager/docs/session-status-redesign.html +554 -0
- package/template/claude-task-manager/docs/terminal-rendering-redesign.html +529 -0
- package/template/claude-task-manager/lib/flush-redraw-markers.js +72 -0
- package/template/claude-task-manager/lib/macos-capabilities.js +190 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +224 -3
- package/template/claude-task-manager/lib/ttl-memo.js +61 -0
- package/template/claude-task-manager/public/index.html +892 -11
- package/template/claude-task-manager/public/js/activation-render-check.js +40 -2
- package/template/claude-task-manager/public/js/session-phase.js +370 -0
- package/template/claude-task-manager/public/js/setup.js +74 -1
- package/template/claude-task-manager/public/js/stream-view.js +56 -2
- package/template/claude-task-manager/server.js +643 -68
- package/template/claude-task-manager/workers/read-pool-worker.js +10 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +130 -24
- package/template/wall-e/api-walle.js +12 -1
- package/template/wall-e/brain.js +290 -4
- package/template/wall-e/chat.js +30 -25
- package/template/wall-e/coding/session-plan.js +79 -0
- package/template/wall-e/coding-orchestrator.js +9 -3
- package/template/wall-e/coding-prompts.js +10 -3
- package/template/wall-e/embeddings.js +192 -17
- package/template/wall-e/http/model-admin.js +109 -0
- package/template/wall-e/lib/event-loop-monitor.js +2 -2
- package/template/wall-e/lib/scheduler-worker-jobs.js +156 -121
- package/template/wall-e/lib/scheduler.js +226 -13
- package/template/wall-e/lib/worker-thread-pool.js +58 -4
- package/template/wall-e/llm/ollama-library.js +126 -0
- package/template/wall-e/llm/ollama.js +13 -0
- package/template/wall-e/llm/provider-backpressure.js +134 -0
- package/template/wall-e/llm/provider-health-state.js +24 -0
- package/template/wall-e/loops/backfill.js +43 -16
- package/template/wall-e/loops/initiative.js +1 -0
- package/template/wall-e/loops/think.js +38 -5
- package/template/wall-e/mcp-server.js +20 -4
- package/template/wall-e/skills/skill-fallback.js +34 -1
- package/template/wall-e/skills/skill-planner.js +60 -2
- package/template/wall-e/sources/jsonl-utils.js +84 -11
- package/template/wall-e/telemetry.js +42 -7
- package/template/wall-e/tools/local-tools.js +16 -0
- package/template/wall-e/workers/runtime-worker.js +33 -1
- 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 {
|
|
380
|
+
return { messages: visibleMessages, ...finalExtra };
|
|
168
381
|
}
|
|
169
382
|
|
|
170
|
-
module.exports = {
|
|
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 };
|