create-walle 0.9.13 → 0.9.14
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 +6 -1
- package/bin/create-walle.js +195 -30
- package/bin/mcp-inject.js +18 -53
- package/package.json +3 -1
- package/template/claude-task-manager/approval-agent.js +7 -0
- package/template/claude-task-manager/docs/session-standup-command-center-design.md +242 -0
- package/template/claude-task-manager/git-utils.js +111 -3
- package/template/claude-task-manager/lib/session-history.js +144 -16
- package/template/claude-task-manager/lib/session-standup.js +409 -0
- package/template/claude-task-manager/lib/standup-attention.js +200 -0
- package/template/claude-task-manager/lib/status-hooks.js +8 -2
- package/template/claude-task-manager/lib/update-telemetry.js +114 -0
- package/template/claude-task-manager/lib/walle-default-model.js +55 -0
- package/template/claude-task-manager/lib/walle-mcp-auto-config.js +62 -0
- package/template/claude-task-manager/lib/walle-supervisor.js +83 -19
- package/template/claude-task-manager/lib/worktree-cwd.js +82 -0
- package/template/claude-task-manager/providers/codex-mcp.js +104 -0
- package/template/claude-task-manager/providers/index.js +2 -0
- package/template/claude-task-manager/public/css/setup.css +2 -1
- package/template/claude-task-manager/public/css/walle.css +5 -0
- package/template/claude-task-manager/public/index.html +1596 -283
- package/template/claude-task-manager/public/js/session-search-utils.js +171 -1
- package/template/claude-task-manager/public/js/setup.js +62 -19
- package/template/claude-task-manager/public/js/stream-view.js +55 -6
- package/template/claude-task-manager/public/js/walle-session.js +73 -16
- package/template/claude-task-manager/public/js/walle.js +34 -2
- package/template/claude-task-manager/server.js +780 -177
- package/template/claude-task-manager/session-integrity.js +58 -15
- package/template/claude-task-manager/workers/approval-widget-validator.js +15 -5
- package/template/claude-task-manager/workers/state-detectors/codex.js +6 -0
- package/template/package.json +1 -1
- package/template/wall-e/agent.js +36 -7
- package/template/wall-e/api-walle.js +72 -20
- package/template/wall-e/coding/stream-processor.js +22 -2
- package/template/wall-e/coding-orchestrator.js +26 -6
- package/template/wall-e/eval/agent-runner.js +16 -4
- package/template/wall-e/eval/benchmark-generator.js +21 -1
- package/template/wall-e/eval/benchmarks/coding-agent.json +0 -596
- package/template/wall-e/eval/codex-cli-baseline.js +633 -0
- package/template/wall-e/eval/eval-orchestrator.js +3 -3
- package/template/wall-e/eval/run-agent-benchmarks.js +11 -3
- package/template/wall-e/eval/run-codex-cli-baseline.js +177 -0
- package/template/wall-e/lib/mcp-integration.js +220 -0
- package/template/wall-e/llm/ollama.js +47 -8
- package/template/wall-e/llm/ollama.plugin.json +1 -1
- package/template/wall-e/llm/tool-adapter.js +1 -0
- package/template/wall-e/loops/ingest.js +42 -8
- package/template/wall-e/mcp-server.js +272 -10
- package/template/wall-e/memory/ctm-session-context.js +910 -0
- package/template/wall-e/server.js +26 -1
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/SKILL.md +20 -0
- package/template/wall-e/skills/_bundled/scan-ctm-sessions/run.js +43 -0
- package/template/wall-e/skills/skill-planner.js +52 -3
- package/template/wall-e/tools/builtin-middleware.js +55 -2
- package/template/wall-e/tools/shell-policy.js +1 -1
- package/template/wall-e/tools/slack-owner.js +104 -0
- package/template/website/index.html +2 -2
- package/template/builder-journal.md +0 -17
|
@@ -35,6 +35,7 @@ const {
|
|
|
35
35
|
getCodexThreadById,
|
|
36
36
|
getCodexThreadResumeCwd,
|
|
37
37
|
getResumeSpec,
|
|
38
|
+
listCodexSessionsFromRollouts,
|
|
38
39
|
parseSessionStartMs,
|
|
39
40
|
parseCodexJsonlIntoMessages,
|
|
40
41
|
parseCodexJsonlFileIntoMessages,
|
|
@@ -47,6 +48,13 @@ const { JsonlWatcher } = require('./lib/fs-watcher');
|
|
|
47
48
|
const compactStitch = require('./lib/compact-stitch');
|
|
48
49
|
const { registerSessionJobs, registerStreamJobs } = require('./lib/session-jobs');
|
|
49
50
|
const { SessionStream } = require('./lib/session-stream');
|
|
51
|
+
const { buildSessionStandupSnapshot } = require('./lib/session-standup');
|
|
52
|
+
const {
|
|
53
|
+
buildStandupAttentionContext,
|
|
54
|
+
heuristicStandupAttention,
|
|
55
|
+
mergeStickyStandupAttention,
|
|
56
|
+
parseStandupAttentionResult,
|
|
57
|
+
} = require('./lib/standup-attention');
|
|
50
58
|
const { getAllSessionFilesAsync: getAllSessionFilesAsyncFromUtils } = require('./session-utils');
|
|
51
59
|
const { SessionCapture } = require('./lib/session-capture');
|
|
52
60
|
const { resolveFallback: resolveLaunchFallback, isEarlyExit } = require('./lib/launch-presets');
|
|
@@ -62,8 +70,14 @@ const claudeDesktopSessions = require('./lib/claude-desktop-sessions');
|
|
|
62
70
|
const SessionSearchUtils = require('./public/js/session-search-utils.js');
|
|
63
71
|
const agentHooksInstaller = require('./lib/agent-hooks-installer');
|
|
64
72
|
const statusHooks = require('./lib/status-hooks');
|
|
73
|
+
const { ensureWalleMcpForAgentSession } = require('./lib/walle-mcp-auto-config');
|
|
65
74
|
const { getStateDetector } = require('./workers/state-detectors');
|
|
66
75
|
const { canResumeAgent, getAgentCapabilities, normalizeAgentType } = require('./lib/agent-capabilities');
|
|
76
|
+
const {
|
|
77
|
+
clientUpdateTelemetryEvent,
|
|
78
|
+
errorKind: updateTelemetryErrorKind,
|
|
79
|
+
trackUpdateTelemetry,
|
|
80
|
+
} = require('./lib/update-telemetry');
|
|
67
81
|
const { resolveWalleChatContext } = require('./lib/walle-session-context');
|
|
68
82
|
const {
|
|
69
83
|
applyWalleToolEvent,
|
|
@@ -89,6 +103,7 @@ const {
|
|
|
89
103
|
loadConfig,
|
|
90
104
|
ensureWalleToken,
|
|
91
105
|
} = require('./lib/server-config');
|
|
106
|
+
const { resolveWalleDefaultModelSelection } = require('./lib/walle-default-model');
|
|
92
107
|
const { createWalleSupervisor } = require('./lib/walle-supervisor');
|
|
93
108
|
const oauthProxySupervisor = require('./lib/oauth-proxy-supervisor');
|
|
94
109
|
|
|
@@ -163,7 +178,7 @@ function _storedProviderKey(brain, type) {
|
|
|
163
178
|
}
|
|
164
179
|
}
|
|
165
180
|
|
|
166
|
-
async function generateSessionSummaryWithWalleProvider({ turnsText, sysPrompt }) {
|
|
181
|
+
async function generateSessionSummaryWithWalleProvider({ turnsText, sysPrompt, maxTokens = 60 }) {
|
|
167
182
|
const brain = getWalleBrain();
|
|
168
183
|
const provider = sanitizeSetupProviderType(
|
|
169
184
|
brain?.getKv?.('walle_provider') || process.env.WALLE_PROVIDER || 'anthropic'
|
|
@@ -209,7 +224,7 @@ async function generateSessionSummaryWithWalleProvider({ turnsText, sysPrompt })
|
|
|
209
224
|
model: model || undefined,
|
|
210
225
|
system: sysPrompt,
|
|
211
226
|
messages: [{ role: 'user', content: turnsText }],
|
|
212
|
-
maxTokens
|
|
227
|
+
maxTokens,
|
|
213
228
|
temperature: 0.2,
|
|
214
229
|
thinking: 'disabled',
|
|
215
230
|
reasoningEffort: 'low',
|
|
@@ -294,10 +309,36 @@ function _isBusyStatusPtyChunk(session, data) {
|
|
|
294
309
|
return !!detector.isBusyStatusChunk(filtered);
|
|
295
310
|
}
|
|
296
311
|
|
|
312
|
+
const CTM_UI_REFRESH_ACTIVITY_SUPPRESS_MS = 2500;
|
|
313
|
+
|
|
314
|
+
function _markCtmUiRefreshActivitySuppression(session, source) {
|
|
315
|
+
if (!session) return;
|
|
316
|
+
session._ctmUiRefreshActivitySuppressUntil = Date.now() + CTM_UI_REFRESH_ACTIVITY_SUPPRESS_MS;
|
|
317
|
+
session._ctmUiRefreshActivitySuppressSource = source || 'ui-refresh';
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function _suppressCtmUiRefreshActivity(session, statusOnlyChunk) {
|
|
321
|
+
if (!session || !statusOnlyChunk) return false;
|
|
322
|
+
const until = session._ctmUiRefreshActivitySuppressUntil || 0;
|
|
323
|
+
return until && Date.now() < until;
|
|
324
|
+
}
|
|
325
|
+
|
|
297
326
|
function _isServerWaitingForInput(sessionId, session) {
|
|
298
327
|
return !!(session?._waitingForInput || idleNotifyState.get(sessionId)?.notified);
|
|
299
328
|
}
|
|
300
329
|
|
|
330
|
+
function _inputMayResolveWaiting(data, session) {
|
|
331
|
+
const s = String(data || '');
|
|
332
|
+
if (!s) return false;
|
|
333
|
+
if (s.includes('\r') || s.includes('\n')) return true;
|
|
334
|
+
if (s.includes('\x03') || s.includes('\x04')) return true; // Ctrl-C / Ctrl-D
|
|
335
|
+
const reason = session?._waitingForInputReason || '';
|
|
336
|
+
if (reason === 'approval' || reason === 'choice') {
|
|
337
|
+
return /^[\s]*[12yYnN\u001b][\s]*$/.test(s);
|
|
338
|
+
}
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
301
342
|
function resolveAgentCliCommand(cmd) {
|
|
302
343
|
return agentCliCache.resolveCliCommand(cmd, { cacheFile: AGENT_CLI_CACHE_FILE });
|
|
303
344
|
}
|
|
@@ -2970,6 +3011,9 @@ async function handleApi(req, res, url) {
|
|
|
2970
3011
|
if (url.pathname === '/api/recent-sessions' && req.method === 'GET') {
|
|
2971
3012
|
return apiRecentSessions(req, res, url);
|
|
2972
3013
|
}
|
|
3014
|
+
if (url.pathname === '/api/sessions/standup' && req.method === 'GET') {
|
|
3015
|
+
return apiSessionStandup(req, res, url);
|
|
3016
|
+
}
|
|
2973
3017
|
if (url.pathname === '/api/sessions/git-status' && req.method === 'GET') {
|
|
2974
3018
|
return apiSessionsGitStatus(req, res, url);
|
|
2975
3019
|
}
|
|
@@ -3165,13 +3209,19 @@ async function handleApi(req, res, url) {
|
|
|
3165
3209
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3166
3210
|
return res.end(JSON.stringify({ sessions: statuses }));
|
|
3167
3211
|
}
|
|
3212
|
+
if (url.pathname === '/api/app/version' && req.method === 'GET') {
|
|
3213
|
+
return apiAppVersion(req, res);
|
|
3214
|
+
}
|
|
3168
3215
|
// --- Update check endpoints ---
|
|
3169
3216
|
if (url.pathname === '/api/updates/check' && req.method === 'GET') {
|
|
3170
|
-
return apiUpdatesCheck(req, res);
|
|
3217
|
+
return apiUpdatesCheck(req, res, url);
|
|
3171
3218
|
}
|
|
3172
3219
|
if (url.pathname === '/api/updates/apply' && req.method === 'POST') {
|
|
3173
3220
|
return apiUpdatesApply(req, res);
|
|
3174
3221
|
}
|
|
3222
|
+
if (url.pathname === '/api/updates/telemetry' && req.method === 'POST') {
|
|
3223
|
+
return apiUpdatesTelemetry(req, res);
|
|
3224
|
+
}
|
|
3175
3225
|
|
|
3176
3226
|
if (url.pathname === '/api/restart/ctm' && req.method === 'POST') {
|
|
3177
3227
|
return apiRestartCtm(req, res);
|
|
@@ -3811,6 +3861,246 @@ async function apiSessionsGitStatus(req, res, url) {
|
|
|
3811
3861
|
}
|
|
3812
3862
|
}
|
|
3813
3863
|
|
|
3864
|
+
function _standupStreamSource() {
|
|
3865
|
+
return _sessionCapture || _sessionStream || null;
|
|
3866
|
+
}
|
|
3867
|
+
|
|
3868
|
+
function _standupSummaryFor(source, ctmSessionId, agentSessionId) {
|
|
3869
|
+
if (!source || typeof source.getSummary !== 'function') return null;
|
|
3870
|
+
const ids = [ctmSessionId, agentSessionId].filter(Boolean);
|
|
3871
|
+
for (const id of ids) {
|
|
3872
|
+
try {
|
|
3873
|
+
const summary = source.getSummary(id);
|
|
3874
|
+
if (summary) return summary;
|
|
3875
|
+
} catch {}
|
|
3876
|
+
}
|
|
3877
|
+
return null;
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
const _standupAttentionCache = new Map();
|
|
3881
|
+
|
|
3882
|
+
function _standupAttentionKey(session) {
|
|
3883
|
+
return String(session?.id || session?.ctmSessionId || session?.agentSessionId || session?.sessionId || '');
|
|
3884
|
+
}
|
|
3885
|
+
|
|
3886
|
+
function _pruneStandupAttentionCache(now = Date.now()) {
|
|
3887
|
+
if (_standupAttentionCache.size <= 250) return;
|
|
3888
|
+
for (const [key, value] of _standupAttentionCache) {
|
|
3889
|
+
if (_standupAttentionCache.size <= 200) break;
|
|
3890
|
+
if (!value?.updatedAt || now - value.updatedAt > 24 * 60 * 60 * 1000) {
|
|
3891
|
+
_standupAttentionCache.delete(key);
|
|
3892
|
+
}
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
|
|
3896
|
+
function _standupAttentionSystemPrompt() {
|
|
3897
|
+
return [
|
|
3898
|
+
'Classify whether a CTM coding session has a current attention issue.',
|
|
3899
|
+
'Return only compact JSON with keys: severity, recommendation, evidence, confidence.',
|
|
3900
|
+
'severity must be one of: none, warning, failure.',
|
|
3901
|
+
'Use failure only for an unresolved blocker/failure that currently prevents progress or needs operator action.',
|
|
3902
|
+
'Use warning for a real non-blocking warning, risk, or caution that should remain visible.',
|
|
3903
|
+
'Use none for historical warning/failure wording, resolved issues, normal progress, or generic status text.',
|
|
3904
|
+
'evidence must be an array of at most 3 short phrases copied or paraphrased from the context.',
|
|
3905
|
+
'confidence must be low, medium, or high.',
|
|
3906
|
+
].join(' ');
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3909
|
+
async function _classifyStandupAttentionWithAi(context) {
|
|
3910
|
+
if (!context?.text) return null;
|
|
3911
|
+
const result = await generateSessionSummaryWithWalleProvider({
|
|
3912
|
+
turnsText: context.text,
|
|
3913
|
+
sysPrompt: _standupAttentionSystemPrompt(),
|
|
3914
|
+
maxTokens: 220,
|
|
3915
|
+
});
|
|
3916
|
+
return parseStandupAttentionResult(result?.text, {
|
|
3917
|
+
source: 'ai',
|
|
3918
|
+
model: result?.model || '',
|
|
3919
|
+
});
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
async function _standupAttentionFor(session, summary, now = Date.now()) {
|
|
3923
|
+
const key = _standupAttentionKey(session);
|
|
3924
|
+
if (!key) return null;
|
|
3925
|
+
const context = buildStandupAttentionContext({ session, summary });
|
|
3926
|
+
const cached = _standupAttentionCache.get(key);
|
|
3927
|
+
if (cached && cached.hash === context.hash && cached.attention) {
|
|
3928
|
+
return cached.attention;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
let next = null;
|
|
3932
|
+
if (context.hasAttentionLanguage) {
|
|
3933
|
+
try {
|
|
3934
|
+
next = await _classifyStandupAttentionWithAi(context);
|
|
3935
|
+
} catch (e) {
|
|
3936
|
+
console.warn('[standup] attention classifier failed:', e.message || e);
|
|
3937
|
+
}
|
|
3938
|
+
}
|
|
3939
|
+
if (!next) next = heuristicStandupAttention(context);
|
|
3940
|
+
|
|
3941
|
+
const attention = mergeStickyStandupAttention(cached?.attention || null, next, context.text, now);
|
|
3942
|
+
_standupAttentionCache.set(key, {
|
|
3943
|
+
hash: context.hash,
|
|
3944
|
+
attention,
|
|
3945
|
+
updatedAt: now,
|
|
3946
|
+
});
|
|
3947
|
+
_pruneStandupAttentionCache(now);
|
|
3948
|
+
return attention;
|
|
3949
|
+
}
|
|
3950
|
+
|
|
3951
|
+
async function _applyStandupAttention(pairs, now = Date.now()) {
|
|
3952
|
+
for (const pair of pairs) {
|
|
3953
|
+
const attention = await _standupAttentionFor(pair.session, pair.summary || {}, now);
|
|
3954
|
+
if (!attention || attention.severity === 'none') continue;
|
|
3955
|
+
pair.session.attention = attention;
|
|
3956
|
+
if (pair.summary) pair.summary.attention = attention;
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
function _standupTimeMs(value) {
|
|
3961
|
+
if (!value) return 0;
|
|
3962
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
3963
|
+
const parsed = Date.parse(String(value));
|
|
3964
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
3965
|
+
}
|
|
3966
|
+
|
|
3967
|
+
function _normalizeStandupLiveStatus(status) {
|
|
3968
|
+
const text = String(status || '').toLowerCase();
|
|
3969
|
+
if (!text) return '';
|
|
3970
|
+
if (text === 'busy') return 'running';
|
|
3971
|
+
if (text === 'waiting-for-input') return 'waiting_input';
|
|
3972
|
+
if (['running', 'waiting', 'waiting_input', 'idle', 'exited'].includes(text)) return text;
|
|
3973
|
+
return '';
|
|
3974
|
+
}
|
|
3975
|
+
|
|
3976
|
+
const STANDUP_MIN_RUNNING_HOLD_MS = 5000;
|
|
3977
|
+
|
|
3978
|
+
function _standupProviderIdForSession(session) {
|
|
3979
|
+
if (!session) return '';
|
|
3980
|
+
return session._providerId || _providerIdFromCmd(session.cmd || '');
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3983
|
+
function _standupRunningHoldMsForSession(session) {
|
|
3984
|
+
const providerId = _standupProviderIdForSession(session);
|
|
3985
|
+
let detectorMs = 0;
|
|
3986
|
+
try {
|
|
3987
|
+
const detector = getStateDetector(providerId);
|
|
3988
|
+
detectorMs = Number(detector && detector.idleDebounceMs) || 0;
|
|
3989
|
+
} catch {
|
|
3990
|
+
// Unknown providers fall back to the conservative minimum running hold.
|
|
3991
|
+
}
|
|
3992
|
+
return Math.max(STANDUP_MIN_RUNNING_HOLD_MS, detectorMs);
|
|
3993
|
+
}
|
|
3994
|
+
|
|
3995
|
+
function _standupWaitingReason(sessionId, session) {
|
|
3996
|
+
return String(session?._waitingForInputReason || idleNotifyState.get(sessionId)?.reason || '').toLowerCase();
|
|
3997
|
+
}
|
|
3998
|
+
|
|
3999
|
+
function _standupIsBlockingWaitingReason(reason) {
|
|
4000
|
+
return reason === 'approval' || reason === 'choice';
|
|
4001
|
+
}
|
|
4002
|
+
|
|
4003
|
+
function _standupHasRecentNonResolvingInput(session) {
|
|
4004
|
+
return !!(session?._waitingForInput &&
|
|
4005
|
+
session._lastNonResolvingWaitingInputAt &&
|
|
4006
|
+
(!session._lastWaitingResolveInputAt ||
|
|
4007
|
+
session._lastNonResolvingWaitingInputAt > session._lastWaitingResolveInputAt));
|
|
4008
|
+
}
|
|
4009
|
+
|
|
4010
|
+
function _standupLiveStatusForSession(session, now = Date.now()) {
|
|
4011
|
+
if (!session) return '';
|
|
4012
|
+
|
|
4013
|
+
// Wall-E chat tabs do not have PTYs. A non-generating chat is available for
|
|
4014
|
+
// the next message, not blocked waiting for operator input.
|
|
4015
|
+
if (session.type === 'walle') {
|
|
4016
|
+
return session.abortController ? 'running' : 'idle';
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
const providerId = _standupProviderIdForSession(session);
|
|
4020
|
+
const ptyActivity = _standupTimeMs(session.lastPtyActivity);
|
|
4021
|
+
const runningHoldMs = _standupRunningHoldMsForSession(session);
|
|
4022
|
+
const recentPtyActivity = !!(ptyActivity && (now - ptyActivity) < runningHoldMs);
|
|
4023
|
+
|
|
4024
|
+
if (_isServerWaitingForInput(session.id, session)) {
|
|
4025
|
+
const reason = _standupWaitingReason(session.id, session);
|
|
4026
|
+
if (providerId === 'codex' &&
|
|
4027
|
+
recentPtyActivity &&
|
|
4028
|
+
!_standupIsBlockingWaitingReason(reason) &&
|
|
4029
|
+
!_standupHasRecentNonResolvingInput(session)) {
|
|
4030
|
+
return 'running';
|
|
4031
|
+
}
|
|
4032
|
+
return 'waiting_input';
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
let busState = '';
|
|
4036
|
+
try {
|
|
4037
|
+
busState = statusHooks._bus && typeof statusHooks._bus.getState === 'function'
|
|
4038
|
+
? statusHooks._bus.getState(session.id)
|
|
4039
|
+
: '';
|
|
4040
|
+
} catch {}
|
|
4041
|
+
const liveBusStatus = _normalizeStandupLiveStatus(busState);
|
|
4042
|
+
if (liveBusStatus === 'running') return 'running';
|
|
4043
|
+
if (liveBusStatus === 'waiting_input') return 'waiting_input';
|
|
4044
|
+
|
|
4045
|
+
if (recentPtyActivity) return 'running';
|
|
4046
|
+
if (liveBusStatus) return liveBusStatus;
|
|
4047
|
+
|
|
4048
|
+
return '';
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
async function apiSessionStandup(req, res, url) {
|
|
4052
|
+
try {
|
|
4053
|
+
await _refreshSessionWorktreeStatusCache({ force: url.searchParams.get('force') === '1' });
|
|
4054
|
+
const source = _standupStreamSource();
|
|
4055
|
+
let statuses = [];
|
|
4056
|
+
if (source && typeof source.getAllStatuses === 'function') {
|
|
4057
|
+
try { statuses = source.getAllStatuses(); } catch (e) { console.error('[standup] status snapshot error:', e.message); }
|
|
4058
|
+
}
|
|
4059
|
+
|
|
4060
|
+
const summaries = [];
|
|
4061
|
+
const standupPairs = [];
|
|
4062
|
+
const now = Date.now();
|
|
4063
|
+
const activeSessions = [];
|
|
4064
|
+
for (const session of sessions.values()) {
|
|
4065
|
+
const payload = _sessionPayload(session);
|
|
4066
|
+
const agentSessionId = payload.agentSessionId || session._claudeSessionId || null;
|
|
4067
|
+
const rawSummary = _standupSummaryFor(source, payload.id, agentSessionId);
|
|
4068
|
+
const summary = rawSummary ? {
|
|
4069
|
+
...rawSummary,
|
|
4070
|
+
ctmSessionId: rawSummary.ctmSessionId || payload.id,
|
|
4071
|
+
agentSessionId: rawSummary.agentSessionId || agentSessionId || null,
|
|
4072
|
+
sessionId: rawSummary.sessionId || agentSessionId || payload.id,
|
|
4073
|
+
} : null;
|
|
4074
|
+
if (summary) summaries.push(summary);
|
|
4075
|
+
const standupSession = {
|
|
4076
|
+
...payload,
|
|
4077
|
+
serverState: statusHooks._bus && typeof statusHooks._bus.getState === 'function'
|
|
4078
|
+
? statusHooks._bus.getState(session.id)
|
|
4079
|
+
: null,
|
|
4080
|
+
standupStatus: _standupLiveStatusForSession(session, now),
|
|
4081
|
+
waitingForInput: _isServerWaitingForInput(session.id, session),
|
|
4082
|
+
waitingReason: session._waitingForInputReason || '',
|
|
4083
|
+
};
|
|
4084
|
+
activeSessions.push(standupSession);
|
|
4085
|
+
standupPairs.push({ session: standupSession, summary });
|
|
4086
|
+
}
|
|
4087
|
+
await _applyStandupAttention(standupPairs, now);
|
|
4088
|
+
|
|
4089
|
+
const snapshot = buildSessionStandupSnapshot({
|
|
4090
|
+
sessions: activeSessions,
|
|
4091
|
+
summaries,
|
|
4092
|
+
statuses,
|
|
4093
|
+
now,
|
|
4094
|
+
});
|
|
4095
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
4096
|
+
res.end(JSON.stringify(snapshot));
|
|
4097
|
+
} catch (e) {
|
|
4098
|
+
console.error('[standup] snapshot error:', e.stack || e.message || e);
|
|
4099
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
4100
|
+
res.end(JSON.stringify({ error: e.message || 'standup snapshot failed' }));
|
|
4101
|
+
}
|
|
4102
|
+
}
|
|
4103
|
+
|
|
3814
4104
|
function apiRecentSessions(req, res, url) {
|
|
3815
4105
|
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
|
|
3816
4106
|
const forceRescan = url.searchParams.get('rescan') === '1';
|
|
@@ -3827,45 +4117,52 @@ function apiRecentSessions(req, res, url) {
|
|
|
3827
4117
|
} catch { /* skip */ }
|
|
3828
4118
|
}
|
|
3829
4119
|
|
|
3830
|
-
// Include Codex sessions
|
|
4120
|
+
// Include Codex sessions. Primary: ~/.codex/state_5.sqlite. Fallback: scan ~/.codex/sessions/*.jsonl
|
|
4121
|
+
// (used when state_5.sqlite is corrupt or missing — avoids silent loss of all Codex sessions)
|
|
4122
|
+
let _codexThreadsFromDb = null;
|
|
3831
4123
|
try {
|
|
3832
4124
|
const codexDbPath = path.join(process.env.HOME, '.codex', 'state_5.sqlite');
|
|
3833
4125
|
if (fs.existsSync(codexDbPath)) {
|
|
3834
4126
|
const BetterSqlite3 = require('better-sqlite3');
|
|
3835
4127
|
const codexDb = new BetterSqlite3(codexDbPath, { readonly: true });
|
|
3836
|
-
|
|
4128
|
+
_codexThreadsFromDb = codexDb.prepare(
|
|
3837
4129
|
'SELECT id, title, first_user_message, model, cwd, git_branch, created_at, updated_at FROM threads ORDER BY updated_at DESC LIMIT ?'
|
|
3838
4130
|
).all(limit);
|
|
3839
4131
|
codexDb.close();
|
|
3840
|
-
const existingIds = new Set(recentSessions.map(s => s.sessionId));
|
|
3841
|
-
for (const t of codexThreads) {
|
|
3842
|
-
if (existingIds.has(t.id)) continue;
|
|
3843
|
-
const title = t.title || t.first_user_message || '';
|
|
3844
|
-
const titleTrimmed = title.slice(0, 80);
|
|
3845
|
-
const rolloutInfo = _codexRolloutInfo(t.id);
|
|
3846
|
-
const modifiedAt = rolloutInfo.modifiedAt || new Date(t.updated_at * 1000).toISOString();
|
|
3847
|
-
recentSessions.push({
|
|
3848
|
-
sessionId: t.id,
|
|
3849
|
-
project: t.cwd || '',
|
|
3850
|
-
projectEntry: '',
|
|
3851
|
-
cwd: t.cwd || '',
|
|
3852
|
-
firstMessage: t.first_user_message || '',
|
|
3853
|
-
title: titleTrimmed,
|
|
3854
|
-
aiTitle: titleTrimmed || undefined, // prevent re-generation
|
|
3855
|
-
isEmpty: !t.first_user_message,
|
|
3856
|
-
userMsgCount: t.first_user_message ? 1 : 0,
|
|
3857
|
-
modifiedAt,
|
|
3858
|
-
fileModifiedAt: rolloutInfo.modifiedAt || '',
|
|
3859
|
-
timestamp: new Date(t.created_at * 1000).toISOString(),
|
|
3860
|
-
version: '',
|
|
3861
|
-
gitBranch: t.git_branch || '',
|
|
3862
|
-
fileSize: rolloutInfo.fileSize || 0,
|
|
3863
|
-
agent: 'codex',
|
|
3864
|
-
model: t.model || '',
|
|
3865
|
-
});
|
|
3866
|
-
}
|
|
3867
4132
|
}
|
|
3868
|
-
} catch { /*
|
|
4133
|
+
} catch { /* state_5.sqlite unreadable — fall through to JSONL scan */ }
|
|
4134
|
+
|
|
4135
|
+
const _codexThreads = _codexThreadsFromDb
|
|
4136
|
+
|| listCodexSessionsFromRollouts(process.env.HOME, limit);
|
|
4137
|
+
|
|
4138
|
+
const existingIds = new Set(recentSessions.map(s => s.sessionId));
|
|
4139
|
+
for (const t of _codexThreads) {
|
|
4140
|
+
if (existingIds.has(t.id)) continue;
|
|
4141
|
+
const title = t.title || t.first_user_message || '';
|
|
4142
|
+
const titleTrimmed = title.slice(0, 80);
|
|
4143
|
+
const rolloutInfo = _codexRolloutInfo(t.id);
|
|
4144
|
+
const modifiedAt = rolloutInfo.modifiedAt || new Date(t.updated_at * 1000).toISOString();
|
|
4145
|
+
recentSessions.push({
|
|
4146
|
+
sessionId: t.id,
|
|
4147
|
+
project: t.cwd || '',
|
|
4148
|
+
projectEntry: '',
|
|
4149
|
+
cwd: t.cwd || '',
|
|
4150
|
+
firstMessage: t.first_user_message || '',
|
|
4151
|
+
title: titleTrimmed,
|
|
4152
|
+
aiTitle: titleTrimmed || undefined, // prevent re-generation
|
|
4153
|
+
isEmpty: !t.first_user_message,
|
|
4154
|
+
userMsgCount: t.first_user_message ? 1 : 0,
|
|
4155
|
+
modifiedAt,
|
|
4156
|
+
fileModifiedAt: rolloutInfo.modifiedAt || '',
|
|
4157
|
+
timestamp: new Date(t.created_at * 1000).toISOString(),
|
|
4158
|
+
version: '',
|
|
4159
|
+
gitBranch: t.git_branch || '',
|
|
4160
|
+
fileSize: rolloutInfo.fileSize || 0,
|
|
4161
|
+
agent: 'codex',
|
|
4162
|
+
model: t.model || '',
|
|
4163
|
+
});
|
|
4164
|
+
existingIds.add(t.id);
|
|
4165
|
+
}
|
|
3869
4166
|
|
|
3870
4167
|
// Populate ctm_sessions + agent_sessions from scanned sessions (wrap in transaction for perf)
|
|
3871
4168
|
// First, build a map of JSONL file IDs already claimed by CTM tab rows (where id != agent_session_id).
|
|
@@ -3879,6 +4176,20 @@ function apiRecentSessions(req, res, url) {
|
|
|
3879
4176
|
claimedMap[r.agent_session_id] = r.ctm_session_id;
|
|
3880
4177
|
}
|
|
3881
4178
|
} catch (e) { console.error('[session-index] claimedMap build error:', e.message); }
|
|
4179
|
+
try {
|
|
4180
|
+
const cols = new Set(db.prepare('PRAGMA table_info(startup_tasks)').all().map(c => c.name));
|
|
4181
|
+
const agentCols = [];
|
|
4182
|
+
if (cols.has('agent_session_id')) agentCols.push('agent_session_id');
|
|
4183
|
+
if (cols.has('claude_session_id')) agentCols.push('claude_session_id');
|
|
4184
|
+
if (cols.has('ctm_session_id') && agentCols.length) {
|
|
4185
|
+
const selectCols = ['ctm_session_id', ...agentCols].join(', ');
|
|
4186
|
+
for (const r of db.prepare(`SELECT ${selectCols} FROM startup_tasks WHERE ctm_session_id IS NOT NULL`).all()) {
|
|
4187
|
+
for (const col of agentCols) {
|
|
4188
|
+
if (r[col] && !claimedMap[r[col]]) claimedMap[r[col]] = r.ctm_session_id;
|
|
4189
|
+
}
|
|
4190
|
+
}
|
|
4191
|
+
}
|
|
4192
|
+
} catch (e) { console.error('[session-index] startup claimedMap build error:', e.message); }
|
|
3882
4193
|
// Also build a reverse map: CTM tab id → { agentId, fileSize, projectEntry, createdAt }
|
|
3883
4194
|
// so we can detect stale links (tab claims a tiny file when a larger continuation exists)
|
|
3884
4195
|
const tabClaimInfo = {}; // CTM tab id → { agentId, fileSize, projectEntry, createdAt }
|
|
@@ -3981,7 +4292,7 @@ function apiRecentSessions(req, res, url) {
|
|
|
3981
4292
|
if (row.id) { sessionsMap[row.id] = row; idMapped.add(row.id); }
|
|
3982
4293
|
}
|
|
3983
4294
|
for (const row of allSessionRows) {
|
|
3984
|
-
if (row.agent_session_id && !idMapped.has(row.agent_session_id)) {
|
|
4295
|
+
if (row.agent_session_id && (!idMapped.has(row.agent_session_id) || _preferAgentOwnerRow(sessionsMap[row.agent_session_id], row, row.agent_session_id))) {
|
|
3985
4296
|
sessionsMap[row.agent_session_id] = row;
|
|
3986
4297
|
}
|
|
3987
4298
|
}
|
|
@@ -4028,7 +4339,7 @@ function apiRecentSessions(req, res, url) {
|
|
|
4028
4339
|
// columns so post-compact continuations inherit a useful title.
|
|
4029
4340
|
const rowForTitle = {
|
|
4030
4341
|
user_renamed: s.userRenamed ? 1 : 0,
|
|
4031
|
-
title: s.title || '',
|
|
4342
|
+
title: s.aiTitle || s.title || '',
|
|
4032
4343
|
rename_name: dbRow ? dbRow.rename_name : '',
|
|
4033
4344
|
first_message: s.firstMessage || '',
|
|
4034
4345
|
last_user_content: dbRow ? dbRow.last_user_content : '',
|
|
@@ -4302,6 +4613,7 @@ function _cacheParsedCodexMessages(ctmSessionId, parsedFiles) {
|
|
|
4302
4613
|
const agentSessionId = meta.id || _codexAgentIdFromRolloutPath(item.fp);
|
|
4303
4614
|
const fileMessages = Array.isArray(item.messages) ? item.messages : [];
|
|
4304
4615
|
if (!agentSessionId || fileMessages.length === 0) continue;
|
|
4616
|
+
const ownerCtmSessionId = _resolveCodexOwnerCtmSessionId(ctmSessionId, agentSessionId);
|
|
4305
4617
|
|
|
4306
4618
|
const users = fileMessages.filter(m => m.role === 'user' && m.text);
|
|
4307
4619
|
const assistants = fileMessages.filter(m => m.role === 'assistant' && m.text);
|
|
@@ -4338,13 +4650,14 @@ function _cacheParsedCodexMessages(ctmSessionId, parsedFiles) {
|
|
|
4338
4650
|
const st = fs.statSync(item.fp);
|
|
4339
4651
|
const modifiedAt = st.mtime ? st.mtime.toISOString() : '';
|
|
4340
4652
|
const projectDir = path.dirname(item.fp);
|
|
4341
|
-
|
|
4342
|
-
dbModule.
|
|
4653
|
+
const indexTitle = _codexIndexTitleForSession(ownerCtmSessionId, title);
|
|
4654
|
+
dbModule.updateStartupTaskAgentSession(ownerCtmSessionId, agentSessionId, projectDir);
|
|
4655
|
+
dbModule.upsertSession(ownerCtmSessionId, {
|
|
4343
4656
|
agentSessionId,
|
|
4344
4657
|
provider: 'codex',
|
|
4345
4658
|
cwd: meta.cwd || '',
|
|
4346
4659
|
projectPath: meta.cwd || '',
|
|
4347
|
-
title,
|
|
4660
|
+
title: indexTitle,
|
|
4348
4661
|
jsonlPath: item.fp,
|
|
4349
4662
|
fileSize: item.size || st.size || item.parsed?.bytesRead || 0,
|
|
4350
4663
|
modifiedAt,
|
|
@@ -4352,7 +4665,7 @@ function _cacheParsedCodexMessages(ctmSessionId, parsedFiles) {
|
|
|
4352
4665
|
gitBranch: meta.git_branch || '',
|
|
4353
4666
|
hostname: os.hostname(),
|
|
4354
4667
|
userMsgCount: users.length,
|
|
4355
|
-
});
|
|
4668
|
+
}, ownerCtmSessionId && ownerCtmSessionId !== agentSessionId ? { allowReclaim: true } : undefined);
|
|
4356
4669
|
} catch (e) {
|
|
4357
4670
|
console.error(`[session-messages] Codex index update failed for ${agentSessionId.slice(0, 8)}:`, e.message);
|
|
4358
4671
|
}
|
|
@@ -4771,6 +5084,16 @@ function _codexThreadTitle(thread) {
|
|
|
4771
5084
|
return title ? title.slice(0, 80) : '';
|
|
4772
5085
|
}
|
|
4773
5086
|
|
|
5087
|
+
function _preferAgentOwnerRow(existing, row, agentSessionId) {
|
|
5088
|
+
if (!row || !agentSessionId) return false;
|
|
5089
|
+
if (!existing) return true;
|
|
5090
|
+
const rowIsOwner = row.id && row.id !== agentSessionId;
|
|
5091
|
+
const existingIsStandaloneAgent = existing.id === agentSessionId;
|
|
5092
|
+
if (rowIsOwner && existingIsStandaloneAgent && (row.user_renamed || !existing.user_renamed)) return true;
|
|
5093
|
+
if (row.user_renamed && !existing.user_renamed) return true;
|
|
5094
|
+
return false;
|
|
5095
|
+
}
|
|
5096
|
+
|
|
4774
5097
|
function _isKnownCodexThreadId(agentSessionId) {
|
|
4775
5098
|
if (!agentSessionId) return false;
|
|
4776
5099
|
if (getCodexThreadById(agentSessionId)) return true;
|
|
@@ -4877,6 +5200,64 @@ function _existingAgentOwner(agentSessionId) {
|
|
|
4877
5200
|
}
|
|
4878
5201
|
}
|
|
4879
5202
|
|
|
5203
|
+
function _startupTaskOwnerForAgent(agentSessionId) {
|
|
5204
|
+
if (!agentSessionId) return '';
|
|
5205
|
+
try {
|
|
5206
|
+
const db = dbModule.getDb();
|
|
5207
|
+
const cols = new Set(db.prepare('PRAGMA table_info(startup_tasks)').all().map(c => c.name));
|
|
5208
|
+
const clauses = [];
|
|
5209
|
+
const params = [];
|
|
5210
|
+
if (cols.has('agent_session_id')) {
|
|
5211
|
+
clauses.push('agent_session_id = ?');
|
|
5212
|
+
params.push(agentSessionId);
|
|
5213
|
+
}
|
|
5214
|
+
if (cols.has('claude_session_id')) {
|
|
5215
|
+
clauses.push('claude_session_id = ?');
|
|
5216
|
+
params.push(agentSessionId);
|
|
5217
|
+
}
|
|
5218
|
+
if (clauses.length === 0) return '';
|
|
5219
|
+
const row = db.prepare(`SELECT ctm_session_id FROM startup_tasks WHERE ${clauses.join(' OR ')} ORDER BY created_at DESC LIMIT 1`).get(...params);
|
|
5220
|
+
return row?.ctm_session_id || '';
|
|
5221
|
+
} catch {
|
|
5222
|
+
return '';
|
|
5223
|
+
}
|
|
5224
|
+
}
|
|
5225
|
+
|
|
5226
|
+
function _resolveCodexOwnerCtmSessionId(requestedSessionId, agentSessionId) {
|
|
5227
|
+
if (agentSessionId) {
|
|
5228
|
+
const owner = _existingAgentOwner(agentSessionId)?.ctm_session_id || _startupTaskOwnerForAgent(agentSessionId);
|
|
5229
|
+
if (owner && owner !== agentSessionId) return owner;
|
|
5230
|
+
}
|
|
5231
|
+
if (requestedSessionId && sessions.has(requestedSessionId)) return requestedSessionId;
|
|
5232
|
+
try {
|
|
5233
|
+
const task = dbModule.getStartupTask ? dbModule.getStartupTask(requestedSessionId) : null;
|
|
5234
|
+
if (task?.ctm_session_id || task?.session_id) return task.ctm_session_id || task.session_id;
|
|
5235
|
+
} catch {}
|
|
5236
|
+
try {
|
|
5237
|
+
const resolved = dbModule.resolveSession(requestedSessionId);
|
|
5238
|
+
if (resolved?.id) return resolved.id;
|
|
5239
|
+
} catch {}
|
|
5240
|
+
return requestedSessionId || agentSessionId || '';
|
|
5241
|
+
}
|
|
5242
|
+
|
|
5243
|
+
function _codexIndexTitleForSession(ctmSessionId, candidateTitle) {
|
|
5244
|
+
let existingTitle = null;
|
|
5245
|
+
try { existingTitle = dbModule.getSessionTitleNew(ctmSessionId); } catch {}
|
|
5246
|
+
const liveSession = sessions.get(ctmSessionId) || null;
|
|
5247
|
+
const candidate = (candidateTitle || '').trim();
|
|
5248
|
+
if (candidate && shouldApplyCodexAutoTitle({ session: liveSession || { label: existingTitle?.title || '' }, existingTitle })) {
|
|
5249
|
+
return candidate;
|
|
5250
|
+
}
|
|
5251
|
+
return existingTitle?.title || liveSession?.label || candidate;
|
|
5252
|
+
}
|
|
5253
|
+
|
|
5254
|
+
function _shouldReclaimStandaloneAgentOwner(agentSessionId, ctmSessionId) {
|
|
5255
|
+
if (!agentSessionId || !ctmSessionId || agentSessionId === ctmSessionId) return false;
|
|
5256
|
+
if (sessions.has(agentSessionId)) return false;
|
|
5257
|
+
const owner = _existingAgentOwner(agentSessionId);
|
|
5258
|
+
return owner?.ctm_session_id === agentSessionId;
|
|
5259
|
+
}
|
|
5260
|
+
|
|
4880
5261
|
function _ensureCodexThreadLink(ctmSessionId, source) {
|
|
4881
5262
|
if (!ctmSessionId) return null;
|
|
4882
5263
|
|
|
@@ -5003,7 +5384,7 @@ function _linkCodexThreadToSession(ctmSessionId, liveSession, thread, source) {
|
|
|
5003
5384
|
model: thread.model || liveSession.model_id || '',
|
|
5004
5385
|
gitBranch: thread.git_branch || liveSession.branch || '',
|
|
5005
5386
|
hostname: os.hostname(),
|
|
5006
|
-
});
|
|
5387
|
+
}, _shouldReclaimStandaloneAgentOwner(agentSessionId, ctmSessionId) ? { allowReclaim: true } : undefined);
|
|
5007
5388
|
} catch (e) {
|
|
5008
5389
|
console.error('[codex-link] upsertSession error:', e.message);
|
|
5009
5390
|
}
|
|
@@ -5461,7 +5842,7 @@ function apiSessionSearch(req, res, url) {
|
|
|
5461
5842
|
if (row.id) { _searchSessionsMap[row.id] = row; _searchIdMapped.add(row.id); }
|
|
5462
5843
|
}
|
|
5463
5844
|
for (const row of _searchRows) {
|
|
5464
|
-
if (row.agent_session_id && !_searchIdMapped.has(row.agent_session_id)) {
|
|
5845
|
+
if (row.agent_session_id && (!_searchIdMapped.has(row.agent_session_id) || _preferAgentOwnerRow(_searchSessionsMap[row.agent_session_id], row, row.agent_session_id))) {
|
|
5465
5846
|
_searchSessionsMap[row.agent_session_id] = row;
|
|
5466
5847
|
}
|
|
5467
5848
|
}
|
|
@@ -5891,7 +6272,7 @@ ${summaryText}`;
|
|
|
5891
6272
|
delete env.CLAUDE_CODE;
|
|
5892
6273
|
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
5893
6274
|
delete env.CLAUDE_CODE_ENABLE_TELEMETRY;
|
|
5894
|
-
sanitizeAnthropicAuth(env);
|
|
6275
|
+
sanitizeAnthropicAuth(env, 'claude');
|
|
5895
6276
|
let result;
|
|
5896
6277
|
result = require('child_process').spawnSync('claude', ['-p', prompt], {
|
|
5897
6278
|
encoding: 'utf8',
|
|
@@ -5969,7 +6350,7 @@ function callClaude(prompt) {
|
|
|
5969
6350
|
delete env.CLAUDE_CODE;
|
|
5970
6351
|
delete env.CLAUDE_CODE_ENTRYPOINT;
|
|
5971
6352
|
delete env.CLAUDE_CODE_ENABLE_TELEMETRY;
|
|
5972
|
-
sanitizeAnthropicAuth(env);
|
|
6353
|
+
sanitizeAnthropicAuth(env, 'claude');
|
|
5973
6354
|
const result = spawnSync('claude', ['-p', prompt], {
|
|
5974
6355
|
encoding: 'utf8', timeout: 60000, env, maxBuffer: 1024 * 1024,
|
|
5975
6356
|
});
|
|
@@ -6042,13 +6423,16 @@ function apiRenameSession(req, res) {
|
|
|
6042
6423
|
const trimmed = title.trim().slice(0, 120);
|
|
6043
6424
|
dbModule.setSessionTitleNew(sessionId, trimmed, true);
|
|
6044
6425
|
|
|
6426
|
+
const liveSession = findLiveSessionByAnyId(sessionId);
|
|
6427
|
+
const liveSessionId = liveSession ? liveSession.id : sessionId;
|
|
6428
|
+
|
|
6045
6429
|
// Update startup_tasks so the label survives CTM restart
|
|
6046
|
-
try { dbModule.updateStartupTaskLabel(
|
|
6430
|
+
try { dbModule.updateStartupTaskLabel(liveSessionId, trimmed); } catch (e) { console.error('[ctm] updateStartupTaskLabel error:', e.message); }
|
|
6047
6431
|
|
|
6048
6432
|
// Update in-memory session if active
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6433
|
+
if (liveSession) {
|
|
6434
|
+
liveSession.label = trimmed;
|
|
6435
|
+
liveSession.userRenamed = true;
|
|
6052
6436
|
broadcastSessionList();
|
|
6053
6437
|
}
|
|
6054
6438
|
|
|
@@ -6061,6 +6445,15 @@ function apiRenameSession(req, res) {
|
|
|
6061
6445
|
});
|
|
6062
6446
|
}
|
|
6063
6447
|
|
|
6448
|
+
function findLiveSessionByAnyId(id) {
|
|
6449
|
+
const direct = sessions.get(id);
|
|
6450
|
+
if (direct) return direct;
|
|
6451
|
+
for (const session of sessions.values()) {
|
|
6452
|
+
if (session && session._claudeSessionId === id) return session;
|
|
6453
|
+
}
|
|
6454
|
+
return null;
|
|
6455
|
+
}
|
|
6456
|
+
|
|
6064
6457
|
function apiGenerateTitles(req, res) {
|
|
6065
6458
|
let body = '';
|
|
6066
6459
|
req.on('data', chunk => { body += chunk; if (body.length > 1024 * 1024) { req.destroy(); return; } });
|
|
@@ -7006,7 +7399,12 @@ function _markServerSessionResumedFromPty(sessionId, session, source) {
|
|
|
7006
7399
|
}
|
|
7007
7400
|
if (session) {
|
|
7008
7401
|
session._waitingForInput = false;
|
|
7402
|
+
session._waitingForInputReason = '';
|
|
7403
|
+
session._waitingForInputAt = 0;
|
|
7404
|
+
session._lastNonResolvingWaitingInputAt = 0;
|
|
7405
|
+
session._lastWaitingResolveInputAt = now;
|
|
7009
7406
|
session.lastActivity = now;
|
|
7407
|
+
session.lastPtyActivity = now;
|
|
7010
7408
|
}
|
|
7011
7409
|
broadcastToAll({ type: 'session-resumed', id: sessionId, source, timestamp: now });
|
|
7012
7410
|
if (telemetryReceiver.hasAuthoritativeSource(sessionId)) {
|
|
@@ -7045,14 +7443,24 @@ function checkIdleNotify(sessionId, session, data) {
|
|
|
7045
7443
|
/\w/.test(freshStripped) &&
|
|
7046
7444
|
freshStripped.length > 5;
|
|
7047
7445
|
if (hasWords && st.notified) {
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7446
|
+
const hasUnsubmittedWaitingInput = !!(session?._waitingForInput &&
|
|
7447
|
+
session._lastNonResolvingWaitingInputAt &&
|
|
7448
|
+
(!session._lastWaitingResolveInputAt ||
|
|
7449
|
+
session._lastNonResolvingWaitingInputAt > session._lastWaitingResolveInputAt));
|
|
7450
|
+
if (!hasUnsubmittedWaitingInput) {
|
|
7451
|
+
st.notified = false;
|
|
7452
|
+
if (session) {
|
|
7453
|
+
session._waitingForInput = false;
|
|
7454
|
+
session._waitingForInputReason = '';
|
|
7455
|
+
session._waitingForInputAt = 0;
|
|
7456
|
+
}
|
|
7457
|
+
// Tell clients this session is no longer idle — clears _waitingForInput
|
|
7458
|
+
broadcastToAll({ type: 'session-resumed', id: sessionId });
|
|
7459
|
+
// If a provider hook missed the next start transition, real terminal
|
|
7460
|
+
// output is enough to unstick the sidebar until the hook catches up.
|
|
7461
|
+
if (hasAuthoritativeSource) {
|
|
7462
|
+
broadcastToAll({ type: 'session.status', id: sessionId, working: true, source: 'terminal-output', timestamp: Date.now() });
|
|
7463
|
+
}
|
|
7056
7464
|
}
|
|
7057
7465
|
}
|
|
7058
7466
|
|
|
@@ -7086,7 +7494,13 @@ function checkIdleNotify(sessionId, session, data) {
|
|
|
7086
7494
|
}
|
|
7087
7495
|
if (!st.notified) {
|
|
7088
7496
|
st.notified = true;
|
|
7089
|
-
if (session)
|
|
7497
|
+
if (session) {
|
|
7498
|
+
session._waitingForInput = true;
|
|
7499
|
+
session._waitingForInputReason = reason;
|
|
7500
|
+
session._waitingForInputAt = Date.now();
|
|
7501
|
+
session._lastNonResolvingWaitingInputAt = 0;
|
|
7502
|
+
session._lastWaitingResolveInputAt = 0;
|
|
7503
|
+
}
|
|
7090
7504
|
// Provider hooks are useful, but a visible prompt is stronger evidence
|
|
7091
7505
|
// than a stale "working" hook that never received its stop event.
|
|
7092
7506
|
if (hasAuthoritativeSource) {
|
|
@@ -7334,7 +7748,8 @@ function handleCreate(ws, msg) {
|
|
|
7334
7748
|
|
|
7335
7749
|
const cols = msg.cols || 120;
|
|
7336
7750
|
const rows = msg.rows || 30;
|
|
7337
|
-
|
|
7751
|
+
// Keep PWD aligned with the PTY cwd; some agent CLIs trust env.PWD over getcwd().
|
|
7752
|
+
const env = { ...process.env, ...(msg._projectEnv || {}), ...msg.env, PWD: cwd };
|
|
7338
7753
|
// Remove Claude Code env vars so spawned sessions don't think they're nested
|
|
7339
7754
|
delete env.CLAUDECODE;
|
|
7340
7755
|
delete env.CLAUDE_CODE;
|
|
@@ -7345,6 +7760,12 @@ function handleCreate(ws, msg) {
|
|
|
7345
7760
|
// This adds OTEL_EXPORTER_OTLP_ENDPOINT → http://localhost:<PORT> for agents
|
|
7346
7761
|
// that emit OTLP logs, plus CTM_SESSION_ID for our hook scripts to read.
|
|
7347
7762
|
Object.assign(env, buildTelemetryEnv(id, cmd, PORT));
|
|
7763
|
+
ensureWalleMcpForAgentSession({
|
|
7764
|
+
cmd,
|
|
7765
|
+
wallePort: WALLE_PORT,
|
|
7766
|
+
homeDir: env.HOME || process.env.HOME,
|
|
7767
|
+
env,
|
|
7768
|
+
});
|
|
7348
7769
|
|
|
7349
7770
|
// Model: explicit selection, or auto-detected from coding agent config
|
|
7350
7771
|
let model_id = msg.model_id || null;
|
|
@@ -7389,6 +7810,11 @@ function handleCreate(ws, msg) {
|
|
|
7389
7810
|
if (agentType === 'walle') {
|
|
7390
7811
|
// Override auto-generated shell label if user didn't provide one
|
|
7391
7812
|
if (!msg.label) label = 'Wall-E session';
|
|
7813
|
+
if (!model_id) {
|
|
7814
|
+
const defaults = resolveWalleDefaultModelSelection({ brain: getWalleBrain(), env: process.env });
|
|
7815
|
+
model_id = defaults.model_id || null;
|
|
7816
|
+
model_provider = defaults.model_provider || null;
|
|
7817
|
+
}
|
|
7392
7818
|
const chatSessionId = msg.chatSessionId || `walle-${id}`;
|
|
7393
7819
|
const jsonlPath = walleTranscript.sessionPath(WALLE_SESSIONS_DIR, id);
|
|
7394
7820
|
const createResult = walleTranscript.createSession(jsonlPath, {
|
|
@@ -7437,7 +7863,15 @@ function handleCreate(ws, msg) {
|
|
|
7437
7863
|
}
|
|
7438
7864
|
|
|
7439
7865
|
if (ws) {
|
|
7440
|
-
ws.send(JSON.stringify({
|
|
7866
|
+
ws.send(JSON.stringify({
|
|
7867
|
+
type: 'created',
|
|
7868
|
+
id,
|
|
7869
|
+
label: session.label,
|
|
7870
|
+
cwd,
|
|
7871
|
+
sessionType: 'walle',
|
|
7872
|
+
model_id: session.model_id || null,
|
|
7873
|
+
model_provider: session.model_provider || null,
|
|
7874
|
+
}));
|
|
7441
7875
|
}
|
|
7442
7876
|
broadcastSessionList(true);
|
|
7443
7877
|
return;
|
|
@@ -7500,8 +7934,10 @@ function handleCreate(ws, msg) {
|
|
|
7500
7934
|
createdAt: sessionSpawnedAt,
|
|
7501
7935
|
_ctmOriginalCreatedAt: sessionOriginalCreatedAt,
|
|
7502
7936
|
lastActivity: sessionSpawnedAt,
|
|
7937
|
+
lastPtyActivity: 0,
|
|
7503
7938
|
model_id,
|
|
7504
7939
|
model_provider,
|
|
7940
|
+
userRenamed: !!(existingTitle && existingTitle.userRenamed),
|
|
7505
7941
|
_claudeSessionId: claudeResumeId || _injectedSessionId || null,
|
|
7506
7942
|
// When we injected --session-id, the JSONL path is deterministic. Setting
|
|
7507
7943
|
// _claudeProjectDir now lets the post-spawn detection timers short-circuit
|
|
@@ -7791,9 +8227,14 @@ function handleCreate(ws, msg) {
|
|
|
7791
8227
|
const statusOnlyChunk = _isStatusOnlyPtyChunk(session, data);
|
|
7792
8228
|
const busyStatusChunk = _isBusyStatusPtyChunk(session, data);
|
|
7793
8229
|
const waitingForInput = _isServerWaitingForInput(id, session);
|
|
7794
|
-
const
|
|
7795
|
-
|
|
7796
|
-
if (
|
|
8230
|
+
const suppressedUiRefreshChunk = _suppressCtmUiRefreshActivity(session, statusOnlyChunk);
|
|
8231
|
+
const activityChunk = activeChunk && !suppressedUiRefreshChunk && !(statusOnlyChunk && waitingForInput && !busyStatusChunk);
|
|
8232
|
+
if (busyStatusChunk && waitingForInput && !suppressedUiRefreshChunk) _markServerSessionResumedFromPty(id, session, 'codex-working-status');
|
|
8233
|
+
if (activityChunk) {
|
|
8234
|
+
const now = Date.now();
|
|
8235
|
+
session.lastActivity = now;
|
|
8236
|
+
session.lastPtyActivity = now;
|
|
8237
|
+
}
|
|
7797
8238
|
// Monotonic output byte counter — read by approval-agent's Phase 3
|
|
7798
8239
|
// post-keystroke verification (sendApprovalKeystroke in approval-agent.js).
|
|
7799
8240
|
session._outputBytesCounter = (session._outputBytesCounter || 0) + data.length;
|
|
@@ -8234,6 +8675,7 @@ function handleCreate(ws, msg) {
|
|
|
8234
8675
|
model_id: session.model_id || model_id,
|
|
8235
8676
|
model_provider: session.model_provider || model_provider,
|
|
8236
8677
|
branch,
|
|
8678
|
+
userRenamed: !!session.userRenamed,
|
|
8237
8679
|
claudeSessionId: session._claudeSessionId || null,
|
|
8238
8680
|
agentSessionId: session._claudeSessionId || null,
|
|
8239
8681
|
claudeProjectDir: session._claudeProjectDir ? path.basename(session._claudeProjectDir) : null,
|
|
@@ -8268,6 +8710,12 @@ function apiAttachSession(req, res) {
|
|
|
8268
8710
|
sanitizeAnthropicAuth(env, claudeCmd);
|
|
8269
8711
|
// Resume path: inject CTM telemetry env the same way as the create path.
|
|
8270
8712
|
Object.assign(env, buildTelemetryEnv(tabId, claudeCmd, PORT));
|
|
8713
|
+
ensureWalleMcpForAgentSession({
|
|
8714
|
+
cmd: claudeCmd,
|
|
8715
|
+
wallePort: WALLE_PORT,
|
|
8716
|
+
homeDir: env.HOME || process.env.HOME,
|
|
8717
|
+
env,
|
|
8718
|
+
});
|
|
8271
8719
|
|
|
8272
8720
|
// Restore .jsonl from .bak if needed (Claude Code migrates sessions to directory format)
|
|
8273
8721
|
const claudeProjectsDir = path.join(process.env.HOME, '.claude', 'projects');
|
|
@@ -8345,6 +8793,7 @@ function apiAttachSession(req, res) {
|
|
|
8345
8793
|
// scrollback removed — headless terminal is source of truth (Phase 3D)
|
|
8346
8794
|
createdAt: Date.now(),
|
|
8347
8795
|
lastActivity: Date.now(),
|
|
8796
|
+
lastPtyActivity: 0,
|
|
8348
8797
|
model_id: null,
|
|
8349
8798
|
model_provider: null,
|
|
8350
8799
|
_autoRestartCount: 0,
|
|
@@ -8384,9 +8833,14 @@ function apiAttachSession(req, res) {
|
|
|
8384
8833
|
const statusOnlyChunk = _isStatusOnlyPtyChunk(session, data);
|
|
8385
8834
|
const busyStatusChunk = _isBusyStatusPtyChunk(session, data);
|
|
8386
8835
|
const waitingForInput = _isServerWaitingForInput(tabId, session);
|
|
8387
|
-
const
|
|
8388
|
-
|
|
8389
|
-
if (
|
|
8836
|
+
const suppressedUiRefreshChunk = _suppressCtmUiRefreshActivity(session, statusOnlyChunk);
|
|
8837
|
+
const activityChunk = activeChunk && !suppressedUiRefreshChunk && !(statusOnlyChunk && waitingForInput && !busyStatusChunk);
|
|
8838
|
+
if (busyStatusChunk && waitingForInput && !suppressedUiRefreshChunk) _markServerSessionResumedFromPty(tabId, session, 'codex-working-status');
|
|
8839
|
+
if (activityChunk) {
|
|
8840
|
+
const now = Date.now();
|
|
8841
|
+
session.lastActivity = now;
|
|
8842
|
+
session.lastPtyActivity = now;
|
|
8843
|
+
}
|
|
8390
8844
|
const cleanData = data.indexOf('\x1b[') >= 0
|
|
8391
8845
|
? data.replace(/\x1b\[3J|\x1b\[\?100[0236]h|\x1b\[\?1015h/g, '')
|
|
8392
8846
|
: data;
|
|
@@ -8597,10 +9051,20 @@ function handleInput(ws, msg) {
|
|
|
8597
9051
|
const prevInputTs = session._lastInputTs || 0;
|
|
8598
9052
|
session._lastInputData = msg.data;
|
|
8599
9053
|
session._lastInputTs = now;
|
|
8600
|
-
session.
|
|
8601
|
-
|
|
9054
|
+
session.lastUserInputAt = now;
|
|
9055
|
+
const resolvesWaiting = _inputMayResolveWaiting(msg.data, session);
|
|
9056
|
+
if (resolvesWaiting) {
|
|
9057
|
+
session.lastActivity = now;
|
|
9058
|
+
session._waitingForInput = false;
|
|
9059
|
+
session._waitingForInputReason = '';
|
|
9060
|
+
session._waitingForInputAt = 0;
|
|
9061
|
+
session._lastWaitingResolveInputAt = now;
|
|
9062
|
+
session._lastNonResolvingWaitingInputAt = 0;
|
|
9063
|
+
} else if (_isServerWaitingForInput(msg.id, session)) {
|
|
9064
|
+
session._lastNonResolvingWaitingInputAt = now;
|
|
9065
|
+
}
|
|
8602
9066
|
const idleState = idleNotifyState.get(msg.id);
|
|
8603
|
-
if (idleState) idleState.notified = false;
|
|
9067
|
+
if (idleState && resolvesWaiting) idleState.notified = false;
|
|
8604
9068
|
session._inputFrame = (session._inputFrame || 0) + 1;
|
|
8605
9069
|
|
|
8606
9070
|
// Miss detection: if user manually types "1" or "2" while the approver is in
|
|
@@ -8630,11 +9094,12 @@ function handleResize(ws, msg) {
|
|
|
8630
9094
|
const session = sessions.get(msg.id);
|
|
8631
9095
|
if (session && msg.cols && msg.rows) {
|
|
8632
9096
|
if (!session.ptyProcess) return;
|
|
9097
|
+
_markCtmUiRefreshActivitySuppression(session, 'resize');
|
|
8633
9098
|
try {
|
|
8634
9099
|
session.ptyProcess.resize(msg.cols, msg.rows);
|
|
8635
9100
|
} catch (e) {
|
|
8636
|
-
// EBADF: PTY fd already closed (process exited between check and resize)
|
|
8637
|
-
if (e.message && e.message.includes('EBADF')) return;
|
|
9101
|
+
// EBADF / ENOTTY: PTY fd already closed or not a terminal (process exited between check and resize)
|
|
9102
|
+
if (e.message && (e.message.includes('EBADF') || e.message.includes('ENOTTY'))) return;
|
|
8638
9103
|
throw e;
|
|
8639
9104
|
}
|
|
8640
9105
|
// Keep headless terminal in sync so snapshots have correct dimensions
|
|
@@ -8673,6 +9138,7 @@ function handleReflow(ws, msg) {
|
|
|
8673
9138
|
headlessWorker.postMessage({ type: 'serialize', sessionId: msg.id, requestId: reqId });
|
|
8674
9139
|
// Also sync PTY dimensions (in case they drifted)
|
|
8675
9140
|
if (session.ptyProcess) {
|
|
9141
|
+
_markCtmUiRefreshActivitySuppression(session, 'reflow');
|
|
8676
9142
|
try { session.ptyProcess.resize(cols, rows); } catch {}
|
|
8677
9143
|
}
|
|
8678
9144
|
}
|
|
@@ -8875,26 +9341,51 @@ async function handleWalleMessage(ws, msg) {
|
|
|
8875
9341
|
if (!session || session.type !== 'walle') return;
|
|
8876
9342
|
|
|
8877
9343
|
session.lastActivity = Date.now();
|
|
9344
|
+
const broadcastToSession = (data) => {
|
|
9345
|
+
const payload = JSON.stringify(data);
|
|
9346
|
+
for (const client of session.clients) {
|
|
9347
|
+
if (client.readyState === 1) client.send(payload);
|
|
9348
|
+
}
|
|
9349
|
+
};
|
|
9350
|
+
if (session.abortController) {
|
|
9351
|
+
console.warn('[walle-session] ignored message while busy:', session.id);
|
|
9352
|
+
return;
|
|
9353
|
+
}
|
|
9354
|
+
if (!session.model_id) {
|
|
9355
|
+
const defaults = resolveWalleDefaultModelSelection({ brain: getWalleBrain(), env: process.env });
|
|
9356
|
+
if (defaults.model_id) {
|
|
9357
|
+
session.model_id = defaults.model_id;
|
|
9358
|
+
session.model_provider = defaults.model_provider || session.model_provider || null;
|
|
9359
|
+
try {
|
|
9360
|
+
dbModule.addStartupTask(session.id, session.label, '', [], session.cwd, session.model_id, 'walle', session.chatSessionId);
|
|
9361
|
+
} catch (e) {
|
|
9362
|
+
console.error('[ctm] addStartupTask (walle default model) error:', e.message);
|
|
9363
|
+
}
|
|
9364
|
+
broadcastToSession({
|
|
9365
|
+
type: 'walle-model',
|
|
9366
|
+
id: session.id,
|
|
9367
|
+
model_id: session.model_id,
|
|
9368
|
+
model_provider: session.model_provider,
|
|
9369
|
+
});
|
|
9370
|
+
broadcastSessionList(true);
|
|
9371
|
+
}
|
|
9372
|
+
}
|
|
9373
|
+
const selectedModel = msg.model || session.model_id || '';
|
|
9374
|
+
const selectedProvider = msg.provider
|
|
9375
|
+
|| (!msg.model || msg.model === session.model_id ? (session.model_provider || '') : '');
|
|
8878
9376
|
const userAppend = walleTranscript.appendUserMessage(session.jsonlPath, {
|
|
8879
9377
|
sessionId: session.id,
|
|
8880
9378
|
chatSessionId: session.chatSessionId,
|
|
8881
9379
|
parentUuid: session._walleLastTranscriptUuid || null,
|
|
8882
9380
|
cwd: session.cwd,
|
|
8883
9381
|
text: msg.text,
|
|
8884
|
-
modelId:
|
|
8885
|
-
modelProvider: session.model_provider || '',
|
|
9382
|
+
modelId: selectedModel,
|
|
9383
|
+
modelProvider: selectedProvider || session.model_provider || '',
|
|
8886
9384
|
});
|
|
8887
9385
|
_recordWalleTranscriptAppend(session, userAppend);
|
|
8888
9386
|
|
|
8889
9387
|
session.abortController = new AbortController();
|
|
8890
9388
|
|
|
8891
|
-
const broadcastToSession = (data) => {
|
|
8892
|
-
const payload = JSON.stringify(data);
|
|
8893
|
-
for (const client of session.clients) {
|
|
8894
|
-
if (client.readyState === 1) client.send(payload);
|
|
8895
|
-
}
|
|
8896
|
-
};
|
|
8897
|
-
|
|
8898
9389
|
const thinkingEvent = { type: 'thinking' };
|
|
8899
9390
|
broadcastToSession({ type: 'walle-progress', id: session.id, event: thinkingEvent });
|
|
8900
9391
|
_appendWallePart(session, 'thinking', { turn: null });
|
|
@@ -8916,7 +9407,8 @@ async function handleWalleMessage(ws, msg) {
|
|
|
8916
9407
|
message: msg.text,
|
|
8917
9408
|
session_id: session.chatSessionId,
|
|
8918
9409
|
channel: 'ctm-session',
|
|
8919
|
-
model:
|
|
9410
|
+
model: selectedModel || undefined,
|
|
9411
|
+
provider: selectedProvider || undefined,
|
|
8920
9412
|
cwd: effectiveCwd,
|
|
8921
9413
|
context: {
|
|
8922
9414
|
cwd: effectiveCwd,
|
|
@@ -8990,7 +9482,7 @@ async function handleWalleMessage(ws, msg) {
|
|
|
8990
9482
|
jsonlPath: session.jsonlPath,
|
|
8991
9483
|
fileSize: st.size || 0,
|
|
8992
9484
|
modifiedAt: st.mtime ? st.mtime.toISOString() : '',
|
|
8993
|
-
model: result.model ||
|
|
9485
|
+
model: result.model || selectedModel,
|
|
8994
9486
|
userMsgCount: 1,
|
|
8995
9487
|
hostname: HOSTNAME,
|
|
8996
9488
|
});
|
|
@@ -9055,16 +9547,18 @@ function handleRename(ws, msg) {
|
|
|
9055
9547
|
if (!trimmed) return;
|
|
9056
9548
|
|
|
9057
9549
|
// Update in-memory session if active
|
|
9058
|
-
const session =
|
|
9550
|
+
const session = findLiveSessionByAnyId(id);
|
|
9551
|
+
const liveSessionId = session ? session.id : id;
|
|
9059
9552
|
if (session) {
|
|
9060
9553
|
session.label = trimmed;
|
|
9554
|
+
session.userRenamed = true;
|
|
9061
9555
|
}
|
|
9062
9556
|
|
|
9063
9557
|
// Persist to sessions table (single source of truth)
|
|
9064
9558
|
dbModule.setSessionTitleNew(id, trimmed, true);
|
|
9065
9559
|
|
|
9066
9560
|
// Update startup_tasks so the label survives CTM restart
|
|
9067
|
-
try { dbModule.updateStartupTaskLabel(
|
|
9561
|
+
try { dbModule.updateStartupTaskLabel(liveSessionId, trimmed); } catch (e) { console.error('[ctm] updateStartupTaskLabel (ws) error:', e.message); }
|
|
9068
9562
|
|
|
9069
9563
|
ws.send(JSON.stringify({ type: 'renamed', id, label: trimmed }));
|
|
9070
9564
|
sessionEvents.emit('session:renamed', { id, label: trimmed });
|
|
@@ -9158,6 +9652,7 @@ function _sessionPayload(s) {
|
|
|
9158
9652
|
const caps = getAgentCapabilities(agentType);
|
|
9159
9653
|
const fileInfo = _activeSessionFileInfo(s, agentType);
|
|
9160
9654
|
const modifiedAt = fileInfo.modifiedAt || _sessionActivityIso(s.lastActivity) || _sessionActivityIso(s.createdAt);
|
|
9655
|
+
const liveStatus = _standupLiveStatusForSession(s);
|
|
9161
9656
|
return {
|
|
9162
9657
|
id: s.id,
|
|
9163
9658
|
type: s.type || 'pty',
|
|
@@ -9167,11 +9662,15 @@ function _sessionPayload(s) {
|
|
|
9167
9662
|
pid: s.pid,
|
|
9168
9663
|
createdAt: s.createdAt,
|
|
9169
9664
|
lastActivity: s.lastActivity,
|
|
9665
|
+
lastPtyActivity: s.lastPtyActivity || 0,
|
|
9666
|
+
liveStatus,
|
|
9667
|
+
liveStatusAt: liveStatus ? Date.now() : null,
|
|
9170
9668
|
modifiedAt,
|
|
9171
9669
|
fileModifiedAt: fileInfo.modifiedAt || null,
|
|
9172
9670
|
fileSize: fileInfo.fileSize || 0,
|
|
9173
9671
|
model_id: s.model_id,
|
|
9174
9672
|
model_provider: s.model_provider,
|
|
9673
|
+
userRenamed: !!s.userRenamed,
|
|
9175
9674
|
branch: s.branch || null,
|
|
9176
9675
|
worktree_path: s.worktree_path || null,
|
|
9177
9676
|
worktreeStatus: _sessionWorktreeStatusPayload(s),
|
|
@@ -9228,33 +9727,25 @@ function broadcastSessionList(immediate) {
|
|
|
9228
9727
|
}
|
|
9229
9728
|
}
|
|
9230
9729
|
|
|
9231
|
-
// ---
|
|
9232
|
-
// Sends
|
|
9233
|
-
//
|
|
9234
|
-
// Timestamps
|
|
9235
|
-
// so this path cannot corrupt "last touched" sorting.
|
|
9236
|
-
// Two cases are included:
|
|
9237
|
-
// 1. Sessions with recent PTY output (< 5s) — actively producing output
|
|
9238
|
-
// 2. Sessions in "thinking" state — no recent output but idle detection found
|
|
9239
|
-
// no prompt pattern (st.notified === false), meaning the agent is still
|
|
9240
|
-
// working (e.g., compacting, reasoning).
|
|
9730
|
+
// --- Live-status heartbeat (3s) ---
|
|
9731
|
+
// Sends the server's current live status projection so background tabs, the
|
|
9732
|
+
// sidebar, and Command Center converge without waiting for a full session-list
|
|
9733
|
+
// broadcast. Timestamps still reflect observed session output/activity, never
|
|
9734
|
+
// the heartbeat time, so this path cannot corrupt "last touched" sorting.
|
|
9241
9735
|
setInterval(() => {
|
|
9242
9736
|
if (wss.clients.size === 0) return;
|
|
9243
9737
|
const now = Date.now();
|
|
9244
9738
|
const active = [];
|
|
9245
9739
|
for (const [id, s] of sessions) {
|
|
9246
|
-
|
|
9247
|
-
|
|
9248
|
-
|
|
9249
|
-
|
|
9250
|
-
|
|
9251
|
-
|
|
9252
|
-
|
|
9253
|
-
|
|
9254
|
-
|
|
9255
|
-
active.push({ id, ts: s.lastActivity, state: 'thinking' });
|
|
9256
|
-
}
|
|
9257
|
-
}
|
|
9740
|
+
const liveStatus = _standupLiveStatusForSession(s, now);
|
|
9741
|
+
if (!liveStatus) continue;
|
|
9742
|
+
const ptyActivity = s.lastPtyActivity || 0;
|
|
9743
|
+
active.push({
|
|
9744
|
+
id,
|
|
9745
|
+
ts: ptyActivity || s.lastActivity || s.createdAt || now,
|
|
9746
|
+
state: liveStatus === 'running' ? 'active' : liveStatus,
|
|
9747
|
+
status: liveStatus,
|
|
9748
|
+
});
|
|
9258
9749
|
}
|
|
9259
9750
|
if (active.length === 0) return;
|
|
9260
9751
|
const payload = JSON.stringify({ type: 'session-activity', sessions: active });
|
|
@@ -9506,26 +9997,34 @@ function _executeHook(hook, data) {
|
|
|
9506
9997
|
}
|
|
9507
9998
|
}
|
|
9508
9999
|
|
|
9509
|
-
function apiServicesStatus(req, res) {
|
|
10000
|
+
async function apiServicesStatus(req, res) {
|
|
9510
10001
|
const ctmUptime = Math.floor((Date.now() - _ctmStartTime) / 1000);
|
|
9511
|
-
|
|
10002
|
+
let walleStatus;
|
|
10003
|
+
try {
|
|
10004
|
+
walleStatus = await walleSupervisor.getStatus();
|
|
10005
|
+
} catch (err) {
|
|
10006
|
+
walleStatus = { running: false, pid: null, error: err.message };
|
|
10007
|
+
}
|
|
9512
10008
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9513
10009
|
res.end(JSON.stringify({
|
|
9514
10010
|
ctm: { running: true, pid: process.pid, uptime: ctmUptime },
|
|
9515
|
-
walle: {
|
|
10011
|
+
walle: {
|
|
10012
|
+
running: walleStatus.running,
|
|
10013
|
+
pid: walleStatus.pid,
|
|
10014
|
+
pid_source: walleStatus.pidSource || null,
|
|
10015
|
+
stale_pid: walleStatus.stalePid || null,
|
|
10016
|
+
repaired_pid_file: !!walleStatus.repairedPidFile,
|
|
10017
|
+
removed_stale_pid_file: !!walleStatus.removedStalePidFile,
|
|
10018
|
+
error: walleStatus.error || null,
|
|
10019
|
+
}
|
|
9516
10020
|
}));
|
|
9517
10021
|
}
|
|
9518
10022
|
|
|
9519
10023
|
// --- Worktree API Handlers ---
|
|
9520
10024
|
const gitUtilsWorktree = require('./git-utils');
|
|
10025
|
+
const { resolveWorktreeRepoRoot } = require('./lib/worktree-cwd');
|
|
9521
10026
|
const _projectRoot = path.resolve(__dirname, '..');
|
|
9522
10027
|
|
|
9523
|
-
// Validate cwd is within the project root (prevents path traversal)
|
|
9524
|
-
function isValidWorktreeCwd(cwd) {
|
|
9525
|
-
const resolved = path.resolve(cwd);
|
|
9526
|
-
return resolved === _projectRoot || resolved.startsWith(_projectRoot + path.sep);
|
|
9527
|
-
}
|
|
9528
|
-
|
|
9529
10028
|
// Validate git ref names (branch names, tags) — reject values starting with - and special chars
|
|
9530
10029
|
const VALID_GIT_REF = /^[a-zA-Z0-9][a-zA-Z0-9._\/-]*$/;
|
|
9531
10030
|
const WORKTREE_JSON_HEADERS = {
|
|
@@ -9533,12 +10032,18 @@ const WORKTREE_JSON_HEADERS = {
|
|
|
9533
10032
|
'Cache-Control': 'no-store, max-age=0',
|
|
9534
10033
|
};
|
|
9535
10034
|
|
|
10035
|
+
function _resolveWorktreeApiCwd(res, reqCwd, headers) {
|
|
10036
|
+
const result = resolveWorktreeRepoRoot(reqCwd || _projectRoot);
|
|
10037
|
+
if (result.ok) return result.cwd;
|
|
10038
|
+
res.writeHead(result.status || 400, headers || WORKTREE_JSON_HEADERS);
|
|
10039
|
+
res.end(JSON.stringify({ error: result.error }));
|
|
10040
|
+
return null;
|
|
10041
|
+
}
|
|
10042
|
+
|
|
9536
10043
|
function apiListWorktrees(req, res) {
|
|
9537
|
-
const
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9541
|
-
}
|
|
10044
|
+
const reqCwd = new URL(req.url, 'http://localhost').searchParams.get('cwd') || _projectRoot;
|
|
10045
|
+
const cwd = _resolveWorktreeApiCwd(res, reqCwd, WORKTREE_JSON_HEADERS);
|
|
10046
|
+
if (!cwd) return;
|
|
9542
10047
|
gitUtilsWorktree.listRichWorktrees(cwd).then(worktrees => {
|
|
9543
10048
|
// Match worktree paths to active sessions
|
|
9544
10049
|
for (const wt of worktrees) {
|
|
@@ -9600,19 +10105,8 @@ function apiCreateWorktree(req, res) {
|
|
|
9600
10105
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
9601
10106
|
return res.end(JSON.stringify({ error: 'Invalid base branch name' }));
|
|
9602
10107
|
}
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
if (reqCwd && reqCwd.includes('~')) {
|
|
9606
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
9607
|
-
return res.end(JSON.stringify({ error: 'cwd must be an absolute path with no `~` (the literal-tilde is the source of ghost worktrees).' }));
|
|
9608
|
-
}
|
|
9609
|
-
let cwd = reqCwd || _projectRoot;
|
|
9610
|
-
// Resolve symlinks so we always pass git an absolute, canonical path.
|
|
9611
|
-
try { cwd = require('fs').realpathSync(cwd); } catch (_) { /* fall through to validation */ }
|
|
9612
|
-
if (!isValidWorktreeCwd(cwd)) {
|
|
9613
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
9614
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9615
|
-
}
|
|
10108
|
+
const cwd = _resolveWorktreeApiCwd(res, reqCwd, WORKTREE_JSON_HEADERS);
|
|
10109
|
+
if (!cwd) return;
|
|
9616
10110
|
gitUtilsWorktree.createWorktree(cwd, name, base_branch).then(result => {
|
|
9617
10111
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9618
10112
|
res.end(JSON.stringify({ ok: true, ...result }));
|
|
@@ -9641,11 +10135,8 @@ function apiMergeWorktree(req, res) {
|
|
|
9641
10135
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
9642
10136
|
return res.end(JSON.stringify({ error: 'Invalid merge strategy. Use: squash, no-ff, or ff' }));
|
|
9643
10137
|
}
|
|
9644
|
-
const cwd = reqCwd
|
|
9645
|
-
if (!
|
|
9646
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
9647
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9648
|
-
}
|
|
10138
|
+
const cwd = _resolveWorktreeApiCwd(res, reqCwd, WORKTREE_JSON_HEADERS);
|
|
10139
|
+
if (!cwd) return;
|
|
9649
10140
|
|
|
9650
10141
|
// Pre-check for conflicts
|
|
9651
10142
|
gitUtilsWorktree.mergeWorktreePreCheck(cwd, name).then(check => {
|
|
@@ -9680,7 +10171,7 @@ function apiRemoveWorktree(req, res) {
|
|
|
9680
10171
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
9681
10172
|
return res.end(JSON.stringify({ error: 'Invalid worktree name' }));
|
|
9682
10173
|
}
|
|
9683
|
-
const
|
|
10174
|
+
const reqCwd = reqUrl.searchParams.get('cwd') || _projectRoot;
|
|
9684
10175
|
const deleteBranchRaw = String(reqUrl.searchParams.get('delete_branch') || 'true').toLowerCase();
|
|
9685
10176
|
const deleteBranch = !['false', '0', 'no'].includes(deleteBranchRaw);
|
|
9686
10177
|
const force = reqUrl.searchParams.get('force') === 'true';
|
|
@@ -9689,10 +10180,8 @@ function apiRemoveWorktree(req, res) {
|
|
|
9689
10180
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
9690
10181
|
return res.end(JSON.stringify({ error: 'Invalid worktree name' }));
|
|
9691
10182
|
}
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9695
|
-
}
|
|
10183
|
+
const cwd = _resolveWorktreeApiCwd(res, reqCwd, WORKTREE_JSON_HEADERS);
|
|
10184
|
+
if (!cwd) return;
|
|
9696
10185
|
|
|
9697
10186
|
// Check if any active session is using this worktree
|
|
9698
10187
|
const worktreePath = path.join(cwd, '.claude', 'worktrees', name);
|
|
@@ -9759,11 +10248,8 @@ function apiSyncWorktree(req, res) {
|
|
|
9759
10248
|
res.writeHead(400, WORKTREE_JSON_HEADERS);
|
|
9760
10249
|
return res.end(JSON.stringify({ error: 'Invalid sync strategy. Use: merge or rebase' }));
|
|
9761
10250
|
}
|
|
9762
|
-
const cwd = reqCwd
|
|
9763
|
-
if (!
|
|
9764
|
-
res.writeHead(403, WORKTREE_JSON_HEADERS);
|
|
9765
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9766
|
-
}
|
|
10251
|
+
const cwd = _resolveWorktreeApiCwd(res, reqCwd, WORKTREE_JSON_HEADERS);
|
|
10252
|
+
if (!cwd) return;
|
|
9767
10253
|
gitUtilsWorktree.syncWorktree(cwd, name, strategy || 'merge').then(result => {
|
|
9768
10254
|
if (result.conflicts) {
|
|
9769
10255
|
res.writeHead(409, WORKTREE_JSON_HEADERS);
|
|
@@ -9793,11 +10279,8 @@ function apiSyncAllWorktrees(req, res) {
|
|
|
9793
10279
|
res.writeHead(400, WORKTREE_JSON_HEADERS);
|
|
9794
10280
|
return res.end(JSON.stringify({ error: 'Invalid sync strategy. Use: merge or rebase' }));
|
|
9795
10281
|
}
|
|
9796
|
-
const cwd = reqCwd
|
|
9797
|
-
if (!
|
|
9798
|
-
res.writeHead(403, WORKTREE_JSON_HEADERS);
|
|
9799
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9800
|
-
}
|
|
10282
|
+
const cwd = _resolveWorktreeApiCwd(res, reqCwd, WORKTREE_JSON_HEADERS);
|
|
10283
|
+
if (!cwd) return;
|
|
9801
10284
|
const activeWorktreePaths = [...sessions.values()]
|
|
9802
10285
|
.map(s => s.worktree_path || s.cwd)
|
|
9803
10286
|
.filter(Boolean)
|
|
@@ -9820,11 +10303,8 @@ function apiSyncAllWorktrees(req, res) {
|
|
|
9820
10303
|
|
|
9821
10304
|
function apiPruneWorktrees(req, res) {
|
|
9822
10305
|
const reqUrl = new URL(req.url, 'http://localhost');
|
|
9823
|
-
const cwd = reqUrl.searchParams.get('cwd') || _projectRoot;
|
|
9824
|
-
if (!
|
|
9825
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
9826
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9827
|
-
}
|
|
10306
|
+
const cwd = _resolveWorktreeApiCwd(res, reqUrl.searchParams.get('cwd') || _projectRoot, WORKTREE_JSON_HEADERS);
|
|
10307
|
+
if (!cwd) return;
|
|
9828
10308
|
gitUtilsWorktree.pruneGhosts(cwd).then(result => {
|
|
9829
10309
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9830
10310
|
res.end(JSON.stringify({ ok: true, ...result }));
|
|
@@ -9857,11 +10337,8 @@ function apiRecoverDetached(req, res) {
|
|
|
9857
10337
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
9858
10338
|
return res.end(JSON.stringify({ error: 'Invalid worktree path' }));
|
|
9859
10339
|
}
|
|
9860
|
-
const cwd = reqCwd
|
|
9861
|
-
if (!
|
|
9862
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
9863
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9864
|
-
}
|
|
10340
|
+
const cwd = _resolveWorktreeApiCwd(res, reqCwd, WORKTREE_JSON_HEADERS);
|
|
10341
|
+
if (!cwd) return;
|
|
9865
10342
|
gitUtilsWorktree.recoverWorktree(cwd, { name, path: requestedPath }).then(result => {
|
|
9866
10343
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9867
10344
|
res.end(JSON.stringify(result));
|
|
@@ -9890,11 +10367,8 @@ function apiCreatePR(req, res) {
|
|
|
9890
10367
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
9891
10368
|
return res.end(JSON.stringify({ error: 'Invalid base branch' }));
|
|
9892
10369
|
}
|
|
9893
|
-
const cwd = reqCwd
|
|
9894
|
-
if (!
|
|
9895
|
-
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
9896
|
-
return res.end(JSON.stringify({ error: 'cwd must be within project root' }));
|
|
9897
|
-
}
|
|
10370
|
+
const cwd = _resolveWorktreeApiCwd(res, reqCwd, WORKTREE_JSON_HEADERS);
|
|
10371
|
+
if (!cwd) return;
|
|
9898
10372
|
gitUtilsWorktree.pushAndCreatePR(cwd, name, { base: base || 'main', title, body: prBody }).then(result => {
|
|
9899
10373
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
9900
10374
|
res.end(JSON.stringify(result));
|
|
@@ -10208,14 +10682,36 @@ function _restoreSessionsAsync() {
|
|
|
10208
10682
|
|
|
10209
10683
|
// --- Update checker ---
|
|
10210
10684
|
const _updateState = { latestVersion: null, currentVersion: null, checkedAt: null, updateAvailable: false, error: null };
|
|
10685
|
+
const UPDATE_CHECK_TTL_MS = 60 * 60 * 1000;
|
|
10686
|
+
let _updateCheckInFlight = null;
|
|
10211
10687
|
|
|
10212
|
-
function
|
|
10688
|
+
function readPackageVersion(pkgPath) {
|
|
10213
10689
|
try {
|
|
10214
|
-
const pkg = JSON.parse(fs.readFileSync(
|
|
10690
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
10215
10691
|
return pkg.version;
|
|
10216
10692
|
} catch { return null; }
|
|
10217
10693
|
}
|
|
10218
10694
|
|
|
10695
|
+
function getCurrentVersion() {
|
|
10696
|
+
return readPackageVersion(path.join(__dirname, '..', 'package.json'));
|
|
10697
|
+
}
|
|
10698
|
+
|
|
10699
|
+
function getAppVersionInfo() {
|
|
10700
|
+
return {
|
|
10701
|
+
version: getCurrentVersion(),
|
|
10702
|
+
product: 'create-walle',
|
|
10703
|
+
components: {
|
|
10704
|
+
ctm: readPackageVersion(path.join(__dirname, 'package.json')),
|
|
10705
|
+
wallE: readPackageVersion(path.join(__dirname, '..', 'wall-e', 'package.json')),
|
|
10706
|
+
},
|
|
10707
|
+
};
|
|
10708
|
+
}
|
|
10709
|
+
|
|
10710
|
+
function apiAppVersion(req, res) {
|
|
10711
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
10712
|
+
res.end(JSON.stringify(getAppVersionInfo()));
|
|
10713
|
+
}
|
|
10714
|
+
|
|
10219
10715
|
// Simple semver comparison: returns 1 if a > b, -1 if a < b, 0 if equal
|
|
10220
10716
|
function semverCompare(a, b) {
|
|
10221
10717
|
const pa = a.split('.').map(Number);
|
|
@@ -10227,8 +10723,12 @@ function semverCompare(a, b) {
|
|
|
10227
10723
|
return 0;
|
|
10228
10724
|
}
|
|
10229
10725
|
|
|
10230
|
-
async function checkForUpdates() {
|
|
10726
|
+
async function checkForUpdates(source = 'scheduled') {
|
|
10231
10727
|
_updateState.currentVersion = getCurrentVersion();
|
|
10728
|
+
trackUpdateTelemetry(telemetry, 'check_started', {
|
|
10729
|
+
source,
|
|
10730
|
+
currentVersion: _updateState.currentVersion,
|
|
10731
|
+
});
|
|
10232
10732
|
try {
|
|
10233
10733
|
const resp = await fetch('https://registry.npmjs.org/create-walle/latest', {
|
|
10234
10734
|
headers: { 'Accept': 'application/json' },
|
|
@@ -10245,8 +10745,21 @@ async function checkForUpdates() {
|
|
|
10245
10745
|
_updateState.latestVersion && _updateState.currentVersion &&
|
|
10246
10746
|
semverCompare(_updateState.latestVersion, _updateState.currentVersion) > 0
|
|
10247
10747
|
);
|
|
10748
|
+
trackUpdateTelemetry(telemetry, 'check_completed', {
|
|
10749
|
+
source,
|
|
10750
|
+
currentVersion: _updateState.currentVersion,
|
|
10751
|
+
latestVersion: _updateState.latestVersion,
|
|
10752
|
+
updateAvailable: _updateState.updateAvailable,
|
|
10753
|
+
result: _updateState.updateAvailable ? 'available' : 'up_to_date',
|
|
10754
|
+
});
|
|
10248
10755
|
|
|
10249
10756
|
if (_updateState.updateAvailable) {
|
|
10757
|
+
trackUpdateTelemetry(telemetry, 'update_available', {
|
|
10758
|
+
source,
|
|
10759
|
+
currentVersion: _updateState.currentVersion,
|
|
10760
|
+
latestVersion: _updateState.latestVersion,
|
|
10761
|
+
updateAvailable: true,
|
|
10762
|
+
});
|
|
10250
10763
|
broadcastToAll({
|
|
10251
10764
|
type: 'update-available',
|
|
10252
10765
|
currentVersion: _updateState.currentVersion,
|
|
@@ -10256,11 +10769,42 @@ async function checkForUpdates() {
|
|
|
10256
10769
|
}
|
|
10257
10770
|
} catch (e) {
|
|
10258
10771
|
_updateState.error = e.message;
|
|
10772
|
+
trackUpdateTelemetry(telemetry, 'check_failed', {
|
|
10773
|
+
source,
|
|
10774
|
+
currentVersion: _updateState.currentVersion,
|
|
10775
|
+
errorKind: updateTelemetryErrorKind(e),
|
|
10776
|
+
result: 'error',
|
|
10777
|
+
});
|
|
10259
10778
|
}
|
|
10260
10779
|
return _updateState;
|
|
10261
10780
|
}
|
|
10262
10781
|
|
|
10263
|
-
function
|
|
10782
|
+
function updateStateIsStale() {
|
|
10783
|
+
const checkedAt = Date.parse(_updateState.checkedAt || '');
|
|
10784
|
+
return !checkedAt || (Date.now() - checkedAt) > UPDATE_CHECK_TTL_MS;
|
|
10785
|
+
}
|
|
10786
|
+
|
|
10787
|
+
async function refreshUpdateState(source) {
|
|
10788
|
+
if (_updateCheckInFlight) return _updateCheckInFlight;
|
|
10789
|
+
_updateCheckInFlight = checkForUpdates(source).finally(() => {
|
|
10790
|
+
_updateCheckInFlight = null;
|
|
10791
|
+
});
|
|
10792
|
+
return _updateCheckInFlight;
|
|
10793
|
+
}
|
|
10794
|
+
|
|
10795
|
+
async function apiUpdatesCheck(req, res, url) {
|
|
10796
|
+
_updateState.currentVersion = getCurrentVersion();
|
|
10797
|
+
const refresh = url?.searchParams?.get('refresh') === '1';
|
|
10798
|
+
if (refresh || updateStateIsStale()) {
|
|
10799
|
+
await refreshUpdateState(refresh ? 'api_refresh' : 'api_stale');
|
|
10800
|
+
}
|
|
10801
|
+
trackUpdateTelemetry(telemetry, 'api_check', {
|
|
10802
|
+
source: 'api',
|
|
10803
|
+
currentVersion: _updateState.currentVersion,
|
|
10804
|
+
latestVersion: _updateState.latestVersion,
|
|
10805
|
+
updateAvailable: _updateState.updateAvailable,
|
|
10806
|
+
result: _updateState.updateAvailable ? 'available' : 'up_to_date',
|
|
10807
|
+
});
|
|
10264
10808
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
10265
10809
|
res.end(JSON.stringify(_updateState));
|
|
10266
10810
|
}
|
|
@@ -10268,7 +10812,20 @@ function apiUpdatesCheck(req, res) {
|
|
|
10268
10812
|
function apiUpdatesApply(req, res) {
|
|
10269
10813
|
const current = _updateState.currentVersion;
|
|
10270
10814
|
const latest = _updateState.latestVersion;
|
|
10815
|
+
trackUpdateTelemetry(telemetry, 'apply_requested', {
|
|
10816
|
+
source: 'api',
|
|
10817
|
+
currentVersion: current,
|
|
10818
|
+
latestVersion: latest,
|
|
10819
|
+
updateAvailable: _updateState.updateAvailable,
|
|
10820
|
+
});
|
|
10271
10821
|
if (!_updateState.updateAvailable) {
|
|
10822
|
+
trackUpdateTelemetry(telemetry, 'apply_ignored', {
|
|
10823
|
+
source: 'api',
|
|
10824
|
+
currentVersion: current,
|
|
10825
|
+
latestVersion: latest,
|
|
10826
|
+
updateAvailable: false,
|
|
10827
|
+
result: 'ignored',
|
|
10828
|
+
});
|
|
10272
10829
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
10273
10830
|
res.end(JSON.stringify({ status: 'up-to-date', version: current }));
|
|
10274
10831
|
return;
|
|
@@ -10279,12 +10836,58 @@ function apiUpdatesApply(req, res) {
|
|
|
10279
10836
|
|
|
10280
10837
|
// Run update in background — npx create-walle@latest update handles stop/update/restart
|
|
10281
10838
|
const { spawn } = require('child_process');
|
|
10282
|
-
|
|
10283
|
-
|
|
10284
|
-
|
|
10285
|
-
|
|
10839
|
+
try {
|
|
10840
|
+
const child = spawn('npx', ['create-walle@latest', 'update'], {
|
|
10841
|
+
cwd: path.join(__dirname, '..'),
|
|
10842
|
+
detached: true,
|
|
10843
|
+
stdio: 'ignore',
|
|
10844
|
+
});
|
|
10845
|
+
trackUpdateTelemetry(telemetry, 'apply_started', {
|
|
10846
|
+
source: 'api',
|
|
10847
|
+
currentVersion: current,
|
|
10848
|
+
latestVersion: latest,
|
|
10849
|
+
updateAvailable: true,
|
|
10850
|
+
result: 'started',
|
|
10851
|
+
});
|
|
10852
|
+
child.on('error', (e) => {
|
|
10853
|
+
trackUpdateTelemetry(telemetry, 'apply_spawn_error', {
|
|
10854
|
+
source: 'api',
|
|
10855
|
+
currentVersion: current,
|
|
10856
|
+
latestVersion: latest,
|
|
10857
|
+
errorKind: updateTelemetryErrorKind(e),
|
|
10858
|
+
result: 'error',
|
|
10859
|
+
});
|
|
10860
|
+
});
|
|
10861
|
+
child.unref();
|
|
10862
|
+
} catch (e) {
|
|
10863
|
+
trackUpdateTelemetry(telemetry, 'apply_spawn_error', {
|
|
10864
|
+
source: 'api',
|
|
10865
|
+
currentVersion: current,
|
|
10866
|
+
latestVersion: latest,
|
|
10867
|
+
errorKind: updateTelemetryErrorKind(e),
|
|
10868
|
+
result: 'error',
|
|
10869
|
+
});
|
|
10870
|
+
}
|
|
10871
|
+
}
|
|
10872
|
+
|
|
10873
|
+
function apiUpdatesTelemetry(req, res) {
|
|
10874
|
+
let body = '';
|
|
10875
|
+
req.on('data', (chunk) => {
|
|
10876
|
+
body += chunk;
|
|
10877
|
+
if (body.length > 4096) req.destroy();
|
|
10878
|
+
});
|
|
10879
|
+
req.on('end', () => {
|
|
10880
|
+
try {
|
|
10881
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
10882
|
+
const item = clientUpdateTelemetryEvent(parsed);
|
|
10883
|
+
if (item) trackUpdateTelemetry(telemetry, item.event, item.details);
|
|
10884
|
+
res.writeHead(item ? 200 : 400, { 'Content-Type': 'application/json' });
|
|
10885
|
+
res.end(JSON.stringify(item ? { ok: true } : { ok: false, error: 'invalid telemetry event' }));
|
|
10886
|
+
} catch {
|
|
10887
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
10888
|
+
res.end(JSON.stringify({ ok: false, error: 'invalid JSON' }));
|
|
10889
|
+
}
|
|
10286
10890
|
});
|
|
10287
|
-
child.unref();
|
|
10288
10891
|
}
|
|
10289
10892
|
|
|
10290
10893
|
// --- Auto Title Generation ---
|
|
@@ -10556,8 +11159,8 @@ server.on('listening', () => {
|
|
|
10556
11159
|
_restoreSessionsAsync();
|
|
10557
11160
|
|
|
10558
11161
|
// Check for updates on startup (after 10s delay) and every 24h
|
|
10559
|
-
setTimeout(() => checkForUpdates().catch(() => {}), 10000);
|
|
10560
|
-
setInterval(() => checkForUpdates().catch(() => {}), 24 * 60 * 60 * 1000);
|
|
11162
|
+
setTimeout(() => checkForUpdates('startup').catch(() => {}), 10000);
|
|
11163
|
+
setInterval(() => checkForUpdates('scheduled').catch(() => {}), 24 * 60 * 60 * 1000);
|
|
10561
11164
|
|
|
10562
11165
|
// --- Scheduler: replaces ad-hoc setInterval timers for session management ---
|
|
10563
11166
|
const ctmScheduler = _ctmScheduler = new Scheduler({ tickMs: 5000, pools: { io: 3, llm: 1 } });
|