create-walle 0.9.20 → 0.9.21
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 +2 -2
- package/package.json +1 -1
- package/template/claude-task-manager/db.js +131 -0
- package/template/claude-task-manager/docs/microsoft-dev-tunnel-phone-access-design.md +58 -50
- package/template/claude-task-manager/docs/phone-access-design.md +23 -7
- package/template/claude-task-manager/docs/walle-session-model-preferences.md +119 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +32 -48
- package/template/claude-task-manager/lib/remote-relay-protocol.js +5 -0
- package/template/claude-task-manager/lib/walle-external-actions.js +20 -3
- package/template/claude-task-manager/public/index.html +25 -0
- package/template/claude-task-manager/public/js/setup.js +16 -12
- package/template/claude-task-manager/public/js/walle-session.js +31 -3
- package/template/claude-task-manager/public/js/walle.js +93 -23
- package/template/claude-task-manager/public/m/app.css +417 -21
- package/template/claude-task-manager/public/m/app.js +831 -44
- package/template/claude-task-manager/public/m/claim.html +1 -1
- package/template/claude-task-manager/public/m/index.html +41 -7
- package/template/claude-task-manager/public/m/sw.js +1 -1
- package/template/claude-task-manager/server.js +377 -30
- package/template/claude-task-manager/workers/state-detectors/codex.js +18 -3
- package/template/package.json +1 -1
- package/template/wall-e/chat.js +32 -2
- package/template/wall-e/coding/stream-processor.js +36 -0
- package/template/wall-e/coding-orchestrator.js +45 -0
- package/template/wall-e/docs/external-action-controller.md +60 -2
- package/template/wall-e/external-action-controller.js +23 -1
- package/template/wall-e/external-action-gateway.js +163 -0
- package/template/wall-e/fly.toml +1 -0
- package/template/wall-e/tools/local-tools.js +122 -4
- package/template/website/index.html +2 -2
|
@@ -221,6 +221,7 @@ const {
|
|
|
221
221
|
buildPendingExternalActionNotExecutedReply,
|
|
222
222
|
extractLatestPendingExternalActions,
|
|
223
223
|
externalActionCompletionClaim,
|
|
224
|
+
hasBlockedExternalActionGatewayToolResult,
|
|
224
225
|
hasExecutedExternalActionToolResult,
|
|
225
226
|
} = require('./lib/walle-external-actions');
|
|
226
227
|
const { persistWalleSessionConversation } = require('./lib/walle-session-cache');
|
|
@@ -506,6 +507,15 @@ function _isBusyStatusPtyChunk(session, data) {
|
|
|
506
507
|
return !!detector.isBusyStatusChunk(filtered);
|
|
507
508
|
}
|
|
508
509
|
|
|
510
|
+
function _hasBufferedBusyStatusPtyEvidence(session, data) {
|
|
511
|
+
const providerId = session?._providerId || _providerIdFromCmd(session?.cmd || '');
|
|
512
|
+
const detector = getStateDetector(providerId);
|
|
513
|
+
if (typeof detector.textHasCodexBusyStatus !== 'function') return false;
|
|
514
|
+
const buffer = String(session?._busyStatusEvidenceBuffer || '') + String(data || '');
|
|
515
|
+
if (session) session._busyStatusEvidenceBuffer = buffer.slice(-512);
|
|
516
|
+
return !!detector.textHasCodexBusyStatus(buffer);
|
|
517
|
+
}
|
|
518
|
+
|
|
509
519
|
// Tab activation/resize can trigger delayed TUI repaint frames. Those frames
|
|
510
520
|
// should redraw the terminal, but they must not manufacture fresh activity.
|
|
511
521
|
const CTM_UI_REFRESH_ACTIVITY_SUPPRESS_MS = 7000;
|
|
@@ -2747,6 +2757,7 @@ function _sendRemoteJson(res, status, payload) {
|
|
|
2747
2757
|
}
|
|
2748
2758
|
|
|
2749
2759
|
const REMOTE_TERMINAL_SUBMIT_DELAY_MS = 100;
|
|
2760
|
+
const REMOTE_TERMINAL_BUSY_STATUS_TTL_MS = 90 * 1000;
|
|
2750
2761
|
|
|
2751
2762
|
function _remoteTerminalSubmissionText(text) {
|
|
2752
2763
|
const normalized = String(text || '').replace(/\r\n?/g, '\n');
|
|
@@ -2754,32 +2765,120 @@ function _remoteTerminalSubmissionText(text) {
|
|
|
2754
2765
|
}
|
|
2755
2766
|
|
|
2756
2767
|
async function _sendRemoteTerminalSubmission(req, sessionId, text) {
|
|
2768
|
+
const initialSession = sessions.get(sessionId);
|
|
2769
|
+
const blocker = _remoteTerminalSubmissionBlocker(sessionId, initialSession);
|
|
2770
|
+
if (blocker) {
|
|
2771
|
+
recordSessionDiagnostic(sessionId, 'remote-terminal-submit-blocked', blocker);
|
|
2772
|
+
try {
|
|
2773
|
+
telemetry.track('remote_terminal_submit_blocked', {
|
|
2774
|
+
session_id: String(sessionId || '').slice(0, 8),
|
|
2775
|
+
error: blocker.error || 'blocked',
|
|
2776
|
+
reason: blocker.reason || '',
|
|
2777
|
+
});
|
|
2778
|
+
} catch {}
|
|
2779
|
+
return blocker;
|
|
2780
|
+
}
|
|
2781
|
+
initialSession._remoteTerminalSubmissionInFlight = true;
|
|
2757
2782
|
const data = _remoteTerminalSubmissionText(text);
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2783
|
+
try {
|
|
2784
|
+
handleInput({ auth: req.ctmAuth }, { id: sessionId, data });
|
|
2785
|
+
recordSessionDiagnostic(sessionId, 'remote-terminal-submit-text', {
|
|
2786
|
+
chars: data.length,
|
|
2787
|
+
enterDelayMs: REMOTE_TERMINAL_SUBMIT_DELAY_MS,
|
|
2788
|
+
});
|
|
2789
|
+
await new Promise((resolve) => setTimeout(resolve, REMOTE_TERMINAL_SUBMIT_DELAY_MS));
|
|
2790
|
+
const session = sessions.get(sessionId);
|
|
2791
|
+
if (!_isLiveTerminalSession(session)) {
|
|
2792
|
+
recordSessionDiagnostic(sessionId, 'remote-terminal-submit-enter-skipped', { reason: 'session_not_live' });
|
|
2793
|
+
return {
|
|
2794
|
+
ok: false,
|
|
2795
|
+
error: 'session_not_live',
|
|
2796
|
+
message: 'The text reached the terminal, but the session stopped before CTM could press Enter.',
|
|
2797
|
+
};
|
|
2798
|
+
}
|
|
2799
|
+
// Keep terminal submit as its own input frame. Codex/Claude-style TUIs run
|
|
2800
|
+
// real line editors; CTM's local queue/review flows already use this same
|
|
2801
|
+
// text-then-Enter contract so the editor can first commit the pasted text.
|
|
2802
|
+
handleInput({ auth: req.ctmAuth }, { id: sessionId, data: '\r' });
|
|
2803
|
+
recordSessionDiagnostic(sessionId, 'remote-terminal-submit-enter', { enterDelayMs: REMOTE_TERMINAL_SUBMIT_DELAY_MS });
|
|
2804
|
+
return { ok: true, delivered: true, submitted: true };
|
|
2805
|
+
} finally {
|
|
2806
|
+
const latest = sessions.get(sessionId);
|
|
2807
|
+
if (latest) latest._remoteTerminalSubmissionInFlight = false;
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
|
|
2811
|
+
function _isLiveTerminalSession(session) {
|
|
2812
|
+
return !!(session && session.ptyProcess && session.status !== 'exited' && session.status !== 'closed');
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
function _remoteTerminalAgentType(session) {
|
|
2816
|
+
if (!session || session.type === 'walle') return '';
|
|
2817
|
+
return normalizeAgentType(session.agentType || session._providerId || '') ||
|
|
2818
|
+
detectAgentType(session.cmd || '') ||
|
|
2819
|
+
_providerIdFromCmd(session.cmd || '') ||
|
|
2820
|
+
'shell';
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
function _remoteTerminalRequiresReadyPrompt(session) {
|
|
2824
|
+
return new Set(['claude', 'claude-code', 'codex', 'opencode', 'gemini', 'gemini-cli']).has(_remoteTerminalAgentType(session));
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
function _remoteTerminalSubmissionBlocker(sessionId, session, now = Date.now()) {
|
|
2765
2828
|
if (!_isLiveTerminalSession(session)) {
|
|
2766
|
-
recordSessionDiagnostic(sessionId, 'remote-terminal-submit-enter-skipped', { reason: 'session_not_live' });
|
|
2767
2829
|
return {
|
|
2768
2830
|
ok: false,
|
|
2769
2831
|
error: 'session_not_live',
|
|
2770
|
-
message: '
|
|
2832
|
+
message: 'This session is not accepting terminal input.',
|
|
2771
2833
|
};
|
|
2772
2834
|
}
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
}
|
|
2835
|
+
if (session._remoteTerminalSubmissionInFlight) {
|
|
2836
|
+
return {
|
|
2837
|
+
ok: false,
|
|
2838
|
+
error: 'session_input_in_flight',
|
|
2839
|
+
message: 'Another phone reply is already being delivered to this session.',
|
|
2840
|
+
};
|
|
2841
|
+
}
|
|
2842
|
+
if (!_remoteTerminalRequiresReadyPrompt(session)) return null;
|
|
2780
2843
|
|
|
2781
|
-
|
|
2782
|
-
|
|
2844
|
+
const waiting = _isServerWaitingForInput(sessionId, session, now);
|
|
2845
|
+
const waitingReason = _standupWaitingReason(sessionId, session);
|
|
2846
|
+
if (waiting && _standupIsBlockingWaitingReason(waitingReason)) {
|
|
2847
|
+
return {
|
|
2848
|
+
ok: false,
|
|
2849
|
+
error: 'session_waiting_for_approval',
|
|
2850
|
+
reason: waitingReason,
|
|
2851
|
+
message: 'This session is waiting for an approval or choice. Respond to that prompt before sending a new message.',
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
if (waiting) return null;
|
|
2855
|
+
|
|
2856
|
+
let busState = '';
|
|
2857
|
+
try {
|
|
2858
|
+
busState = statusHooks._bus && typeof statusHooks._bus.getState === 'function'
|
|
2859
|
+
? statusHooks._bus.getState(sessionId)
|
|
2860
|
+
: '';
|
|
2861
|
+
} catch {}
|
|
2862
|
+
if (busState === 'busy') {
|
|
2863
|
+
return {
|
|
2864
|
+
ok: false,
|
|
2865
|
+
error: 'session_busy',
|
|
2866
|
+
reason: 'status_bus_busy',
|
|
2867
|
+
message: 'The agent is still working. CTM kept this phone reply queued instead of typing into a busy terminal.',
|
|
2868
|
+
};
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
const busyAt = Number(session._lastBusyStatusAt || 0);
|
|
2872
|
+
if (busyAt && (now - busyAt) < REMOTE_TERMINAL_BUSY_STATUS_TTL_MS) {
|
|
2873
|
+
return {
|
|
2874
|
+
ok: false,
|
|
2875
|
+
error: 'session_busy',
|
|
2876
|
+
reason: 'terminal_busy_status',
|
|
2877
|
+
busyAgeMs: now - busyAt,
|
|
2878
|
+
message: 'The terminal still shows the agent working. CTM kept this phone reply queued instead of typing into a busy terminal.',
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
return null;
|
|
2783
2882
|
}
|
|
2784
2883
|
|
|
2785
2884
|
function _remoteHostedPairingReady(status) {
|
|
@@ -2999,15 +3098,46 @@ async function handleRemoteApi(req, res, url) {
|
|
|
2999
3098
|
if (session.type !== 'walle') return { ok: false, error: 'not_walle_session' };
|
|
3000
3099
|
if (!text.trim()) return { ok: false, error: 'message_required' };
|
|
3001
3100
|
const attachments = Array.isArray(body.attachments) ? body.attachments : [];
|
|
3101
|
+
const explicitModel = body.model_id || body.model || '';
|
|
3102
|
+
const explicitProvider = body.model_provider || body.provider || '';
|
|
3002
3103
|
await handleWalleMessage({ auth: req.ctmAuth }, {
|
|
3003
3104
|
id: sessionId,
|
|
3004
3105
|
text,
|
|
3005
3106
|
attachments,
|
|
3006
3107
|
echoUser: true,
|
|
3007
3108
|
displayText: text,
|
|
3109
|
+
model: explicitModel || session.model_id || '',
|
|
3110
|
+
provider: explicitProvider || session.model_provider || '',
|
|
3111
|
+
model_registry_id: body.model_registry_id || body.modelRegistryId || session.model_registry_id || '',
|
|
3112
|
+
modelPinned: body.modelPinned === true || body.model_pinned === true || !!explicitModel || !!session.model_pinned,
|
|
3113
|
+
allowProviderFallback: explicitModel ? false : !session.model_pinned,
|
|
3008
3114
|
});
|
|
3009
3115
|
return { ok: true, delivered: true };
|
|
3010
3116
|
},
|
|
3117
|
+
setWalleModel: async (sessionId, body = {}) => {
|
|
3118
|
+
const session = sessions.get(sessionId);
|
|
3119
|
+
if (!session) return { ok: false, error: 'session_not_found' };
|
|
3120
|
+
if (session.type !== 'walle') return { ok: false, error: 'not_walle_session' };
|
|
3121
|
+
const pref = _persistWalleSessionModelPreference(session, {
|
|
3122
|
+
model_id: body.model_id || body.model || '',
|
|
3123
|
+
model_provider: body.model_provider || body.provider || '',
|
|
3124
|
+
model_registry_id: body.model_registry_id || body.modelRegistryId || '',
|
|
3125
|
+
model_provider_id: body.model_provider_id || body.provider_id || '',
|
|
3126
|
+
scope: body.scope || 'session',
|
|
3127
|
+
source: 'phone',
|
|
3128
|
+
pinned: body.pinned !== false,
|
|
3129
|
+
});
|
|
3130
|
+
return {
|
|
3131
|
+
ok: true,
|
|
3132
|
+
delivered: true,
|
|
3133
|
+
model_id: session.model_id || null,
|
|
3134
|
+
model_provider: session.model_provider || null,
|
|
3135
|
+
model_registry_id: session.model_registry_id || '',
|
|
3136
|
+
model_provider_id: session.model_provider_id || '',
|
|
3137
|
+
model_pinned: !!session.model_pinned,
|
|
3138
|
+
cleared: !pref,
|
|
3139
|
+
};
|
|
3140
|
+
},
|
|
3011
3141
|
},
|
|
3012
3142
|
});
|
|
3013
3143
|
_sendRemoteJson(res, result.ok ? 200 : 400, result);
|
|
@@ -11846,6 +11976,24 @@ if (ws.readyState === 1) ws.send(JSON.stringify(_streamEventWsPayload(_ctmIdForS
|
|
|
11846
11976
|
if (msg.id && msg.model_id) {
|
|
11847
11977
|
const session = sessions.get(msg.id);
|
|
11848
11978
|
if (session) {
|
|
11979
|
+
if (session.type === 'walle') {
|
|
11980
|
+
try {
|
|
11981
|
+
_persistWalleSessionModelPreference(session, {
|
|
11982
|
+
...msg,
|
|
11983
|
+
source: msg.source || 'user',
|
|
11984
|
+
pinned: msg.pinned !== false,
|
|
11985
|
+
});
|
|
11986
|
+
} catch (e) {
|
|
11987
|
+
if (ws.readyState === 1) {
|
|
11988
|
+
ws.send(JSON.stringify({
|
|
11989
|
+
type: 'toast',
|
|
11990
|
+
level: 'error',
|
|
11991
|
+
message: `Could not save Wall-E model preference: ${e.message}`,
|
|
11992
|
+
}));
|
|
11993
|
+
}
|
|
11994
|
+
}
|
|
11995
|
+
return;
|
|
11996
|
+
}
|
|
11849
11997
|
session.model_id = msg.model_id;
|
|
11850
11998
|
session.model_provider = msg.model_provider || null;
|
|
11851
11999
|
// Persist so preference survives restart
|
|
@@ -11999,8 +12147,8 @@ async function dispatchQueuedWalleMessage(sessionId, text, options = {}) {
|
|
|
11999
12147
|
source: 'queue',
|
|
12000
12148
|
model: session.model_id || '',
|
|
12001
12149
|
provider: session.model_provider || '',
|
|
12002
|
-
modelPinned:
|
|
12003
|
-
allowProviderFallback:
|
|
12150
|
+
modelPinned: !!session.model_pinned,
|
|
12151
|
+
allowProviderFallback: !session.model_pinned,
|
|
12004
12152
|
});
|
|
12005
12153
|
}
|
|
12006
12154
|
|
|
@@ -12554,6 +12702,8 @@ function checkIdleNotify(sessionId, session, data) {
|
|
|
12554
12702
|
session._waitingForInputAt = waitingAt;
|
|
12555
12703
|
session._lastNonResolvingWaitingInputAt = 0;
|
|
12556
12704
|
session._lastWaitingResolveInputAt = 0;
|
|
12705
|
+
session._lastBusyStatusAt = 0;
|
|
12706
|
+
session._lastBusyStatusSource = reason ? `waiting:${reason}` : 'waiting';
|
|
12557
12707
|
_markTerminalLifecycleChanged(session, `waiting-for-input:${reason}`);
|
|
12558
12708
|
}
|
|
12559
12709
|
// Provider hooks are useful, but a visible prompt is stronger evidence
|
|
@@ -13197,15 +13347,32 @@ function handleCreate(ws, msg) {
|
|
|
13197
13347
|
if (agentType === 'walle') {
|
|
13198
13348
|
// Override auto-generated shell label if user didn't provide one
|
|
13199
13349
|
if (!msg.label) label = 'Wall-E session';
|
|
13350
|
+
let model_registry_id = msg.model_registry_id || msg.modelRegistryId || '';
|
|
13351
|
+
let model_provider_id = msg.model_provider_id || msg.provider_id || '';
|
|
13352
|
+
let model_pinned = false;
|
|
13353
|
+
try {
|
|
13354
|
+
const pref = dbModule.getSessionModelPreference?.(id);
|
|
13355
|
+
if (pref?.model_id) {
|
|
13356
|
+
model_id = pref.model_id;
|
|
13357
|
+
model_provider = pref.provider_type || model_provider || null;
|
|
13358
|
+
model_registry_id = pref.registry_id || model_registry_id || '';
|
|
13359
|
+
model_provider_id = pref.provider_id || model_provider_id || '';
|
|
13360
|
+
model_pinned = pref.pinned !== false;
|
|
13361
|
+
}
|
|
13362
|
+
} catch (e) {
|
|
13363
|
+
console.error('[walle-session] model preference restore error:', e.message);
|
|
13364
|
+
}
|
|
13200
13365
|
if (!model_id) {
|
|
13201
13366
|
const defaults = resolveWalleDefaultModelSelection({ brain: getWalleBrain(), env: process.env });
|
|
13202
13367
|
model_id = defaults.model_id || null;
|
|
13203
13368
|
model_provider = defaults.model_provider || null;
|
|
13369
|
+
model_pinned = false;
|
|
13204
13370
|
}
|
|
13205
13371
|
const agentMode = msg.agentMode || msg.agent_mode || 'coding';
|
|
13206
13372
|
const agentKind = msg.agentKind || msg.agent_kind || (agentMode === 'coding' ? 'walle-coding' : 'walle-chat');
|
|
13207
13373
|
const taskType = msg.taskType || msg.task_type || (agentMode === 'coding' ? 'coding' : 'chat');
|
|
13208
13374
|
const chatSessionId = msg.chatSessionId || `walle-${id}`;
|
|
13375
|
+
const initialMessage = String(msg.initialMessage || msg.initial_message || '').trim();
|
|
13209
13376
|
const jsonlPath = _walleTranscriptPathForSession(id, chatSessionId);
|
|
13210
13377
|
const createResult = walleTranscript.createSession(jsonlPath, {
|
|
13211
13378
|
reset: !msg._isRestore,
|
|
@@ -13231,6 +13398,9 @@ function handleCreate(ws, msg) {
|
|
|
13231
13398
|
chatSessionId, jsonlPath,
|
|
13232
13399
|
abortController: null,
|
|
13233
13400
|
model_id: model_id || null, model_provider: model_provider || null,
|
|
13401
|
+
model_registry_id: model_registry_id || '',
|
|
13402
|
+
model_provider_id: model_provider_id || '',
|
|
13403
|
+
model_pinned,
|
|
13234
13404
|
_walleLastTranscriptUuid: createResult?.uuid || walleTranscript.readLastUuid(jsonlPath) || null,
|
|
13235
13405
|
};
|
|
13236
13406
|
sessions.set(id, session);
|
|
@@ -13269,9 +13439,35 @@ function handleCreate(ws, msg) {
|
|
|
13269
13439
|
sessionType: 'walle',
|
|
13270
13440
|
model_id: session.model_id || null,
|
|
13271
13441
|
model_provider: session.model_provider || null,
|
|
13442
|
+
model_registry_id: session.model_registry_id || '',
|
|
13443
|
+
model_provider_id: session.model_provider_id || '',
|
|
13444
|
+
model_pinned: !!session.model_pinned,
|
|
13272
13445
|
}));
|
|
13273
13446
|
}
|
|
13274
13447
|
broadcastSessionList(true);
|
|
13448
|
+
if (initialMessage) {
|
|
13449
|
+
setImmediate(() => {
|
|
13450
|
+
handleWalleMessage(ws, {
|
|
13451
|
+
type: 'walle-message',
|
|
13452
|
+
id,
|
|
13453
|
+
text: initialMessage,
|
|
13454
|
+
echoUser: true,
|
|
13455
|
+
source: 'new-session',
|
|
13456
|
+
agentMode,
|
|
13457
|
+
agentKind,
|
|
13458
|
+
taskType,
|
|
13459
|
+
}).catch((e) => {
|
|
13460
|
+
console.error('[walle-session] initial prompt failed:', e.message);
|
|
13461
|
+
if (ws && ws.readyState === 1) {
|
|
13462
|
+
ws.send(JSON.stringify({
|
|
13463
|
+
type: 'walle-error',
|
|
13464
|
+
id,
|
|
13465
|
+
error: e.message || 'Wall-E initial prompt failed',
|
|
13466
|
+
}));
|
|
13467
|
+
}
|
|
13468
|
+
});
|
|
13469
|
+
});
|
|
13470
|
+
}
|
|
13275
13471
|
return;
|
|
13276
13472
|
}
|
|
13277
13473
|
|
|
@@ -14245,8 +14441,13 @@ function handleCreate(ws, msg) {
|
|
|
14245
14441
|
const activeChunk = _isActivePtyChunk(session, data);
|
|
14246
14442
|
const statusOnlyChunk = _isStatusOnlyPtyChunk(session, data);
|
|
14247
14443
|
const busyStatusChunk = _isBusyStatusPtyChunk(session, data);
|
|
14444
|
+
const busyStatusEvidenceChunk = busyStatusChunk || _hasBufferedBusyStatusPtyEvidence(session, data);
|
|
14248
14445
|
const waitingForInput = _isServerWaitingForInput(id, session);
|
|
14249
14446
|
const suppressedUiRefreshChunk = _suppressCtmUiRefreshActivity(session, statusOnlyChunk);
|
|
14447
|
+
if (busyStatusEvidenceChunk) {
|
|
14448
|
+
session._lastBusyStatusAt = Date.now();
|
|
14449
|
+
session._lastBusyStatusSource = suppressedUiRefreshChunk ? 'pty-ui-refresh' : 'pty';
|
|
14450
|
+
}
|
|
14250
14451
|
const activityChunk = activeChunk && !suppressedUiRefreshChunk && !(statusOnlyChunk && waitingForInput && !busyStatusChunk);
|
|
14251
14452
|
if (busyStatusChunk && waitingForInput && !suppressedUiRefreshChunk) _markServerSessionResumedFromPty(id, session, 'codex-working-status');
|
|
14252
14453
|
if (activityChunk) {
|
|
@@ -14968,8 +15169,13 @@ function apiAttachSession(req, res) {
|
|
|
14968
15169
|
const activeChunk = _isActivePtyChunk(session, data);
|
|
14969
15170
|
const statusOnlyChunk = _isStatusOnlyPtyChunk(session, data);
|
|
14970
15171
|
const busyStatusChunk = _isBusyStatusPtyChunk(session, data);
|
|
15172
|
+
const busyStatusEvidenceChunk = busyStatusChunk || _hasBufferedBusyStatusPtyEvidence(session, data);
|
|
14971
15173
|
const waitingForInput = _isServerWaitingForInput(tabId, session);
|
|
14972
15174
|
const suppressedUiRefreshChunk = _suppressCtmUiRefreshActivity(session, statusOnlyChunk);
|
|
15175
|
+
if (busyStatusEvidenceChunk) {
|
|
15176
|
+
session._lastBusyStatusAt = Date.now();
|
|
15177
|
+
session._lastBusyStatusSource = suppressedUiRefreshChunk ? 'attached-pty-ui-refresh' : 'attached-pty';
|
|
15178
|
+
}
|
|
14973
15179
|
const activityChunk = activeChunk && !suppressedUiRefreshChunk && !(statusOnlyChunk && waitingForInput && !busyStatusChunk);
|
|
14974
15180
|
if (busyStatusChunk && waitingForInput && !suppressedUiRefreshChunk) _markServerSessionResumedFromPty(tabId, session, 'codex-working-status');
|
|
14975
15181
|
if (activityChunk) {
|
|
@@ -15856,6 +16062,144 @@ function normalizeWalleClientModelSelection({ model, provider, session }) {
|
|
|
15856
16062
|
return { model: selectedModel, provider: selectedProvider, note };
|
|
15857
16063
|
}
|
|
15858
16064
|
|
|
16065
|
+
function _applyWalleSessionModelPreference(session, pref) {
|
|
16066
|
+
if (!session || !pref || !pref.model_id) return false;
|
|
16067
|
+
session.model_id = pref.model_id;
|
|
16068
|
+
session.model_provider = pref.provider_type || session.model_provider || null;
|
|
16069
|
+
session.model_registry_id = pref.registry_id || '';
|
|
16070
|
+
session.model_provider_id = pref.provider_id || '';
|
|
16071
|
+
session.model_pinned = pref.pinned !== false;
|
|
16072
|
+
return true;
|
|
16073
|
+
}
|
|
16074
|
+
|
|
16075
|
+
function _walleModelPayload(session, extra = {}) {
|
|
16076
|
+
return {
|
|
16077
|
+
type: 'walle-model',
|
|
16078
|
+
id: session.id,
|
|
16079
|
+
model_id: session.model_id || null,
|
|
16080
|
+
model_provider: session.model_provider || null,
|
|
16081
|
+
model_registry_id: session.model_registry_id || '',
|
|
16082
|
+
model_provider_id: session.model_provider_id || '',
|
|
16083
|
+
model_pinned: !!session.model_pinned,
|
|
16084
|
+
...extra,
|
|
16085
|
+
};
|
|
16086
|
+
}
|
|
16087
|
+
|
|
16088
|
+
function _broadcastWalleModel(session, extra = {}) {
|
|
16089
|
+
if (!session || !session.clients) return;
|
|
16090
|
+
const payload = JSON.stringify(_walleModelPayload(session, extra));
|
|
16091
|
+
for (const client of session.clients) {
|
|
16092
|
+
if (client.readyState === 1) client.send(payload);
|
|
16093
|
+
}
|
|
16094
|
+
}
|
|
16095
|
+
|
|
16096
|
+
function _hydrateWalleSessionModelPreference(session, { broadcast = false } = {}) {
|
|
16097
|
+
if (!session || session.type !== 'walle') return null;
|
|
16098
|
+
let pref = null;
|
|
16099
|
+
try {
|
|
16100
|
+
pref = dbModule.getSessionModelPreference?.(session.id) || null;
|
|
16101
|
+
} catch (e) {
|
|
16102
|
+
console.error('[walle-session] model preference read error:', e.message);
|
|
16103
|
+
return null;
|
|
16104
|
+
}
|
|
16105
|
+
if (!_applyWalleSessionModelPreference(session, pref)) return null;
|
|
16106
|
+
if (broadcast) _broadcastWalleModel(session, { model_source: 'session-preference' });
|
|
16107
|
+
return pref;
|
|
16108
|
+
}
|
|
16109
|
+
|
|
16110
|
+
function _persistWalleSessionModelPreference(session, msg = {}) {
|
|
16111
|
+
if (!session || session.type !== 'walle') return null;
|
|
16112
|
+
const rawModel = msg.model_id || msg.model || '';
|
|
16113
|
+
const rawProvider = msg.model_provider || msg.provider || '';
|
|
16114
|
+
if (!rawModel) {
|
|
16115
|
+
try { dbModule.clearSessionModelPreference?.(session.id); } catch (e) { console.error('[walle-session] model preference clear error:', e.message); }
|
|
16116
|
+
session.model_id = null;
|
|
16117
|
+
session.model_provider = null;
|
|
16118
|
+
session.model_registry_id = '';
|
|
16119
|
+
session.model_provider_id = '';
|
|
16120
|
+
session.model_pinned = false;
|
|
16121
|
+
try { dbModule.addStartupTask(session.id, session.label, '', [], session.cwd, null, 'walle', session.chatSessionId); } catch (e) { console.error('[ctm] addStartupTask (walle model clear) error:', e.message); }
|
|
16122
|
+
_broadcastWalleModel(session, { model_source: 'session-preference-cleared' });
|
|
16123
|
+
broadcastSessionList(true);
|
|
16124
|
+
return null;
|
|
16125
|
+
}
|
|
16126
|
+
|
|
16127
|
+
const selection = normalizeWalleClientModelSelection({
|
|
16128
|
+
model: rawModel,
|
|
16129
|
+
provider: rawProvider,
|
|
16130
|
+
session,
|
|
16131
|
+
});
|
|
16132
|
+
if (selection.note.normalized) {
|
|
16133
|
+
recordSessionDiagnostic(session.id, 'walle-model-preference-normalized', selection.note);
|
|
16134
|
+
console.warn('[walle-session] normalized session model preference:', selection.note);
|
|
16135
|
+
}
|
|
16136
|
+
|
|
16137
|
+
const modelId = selection.model;
|
|
16138
|
+
const providerType = selection.provider || rawProvider || '';
|
|
16139
|
+
if (!providerType) {
|
|
16140
|
+
recordSessionDiagnostic(session.id, 'walle-model-preference-provider-missing', {
|
|
16141
|
+
model_id: modelId,
|
|
16142
|
+
registry_id: msg.model_registry_id || msg.modelRegistryId || '',
|
|
16143
|
+
source: msg.source || 'model-change',
|
|
16144
|
+
});
|
|
16145
|
+
}
|
|
16146
|
+
|
|
16147
|
+
let pref = null;
|
|
16148
|
+
try {
|
|
16149
|
+
pref = dbModule.upsertSessionModelPreference?.({
|
|
16150
|
+
ctmSessionId: session.id,
|
|
16151
|
+
agentType: 'walle',
|
|
16152
|
+
providerType,
|
|
16153
|
+
providerId: msg.provider_id || msg.model_provider_id || '',
|
|
16154
|
+
modelId,
|
|
16155
|
+
registryId: msg.model_registry_id || msg.modelRegistryId || '',
|
|
16156
|
+
scope: msg.scope || 'session',
|
|
16157
|
+
source: msg.source || 'user',
|
|
16158
|
+
pinned: msg.pinned !== false,
|
|
16159
|
+
}) || null;
|
|
16160
|
+
} catch (e) {
|
|
16161
|
+
console.error('[walle-session] model preference persist error:', e.message);
|
|
16162
|
+
recordSessionDiagnostic(session.id, 'walle-model-preference-persist-failed', {
|
|
16163
|
+
error: e.message,
|
|
16164
|
+
model_id: modelId,
|
|
16165
|
+
provider_type: providerType,
|
|
16166
|
+
});
|
|
16167
|
+
throw e;
|
|
16168
|
+
}
|
|
16169
|
+
|
|
16170
|
+
_applyWalleSessionModelPreference(session, pref);
|
|
16171
|
+
try {
|
|
16172
|
+
dbModule.addStartupTask(session.id, session.label, '', [], session.cwd, session.model_id, 'walle', session.chatSessionId);
|
|
16173
|
+
} catch (e) {
|
|
16174
|
+
console.error('[ctm] addStartupTask (walle model preference) error:', e.message);
|
|
16175
|
+
}
|
|
16176
|
+
try {
|
|
16177
|
+
dbModule.upsertSession(session.id, {
|
|
16178
|
+
agentSessionId: session.chatSessionId,
|
|
16179
|
+
provider: 'walle',
|
|
16180
|
+
cwd: session.cwd || '',
|
|
16181
|
+
projectPath: session.cwd || '',
|
|
16182
|
+
title: session.label || '',
|
|
16183
|
+
jsonlPath: session.jsonlPath || '',
|
|
16184
|
+
model: session.model_id || '',
|
|
16185
|
+
hostname: HOSTNAME,
|
|
16186
|
+
});
|
|
16187
|
+
} catch (e) {
|
|
16188
|
+
console.error('[walle-session] model preference index update error:', e.message);
|
|
16189
|
+
}
|
|
16190
|
+
try {
|
|
16191
|
+
telemetry.track('walle_session_model_changed', {
|
|
16192
|
+
session_id: String(session.id || '').slice(0, 8),
|
|
16193
|
+
provider_type: session.model_provider || '',
|
|
16194
|
+
model_id: session.model_id || '',
|
|
16195
|
+
source: msg.source || 'user',
|
|
16196
|
+
});
|
|
16197
|
+
} catch {}
|
|
16198
|
+
_broadcastWalleModel(session, { model_source: 'session-preference' });
|
|
16199
|
+
broadcastSessionList(true);
|
|
16200
|
+
return pref;
|
|
16201
|
+
}
|
|
16202
|
+
|
|
15859
16203
|
async function handleWalleMessage(ws, msg) {
|
|
15860
16204
|
const session = sessions.get(msg.id);
|
|
15861
16205
|
if (!session || session.type !== 'walle') return;
|
|
@@ -15873,27 +16217,24 @@ async function handleWalleMessage(ws, msg) {
|
|
|
15873
16217
|
return;
|
|
15874
16218
|
}
|
|
15875
16219
|
session._walleCancelRequested = false;
|
|
16220
|
+
if (!session.model_id) {
|
|
16221
|
+
_hydrateWalleSessionModelPreference(session, { broadcast: true });
|
|
16222
|
+
}
|
|
15876
16223
|
if (!session.model_id) {
|
|
15877
16224
|
const defaults = resolveWalleDefaultModelSelection({ brain: getWalleBrain(), env: process.env });
|
|
15878
16225
|
if (defaults.model_id) {
|
|
15879
16226
|
session.model_id = defaults.model_id;
|
|
15880
16227
|
session.model_provider = defaults.model_provider || session.model_provider || null;
|
|
16228
|
+
session.model_pinned = false;
|
|
15881
16229
|
try {
|
|
15882
16230
|
dbModule.addStartupTask(session.id, session.label, '', [], session.cwd, session.model_id, 'walle', session.chatSessionId);
|
|
15883
16231
|
} catch (e) {
|
|
15884
16232
|
console.error('[ctm] addStartupTask (walle default model) error:', e.message);
|
|
15885
16233
|
}
|
|
15886
|
-
broadcastToSession({
|
|
15887
|
-
type: 'walle-model',
|
|
15888
|
-
id: session.id,
|
|
15889
|
-
model_id: session.model_id,
|
|
15890
|
-
model_provider: session.model_provider,
|
|
15891
|
-
});
|
|
16234
|
+
broadcastToSession(_walleModelPayload(session, { model_source: 'global-default' }));
|
|
15892
16235
|
broadcastSessionList(true);
|
|
15893
16236
|
}
|
|
15894
16237
|
}
|
|
15895
|
-
const legacyExplicitModel = !!(msg.model && msg.model !== session.model_id);
|
|
15896
|
-
const modelPinned = msg.modelPinned === true || msg.modelExplicit === true || legacyExplicitModel;
|
|
15897
16238
|
const selection = normalizeWalleClientModelSelection({
|
|
15898
16239
|
model: msg.model,
|
|
15899
16240
|
provider: msg.provider,
|
|
@@ -15901,6 +16242,9 @@ async function handleWalleMessage(ws, msg) {
|
|
|
15901
16242
|
});
|
|
15902
16243
|
const selectedModel = selection.model;
|
|
15903
16244
|
const selectedProvider = selection.provider;
|
|
16245
|
+
const legacyExplicitModel = !!(msg.model && selectedModel !== session.model_id);
|
|
16246
|
+
const sessionPinnedModel = !!session.model_pinned && !!session.model_id && selectedModel === session.model_id;
|
|
16247
|
+
const modelPinned = msg.modelPinned === true || msg.modelExplicit === true || legacyExplicitModel || sessionPinnedModel;
|
|
15904
16248
|
if (selection.note.normalized) {
|
|
15905
16249
|
recordSessionDiagnostic(session.id, 'walle-model-selection-normalized', selection.note);
|
|
15906
16250
|
console.warn('[walle-session] normalized client model selection:', selection.note);
|
|
@@ -16111,9 +16455,9 @@ async function handleWalleMessage(ws, msg) {
|
|
|
16111
16455
|
}
|
|
16112
16456
|
if (!result) throw new Error('Wall-E chat ended without a final response');
|
|
16113
16457
|
if (session._walleCancelRequested) throw _createWalleCancelledError();
|
|
16114
|
-
if (
|
|
16115
|
-
&& externalActionApprovals.length === 0
|
|
16458
|
+
if (externalActionApprovals.length === 0
|
|
16116
16459
|
&& externalActionCompletionClaim(result.reply)
|
|
16460
|
+
&& (pendingExternalActions.length || hasBlockedExternalActionGatewayToolResult(turnToolCalls))
|
|
16117
16461
|
&& !hasExecutedExternalActionToolResult(turnToolCalls)) {
|
|
16118
16462
|
result = {
|
|
16119
16463
|
...result,
|
|
@@ -16433,6 +16777,9 @@ function _sessionPayload(s) {
|
|
|
16433
16777
|
fileSize: fileInfo.fileSize || 0,
|
|
16434
16778
|
model_id: walleModel?.model_id || s.model_id,
|
|
16435
16779
|
model_provider: walleModel?.model_provider || s.model_provider,
|
|
16780
|
+
model_registry_id: s.model_registry_id || '',
|
|
16781
|
+
model_provider_id: s.model_provider_id || '',
|
|
16782
|
+
model_pinned: !!s.model_pinned,
|
|
16436
16783
|
walle_model_id: walleModel?.model_id || null,
|
|
16437
16784
|
walle_model_provider: walleModel?.model_provider || null,
|
|
16438
16785
|
runtime_model_id: walleOwned ? (s.model_id || null) : null,
|
|
@@ -41,12 +41,26 @@ function isCodexStatusRedraw(data) {
|
|
|
41
41
|
return (
|
|
42
42
|
stripped.length <= 160 &&
|
|
43
43
|
(CODEX_STATUS_FRAGMENT_RE.test(stripped) ||
|
|
44
|
-
|
|
45
|
-
CODEX_BUSY_COMPACT_STATUS_LINE_RE.test(stripped.replace(/^[\s•◦·∙●○]+\s*/, ''))) &&
|
|
44
|
+
textHasCodexBusyStatus(stripped)) &&
|
|
46
45
|
isCursorAddressedRedraw(data)
|
|
47
46
|
);
|
|
48
47
|
}
|
|
49
48
|
|
|
49
|
+
function normalizeCodexStatusLine(text) {
|
|
50
|
+
return String(text || '').replace(/^[\s•◦·∙●○]+/u, '').trim();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isCodexBusyStatusLine(text) {
|
|
54
|
+
const compact = normalizeCodexStatusLine(text);
|
|
55
|
+
return CODEX_BUSY_STATUS_LINE_RE.test(compact) ||
|
|
56
|
+
CODEX_BUSY_COMPACT_STATUS_LINE_RE.test(compact);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function textHasCodexBusyStatus(text) {
|
|
60
|
+
const clean = stripAnsi(text).replace(/\r/g, '\n');
|
|
61
|
+
return clean.split('\n').some((line) => isCodexBusyStatusLine(line));
|
|
62
|
+
}
|
|
63
|
+
|
|
50
64
|
module.exports = {
|
|
51
65
|
...baseDetector,
|
|
52
66
|
id: 'codex',
|
|
@@ -68,7 +82,8 @@ module.exports = {
|
|
|
68
82
|
return isCodexStatusRedraw(data);
|
|
69
83
|
},
|
|
70
84
|
isBusyStatusChunk(data) {
|
|
71
|
-
return isCodexStatusRedraw(data) &&
|
|
85
|
+
return isCodexStatusRedraw(data) && textHasCodexBusyStatus(data);
|
|
72
86
|
},
|
|
73
87
|
hasCodexBusyStatusFragment,
|
|
88
|
+
textHasCodexBusyStatus,
|
|
74
89
|
};
|