funolio-agent 1.0.75 → 1.1.65
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/dist/auth/credential-reader.d.ts.map +1 -1
- package/dist/auth/credential-reader.js +4 -3
- package/dist/auth/credential-reader.js.map +1 -1
- package/dist/auth/token-refresh.d.ts +8 -0
- package/dist/auth/token-refresh.d.ts.map +1 -1
- package/dist/auth/token-refresh.js +82 -52
- package/dist/auth/token-refresh.js.map +1 -1
- package/dist/auto-organizer.d.ts.map +1 -1
- package/dist/auto-organizer.js +6 -7
- package/dist/auto-organizer.js.map +1 -1
- package/dist/bench-prefix.d.ts +16 -0
- package/dist/bench-prefix.d.ts.map +1 -0
- package/dist/bench-prefix.js +25 -0
- package/dist/bench-prefix.js.map +1 -0
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +23 -14
- package/dist/bot-manager.js.map +1 -1
- package/dist/chat-sync.d.ts +42 -0
- package/dist/chat-sync.d.ts.map +1 -0
- package/dist/chat-sync.js +95 -0
- package/dist/chat-sync.js.map +1 -0
- package/dist/clerk-model.d.ts +7 -0
- package/dist/clerk-model.d.ts.map +1 -1
- package/dist/clerk-model.js +42 -8
- package/dist/clerk-model.js.map +1 -1
- package/dist/cli-bootstrap-history.d.ts +10 -0
- package/dist/cli-bootstrap-history.d.ts.map +1 -0
- package/dist/cli-bootstrap-history.js +112 -0
- package/dist/cli-bootstrap-history.js.map +1 -0
- package/dist/cli-models.d.ts +8 -0
- package/dist/cli-models.d.ts.map +1 -0
- package/dist/cli-models.js +91 -0
- package/dist/cli-models.js.map +1 -0
- package/dist/cli-session-epoch.d.ts +13 -3
- package/dist/cli-session-epoch.d.ts.map +1 -1
- package/dist/cli-session-epoch.js +53 -4
- package/dist/cli-session-epoch.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +64 -4
- package/dist/codex-app-server-manager.d.ts.map +1 -1
- package/dist/codex-app-server-manager.js +755 -55
- package/dist/codex-app-server-manager.js.map +1 -1
- package/dist/commands/pool.d.ts +32 -0
- package/dist/commands/pool.d.ts.map +1 -1
- package/dist/commands/pool.js +145 -66
- package/dist/commands/pool.js.map +1 -1
- package/dist/commands/start.d.ts +21 -0
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +484 -63
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +5 -2
- package/dist/commands/status.js.map +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +170 -58
- package/dist/config.js.map +1 -1
- package/dist/context-window.d.ts +37 -1
- package/dist/context-window.d.ts.map +1 -1
- package/dist/context-window.js +202 -16
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +3 -1
- package/dist/live-activity.d.ts.map +1 -1
- package/dist/live-activity.js.map +1 -1
- package/dist/local-chat-execution.d.ts +114 -0
- package/dist/local-chat-execution.d.ts.map +1 -0
- package/dist/local-chat-execution.js +349 -0
- package/dist/local-chat-execution.js.map +1 -0
- package/dist/local-cli-pty-manager.d.ts +138 -3
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +1415 -111
- package/dist/local-cli-pty-manager.js.map +1 -1
- package/dist/local-conversation-gateway.d.ts +110 -0
- package/dist/local-conversation-gateway.d.ts.map +1 -0
- package/dist/local-conversation-gateway.js +175 -0
- package/dist/local-conversation-gateway.js.map +1 -0
- package/dist/local-data.d.ts +235 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1066 -87
- package/dist/local-data.js.map +1 -1
- package/dist/local-db.d.ts +6 -0
- package/dist/local-db.d.ts.map +1 -1
- package/dist/local-db.js +376 -4
- package/dist/local-db.js.map +1 -1
- package/dist/local-funnel.d.ts.map +1 -1
- package/dist/local-funnel.js +6 -5
- package/dist/local-funnel.js.map +1 -1
- package/dist/local-server.d.ts +30 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +2898 -319
- package/dist/local-server.js.map +1 -1
- package/dist/managed-process-registry.d.ts +59 -0
- package/dist/managed-process-registry.d.ts.map +1 -0
- package/dist/managed-process-registry.js +390 -0
- package/dist/managed-process-registry.js.map +1 -0
- package/dist/mcp/claude-config-writer.d.ts +5 -5
- package/dist/mcp/claude-config-writer.d.ts.map +1 -1
- package/dist/mcp/claude-config-writer.js +19 -11
- package/dist/mcp/claude-config-writer.js.map +1 -1
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/mcp/sync-cli-config.d.ts +42 -4
- package/dist/mcp/sync-cli-config.d.ts.map +1 -1
- package/dist/mcp/sync-cli-config.js +497 -17
- package/dist/mcp/sync-cli-config.js.map +1 -1
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +43 -1
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +34 -0
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +270 -45
- package/dist/mqtt-client.js.map +1 -1
- package/dist/mqtt-data-relay.d.ts +44 -0
- package/dist/mqtt-data-relay.d.ts.map +1 -0
- package/dist/mqtt-data-relay.js +106 -0
- package/dist/mqtt-data-relay.js.map +1 -0
- package/dist/orchestration/capabilities.d.ts +13 -0
- package/dist/orchestration/capabilities.d.ts.map +1 -0
- package/dist/orchestration/capabilities.js +152 -0
- package/dist/orchestration/capabilities.js.map +1 -0
- package/dist/orchestration/dispatch-executor.d.ts +83 -0
- package/dist/orchestration/dispatch-executor.d.ts.map +1 -0
- package/dist/orchestration/dispatch-executor.js +266 -0
- package/dist/orchestration/dispatch-executor.js.map +1 -0
- package/dist/orchestration/dispatch-hint.d.ts +134 -0
- package/dist/orchestration/dispatch-hint.d.ts.map +1 -0
- package/dist/orchestration/dispatch-hint.js +247 -0
- package/dist/orchestration/dispatch-hint.js.map +1 -0
- package/dist/orchestration/dispatch-runner.d.ts +106 -0
- package/dist/orchestration/dispatch-runner.d.ts.map +1 -0
- package/dist/orchestration/dispatch-runner.js +604 -0
- package/dist/orchestration/dispatch-runner.js.map +1 -0
- package/dist/orchestration/dispatch-tools.d.ts +167 -0
- package/dist/orchestration/dispatch-tools.d.ts.map +1 -0
- package/dist/orchestration/dispatch-tools.js +328 -0
- package/dist/orchestration/dispatch-tools.js.map +1 -0
- package/dist/orchestration/front-door-policy.d.ts +35 -10
- package/dist/orchestration/front-door-policy.d.ts.map +1 -1
- package/dist/orchestration/front-door-policy.js +30 -267
- package/dist/orchestration/front-door-policy.js.map +1 -1
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts +43 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.d.ts.map +1 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js +267 -0
- package/dist/orchestration/orchestrator-dispatch-prompt.js.map +1 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts +14 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +157 -31
- package/dist/orchestration/orchestrator-operating-prompt.js.map +1 -1
- package/dist/orchestration/plan-import.d.ts +39 -0
- package/dist/orchestration/plan-import.d.ts.map +1 -0
- package/dist/orchestration/plan-import.js +547 -0
- package/dist/orchestration/plan-import.js.map +1 -0
- package/dist/orchestration/worker-operating-prompt.d.ts +2 -0
- package/dist/orchestration/worker-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/worker-operating-prompt.js +36 -46
- package/dist/orchestration/worker-operating-prompt.js.map +1 -1
- package/dist/orchestrator.d.ts +195 -3
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +1970 -432
- package/dist/orchestrator.js.map +1 -1
- package/dist/providers/anthropic.d.ts.map +1 -1
- package/dist/providers/anthropic.js +8 -4
- package/dist/providers/anthropic.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +28 -3
- package/dist/providers/claude-cli.js.map +1 -1
- package/dist/providers/codex-cli.d.ts +10 -6
- package/dist/providers/codex-cli.d.ts.map +1 -1
- package/dist/providers/codex-cli.js +190 -17
- package/dist/providers/codex-cli.js.map +1 -1
- package/dist/providers/google.d.ts.map +1 -1
- package/dist/providers/google.js +15 -5
- package/dist/providers/google.js.map +1 -1
- package/dist/providers/index.d.ts +15 -1
- package/dist/providers/index.d.ts.map +1 -1
- package/dist/providers/index.js.map +1 -1
- package/dist/providers/openai.d.ts +1 -1
- package/dist/providers/openai.d.ts.map +1 -1
- package/dist/providers/openai.js +13 -5
- package/dist/providers/openai.js.map +1 -1
- package/dist/server-adapter.d.ts +8 -0
- package/dist/server-adapter.d.ts.map +1 -1
- package/dist/server-adapter.js +7 -0
- package/dist/server-adapter.js.map +1 -1
- package/dist/service-mode.d.ts +1 -1
- package/dist/service-mode.d.ts.map +1 -1
- package/dist/service-mode.js +64 -1
- package/dist/service-mode.js.map +1 -1
- package/dist/service-setup-only.d.ts +8 -0
- package/dist/service-setup-only.d.ts.map +1 -0
- package/dist/service-setup-only.js +37 -0
- package/dist/service-setup-only.js.map +1 -0
- package/dist/slash-commands.d.ts +21 -0
- package/dist/slash-commands.d.ts.map +1 -0
- package/dist/slash-commands.js +99 -0
- package/dist/slash-commands.js.map +1 -0
- package/dist/subagent/index.d.ts +4 -2
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js.map +1 -1
- package/dist/summarization-pipeline.d.ts.map +1 -1
- package/dist/summarization-pipeline.js +1 -9
- package/dist/summarization-pipeline.js.map +1 -1
- package/dist/token-counter.d.ts.map +1 -1
- package/dist/token-counter.js +11 -4
- package/dist/token-counter.js.map +1 -1
- package/dist/tool-filter.d.ts.map +1 -1
- package/dist/tool-filter.js +10 -6
- package/dist/tool-filter.js.map +1 -1
- package/dist/tools/admin-tools.d.ts.map +1 -1
- package/dist/tools/admin-tools.js +13 -4
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/run-command.d.ts.map +1 -1
- package/dist/tools/run-command.js +5 -1
- package/dist/tools/run-command.js.map +1 -1
- package/dist/tools/search-conversation-history.d.ts.map +1 -1
- package/dist/tools/search-conversation-history.js +12 -2
- package/dist/tools/search-conversation-history.js.map +1 -1
- package/dist/tools/todo-tasks.d.ts.map +1 -1
- package/dist/tools/todo-tasks.js +77 -5
- package/dist/tools/todo-tasks.js.map +1 -1
- package/dist/usage-log.d.ts +62 -0
- package/dist/usage-log.d.ts.map +1 -0
- package/dist/usage-log.js +98 -0
- package/dist/usage-log.js.map +1 -0
- package/dist/wizard-state.d.ts +13 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +61 -3
- package/dist/wizard-state.js.map +1 -1
- package/dist/wizard-support.d.ts.map +1 -1
- package/dist/wizard-support.js +27 -1
- package/dist/wizard-support.js.map +1 -1
- package/dist/workflow-engine.d.ts +40 -1
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +753 -93
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
|
@@ -34,10 +34,22 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.LocalCliPtySessionManager = void 0;
|
|
37
|
+
exports.detectCliInteractiveAuthFailureForTest = detectCliInteractiveAuthFailureForTest;
|
|
38
|
+
exports.normalizeClaudeFreshSessionAuthErrorForTest = normalizeClaudeFreshSessionAuthErrorForTest;
|
|
39
|
+
exports.shouldResetClaudeSessionForAuthChangeForTest = shouldResetClaudeSessionForAuthChangeForTest;
|
|
40
|
+
exports.shouldRecycleClaudeSessionForFreshAuthForTest = shouldRecycleClaudeSessionForFreshAuthForTest;
|
|
41
|
+
exports.resolveClaudeLaunchSessionIdsForTest = resolveClaudeLaunchSessionIdsForTest;
|
|
42
|
+
exports.shouldContinueWaitingForFreshClaudeSessionForTest = shouldContinueWaitingForFreshClaudeSessionForTest;
|
|
43
|
+
exports.buildTurnPromptForTest = buildTurnPromptForTest;
|
|
44
|
+
exports.shouldIgnoreClaudeRecordForCurrentTurnForTest = shouldIgnoreClaudeRecordForCurrentTurnForTest;
|
|
37
45
|
exports.stripAnsi = stripAnsi;
|
|
38
46
|
exports.resolveConptyOverwrites = resolveConptyOverwrites;
|
|
47
|
+
exports.getProviderSessionRootForTest = getProviderSessionRootForTest;
|
|
48
|
+
exports.emitAssistantChunkSequenceForTest = emitAssistantChunkSequenceForTest;
|
|
39
49
|
exports.parseClaudeSessionRecord = parseClaudeSessionRecord;
|
|
40
50
|
exports.parseCodexSessionRecord = parseCodexSessionRecord;
|
|
51
|
+
exports.trimPassthroughSlashChromeForTest = trimPassthroughSlashChromeForTest;
|
|
52
|
+
exports.finalizePassthroughContentForTest = finalizePassthroughContentForTest;
|
|
41
53
|
exports.getLocalCliPtySessionManager = getLocalCliPtySessionManager;
|
|
42
54
|
exports.runLocalCliPtyHealthCheck = runLocalCliPtyHealthCheck;
|
|
43
55
|
exports.runLocalCliPtyTurnHealthCheck = runLocalCliPtyTurnHealthCheck;
|
|
@@ -49,8 +61,17 @@ const module_1 = require("module");
|
|
|
49
61
|
const claude_cli_prompt_1 = require("./providers/claude-cli-prompt");
|
|
50
62
|
const live_activity_1 = require("./live-activity");
|
|
51
63
|
const completion_marker_1 = require("./completion-marker");
|
|
52
|
-
const
|
|
64
|
+
const sync_cli_config_1 = require("./mcp/sync-cli-config");
|
|
65
|
+
const credential_reader_1 = require("./auth/credential-reader");
|
|
66
|
+
const token_refresh_1 = require("./auth/token-refresh");
|
|
67
|
+
const managed_process_registry_1 = require("./managed-process-registry");
|
|
68
|
+
const CLAUDE_FRESH_SESSION_STARTUP_INITIAL_TIMEOUT_MS = 10_000;
|
|
69
|
+
const CLAUDE_FRESH_SESSION_STARTUP_PROGRESS_STALL_MS = 10_000;
|
|
70
|
+
const CLAUDE_FRESH_SESSION_STARTUP_MAX_TIMEOUT_MS = 30_000;
|
|
53
71
|
const CLAUDE_PTY_INACTIVITY_FAIL_TIMEOUT_MS = 60_000;
|
|
72
|
+
const PASSTHROUGH_FIRST_BYTE_TIMEOUT_MS = 10_000;
|
|
73
|
+
const PASSTHROUGH_TRAILING_IDLE_MS = 2_000;
|
|
74
|
+
const CLI_WARM_TIMEOUT_MS = 10_000;
|
|
54
75
|
function getPtyInactivityFailTimeoutMs(provider) {
|
|
55
76
|
if (provider === 'claude-cli')
|
|
56
77
|
return CLAUDE_PTY_INACTIVITY_FAIL_TIMEOUT_MS;
|
|
@@ -61,18 +82,275 @@ let _manager = null;
|
|
|
61
82
|
function delay(ms) {
|
|
62
83
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
63
84
|
}
|
|
85
|
+
function fileFingerprint(filePath) {
|
|
86
|
+
if (!filePath)
|
|
87
|
+
return null;
|
|
88
|
+
try {
|
|
89
|
+
const stat = fs.statSync(filePath);
|
|
90
|
+
return [stat.dev, stat.ino, stat.size, Math.floor(stat.mtimeMs)].join(':');
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function currentClaudeAuthFingerprint() {
|
|
97
|
+
return fileFingerprint((0, sync_cli_config_1.resolveClaudeCredentialsSourcePath)());
|
|
98
|
+
}
|
|
99
|
+
function appendRecentTerminalOutput(existing, chunk) {
|
|
100
|
+
const combined = `${existing || ''}${normalizeTerminalChunk(chunk || '')}`;
|
|
101
|
+
return combined.length > 4000 ? combined.slice(-4000) : combined;
|
|
102
|
+
}
|
|
103
|
+
const CLI_INTERACTIVE_AUTH_FAILURE_RE = /\b(not logged in|please run \/login|login required|unauthorized|invalid authorization|invalid api key|missing api key|authentication required|invalid authentication credentials|invalid bearer token|token expired|session expired|credentials expired|expired credentials|reauthenticate)\b/i;
|
|
104
|
+
function isCliInteractiveAuthFailureText(text) {
|
|
105
|
+
return CLI_INTERACTIVE_AUTH_FAILURE_RE.test(text);
|
|
106
|
+
}
|
|
107
|
+
function detectCliInteractiveAuthFailure(provider, terminalOutput) {
|
|
108
|
+
if (!isCliInteractiveAuthFailureText(terminalOutput))
|
|
109
|
+
return null;
|
|
110
|
+
if (provider === 'claude-cli') {
|
|
111
|
+
return {
|
|
112
|
+
message: 'Claude authentication is required before this request can continue.',
|
|
113
|
+
detail: 'Claude authentication is required. Opening subscription login...',
|
|
114
|
+
providerId: 'claude-cli',
|
|
115
|
+
cli: 'claude',
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (provider === 'codex-cli') {
|
|
119
|
+
return {
|
|
120
|
+
message: 'Codex authentication is required before this request can continue.',
|
|
121
|
+
detail: 'Codex authentication is required. Opening subscription login...',
|
|
122
|
+
providerId: 'codex-cli',
|
|
123
|
+
cli: 'codex',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function detectCliInteractiveAuthFailureAcrossOutputs(provider, ...terminalOutputs) {
|
|
129
|
+
for (const output of terminalOutputs) {
|
|
130
|
+
const auth = detectCliInteractiveAuthFailure(provider, String(output || ''));
|
|
131
|
+
if (auth)
|
|
132
|
+
return auth;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
function buildCliInteractiveAuthError(provider, terminalOutput) {
|
|
137
|
+
const auth = detectCliInteractiveAuthFailure(provider, terminalOutput);
|
|
138
|
+
if (!auth)
|
|
139
|
+
return null;
|
|
140
|
+
const err = new Error(auth.message);
|
|
141
|
+
err.authRequired = true;
|
|
142
|
+
err.providerId = auth.providerId;
|
|
143
|
+
err.cli = auth.cli;
|
|
144
|
+
return err;
|
|
145
|
+
}
|
|
146
|
+
function buildCliInteractiveAuthErrorFromOutputs(provider, ...terminalOutputs) {
|
|
147
|
+
const auth = detectCliInteractiveAuthFailureAcrossOutputs(provider, ...terminalOutputs);
|
|
148
|
+
if (!auth)
|
|
149
|
+
return null;
|
|
150
|
+
const err = new Error(auth.message);
|
|
151
|
+
err.authRequired = true;
|
|
152
|
+
err.providerId = auth.providerId;
|
|
153
|
+
err.cli = auth.cli;
|
|
154
|
+
return err;
|
|
155
|
+
}
|
|
156
|
+
function detectCliInteractiveAuthFailureForTest(provider, ...terminalOutputs) {
|
|
157
|
+
return detectCliInteractiveAuthFailureAcrossOutputs(provider, ...terminalOutputs);
|
|
158
|
+
}
|
|
64
159
|
function buildAbortError() {
|
|
65
160
|
const abortErr = new Error('PTY turn aborted');
|
|
66
161
|
abortErr.name = 'AbortError';
|
|
67
162
|
return abortErr;
|
|
68
163
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
164
|
+
function buildCliAuthRequiredError(provider) {
|
|
165
|
+
if (provider === 'claude-cli') {
|
|
166
|
+
const err = new Error('Claude authentication is required before this request can continue.');
|
|
167
|
+
err.authRequired = true;
|
|
168
|
+
err.providerId = 'claude-cli';
|
|
169
|
+
err.cli = 'claude';
|
|
170
|
+
return err;
|
|
171
|
+
}
|
|
172
|
+
const err = new Error('Codex authentication is required before this request can continue.');
|
|
173
|
+
err.authRequired = true;
|
|
174
|
+
err.providerId = 'codex-cli';
|
|
175
|
+
err.cli = 'codex';
|
|
176
|
+
return err;
|
|
177
|
+
}
|
|
178
|
+
function normalizeClaudeFreshSessionAuthError(err) {
|
|
179
|
+
const message = String(err?.message || '').trim();
|
|
180
|
+
if (!message)
|
|
181
|
+
return null;
|
|
182
|
+
const normalized = message.toLowerCase();
|
|
183
|
+
if (err instanceof token_refresh_1.TokenRefreshError
|
|
184
|
+
|| normalized.includes('invalid_grant')
|
|
185
|
+
|| normalized.includes('refresh token not found or invalid')
|
|
186
|
+
|| normalized.includes('oauth token refresh failed')
|
|
187
|
+
|| normalized.includes('invalid authentication credentials')
|
|
188
|
+
|| normalized.includes('failed to authenticate')) {
|
|
189
|
+
return buildCliAuthRequiredError('claude-cli');
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
function normalizeClaudeFreshSessionAuthErrorForTest(err) {
|
|
194
|
+
const normalized = normalizeClaudeFreshSessionAuthError(err);
|
|
195
|
+
if (!normalized)
|
|
196
|
+
return null;
|
|
197
|
+
return {
|
|
198
|
+
message: normalized.message,
|
|
199
|
+
providerId: normalized.providerId,
|
|
200
|
+
cli: normalized.cli,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
async function ensureClaudeAuthReadyForFreshSession() {
|
|
204
|
+
const credential = (0, credential_reader_1.readClaudeCredentials)();
|
|
205
|
+
if (!credential?.accessToken) {
|
|
206
|
+
throw buildCliAuthRequiredError('claude-cli');
|
|
207
|
+
}
|
|
208
|
+
if (!(0, credential_reader_1.isExpired)(credential)) {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (!credential.refreshToken) {
|
|
212
|
+
throw buildCliAuthRequiredError('claude-cli');
|
|
213
|
+
}
|
|
214
|
+
try {
|
|
215
|
+
await (0, token_refresh_1.refreshToken)(credential);
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
const authError = normalizeClaudeFreshSessionAuthError(err);
|
|
219
|
+
if (authError)
|
|
220
|
+
throw authError;
|
|
221
|
+
throw err;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function buildClaudeFreshSessionStartupError(sessionId, elapsedMs) {
|
|
225
|
+
const err = new Error(`claude-cli fresh session startup timed out after ${Math.floor(elapsedMs / 1000)}s waiting for transcript file`
|
|
71
226
|
+ (sessionId ? ` (${sessionId})` : ''));
|
|
72
227
|
err.name = 'ClaudeFreshSessionStartupTimeoutError';
|
|
73
228
|
err.code = 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT';
|
|
74
229
|
return err;
|
|
75
230
|
}
|
|
231
|
+
function getTerminalAuthFailure(session, activeTurn) {
|
|
232
|
+
const visibleFailure = buildCliInteractiveAuthError(session.provider, activeTurn.visibleOutput);
|
|
233
|
+
if (visibleFailure)
|
|
234
|
+
return visibleFailure;
|
|
235
|
+
return buildCliInteractiveAuthErrorFromOutputs(session.provider, normalizeTerminalChunk(activeTurn.rawOutput), session.recentTerminalOutput);
|
|
236
|
+
}
|
|
237
|
+
function hasClaudeAuthFingerprintChanged(provider, storedFingerprint, currentFingerprint = currentClaudeAuthFingerprint()) {
|
|
238
|
+
if (provider !== 'claude-cli')
|
|
239
|
+
return false;
|
|
240
|
+
return storedFingerprint !== currentFingerprint;
|
|
241
|
+
}
|
|
242
|
+
function shouldResetClaudeSessionForAuthChange(session) {
|
|
243
|
+
return hasClaudeAuthFingerprintChanged(session.provider, session.claudeAuthFingerprint);
|
|
244
|
+
}
|
|
245
|
+
function syncClaudeSessionCredentialsBackToCanonical(session) {
|
|
246
|
+
if (session.provider !== 'claude-cli')
|
|
247
|
+
return;
|
|
248
|
+
try {
|
|
249
|
+
const botHomeDir = path.dirname(session.sessionFilesRoot);
|
|
250
|
+
(0, sync_cli_config_1.promoteClaudeBotHomeCredentialsToCanonical)(botHomeDir);
|
|
251
|
+
}
|
|
252
|
+
catch {
|
|
253
|
+
// best effort
|
|
254
|
+
}
|
|
255
|
+
session.claudeAuthFingerprint = currentClaudeAuthFingerprint();
|
|
256
|
+
}
|
|
257
|
+
function clearSessionWarmState(session) {
|
|
258
|
+
session.warmPromise = null;
|
|
259
|
+
session.warmRequestedAtMs = null;
|
|
260
|
+
session.warmReadyAtMs = null;
|
|
261
|
+
}
|
|
262
|
+
function isIdleClaudeSession(session) {
|
|
263
|
+
return session.provider === 'claude-cli' && !session.closed && session.activeTurn == null;
|
|
264
|
+
}
|
|
265
|
+
function shouldRecycleClaudeSessionForFreshAuth(session) {
|
|
266
|
+
// A healthy idle Claude PTY is exactly what we want to reuse. Recycling it
|
|
267
|
+
// here forced the next turn down the expensive --resume/fresh-session path.
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
function resolveClaudeLaunchSessionIds(resumeSessionId, newSessionId) {
|
|
271
|
+
const normalizedResumeSessionId = String(resumeSessionId || '').trim() || null;
|
|
272
|
+
const normalizedNewSessionId = String(newSessionId || '').trim() || null;
|
|
273
|
+
// Preserve Claude-side session history whenever Funolio has a stored session
|
|
274
|
+
// id. A fresh --session-id is only valid for a genuinely new epoch; recurring
|
|
275
|
+
// turns must relaunch with --resume if the live PTY is no longer available.
|
|
276
|
+
return {
|
|
277
|
+
resumeSessionId: normalizedResumeSessionId,
|
|
278
|
+
newSessionId: normalizedResumeSessionId ? null : normalizedNewSessionId,
|
|
279
|
+
knownSessionId: normalizedResumeSessionId || normalizedNewSessionId,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
function shouldResetClaudeSessionForAuthChangeForTest(provider, storedFingerprint, currentFingerprint) {
|
|
283
|
+
return hasClaudeAuthFingerprintChanged(provider, storedFingerprint, currentFingerprint);
|
|
284
|
+
}
|
|
285
|
+
function shouldRecycleClaudeSessionForFreshAuthForTest(input) {
|
|
286
|
+
return shouldRecycleClaudeSessionForFreshAuth({
|
|
287
|
+
key: 'test::session',
|
|
288
|
+
conversationId: 'conversation',
|
|
289
|
+
botId: 'bot',
|
|
290
|
+
topicId: null,
|
|
291
|
+
warmRuntimeMode: null,
|
|
292
|
+
provider: input.provider,
|
|
293
|
+
cwd: process.cwd(),
|
|
294
|
+
useConpty: false,
|
|
295
|
+
pty: { write() { }, kill() { }, on() { } },
|
|
296
|
+
createdAtMs: 0,
|
|
297
|
+
lastUsedAtMs: 0,
|
|
298
|
+
launchSnapshot: new Set(),
|
|
299
|
+
sessionFilesRoot: '',
|
|
300
|
+
sessionId: null,
|
|
301
|
+
sessionFilePath: null,
|
|
302
|
+
sessionFileOffset: 0,
|
|
303
|
+
sessionFileCarry: '',
|
|
304
|
+
readyPromise: Promise.resolve(),
|
|
305
|
+
readyResolved: true,
|
|
306
|
+
waitForNextSendMs: 0,
|
|
307
|
+
startupDelayMs: 0,
|
|
308
|
+
startupDelayApplied: false,
|
|
309
|
+
submitDelayMs: 0,
|
|
310
|
+
currentPromptLocator: null,
|
|
311
|
+
currentPromptStartedAtMs: 0,
|
|
312
|
+
activeTurn: input.hasActiveTurn ? {} : null,
|
|
313
|
+
warmPromise: input.hasWarmPromise ? Promise.resolve() : null,
|
|
314
|
+
warmRequestedAtMs: input.hasWarmPromise ? Date.now() : null,
|
|
315
|
+
warmReadyAtMs: input.hasWarmReadyAtMs ? Date.now() : null,
|
|
316
|
+
recentTerminalOutput: '',
|
|
317
|
+
closed: !!input.closed,
|
|
318
|
+
exitReason: null,
|
|
319
|
+
childFollowers: new Map(),
|
|
320
|
+
childSnapshot: new Set(),
|
|
321
|
+
claudeAuthFingerprint: null,
|
|
322
|
+
runtimeHomeDir: null,
|
|
323
|
+
chain: Promise.resolve(),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
function resolveClaudeLaunchSessionIdsForTest(resumeSessionId, newSessionId) {
|
|
327
|
+
return resolveClaudeLaunchSessionIds(resumeSessionId, newSessionId);
|
|
328
|
+
}
|
|
329
|
+
function shouldContinueWaitingForFreshClaudeSession(session, activeTurn, startedAtMs, now) {
|
|
330
|
+
const elapsedMs = now - startedAtMs;
|
|
331
|
+
if (elapsedMs < CLAUDE_FRESH_SESSION_STARTUP_INITIAL_TIMEOUT_MS)
|
|
332
|
+
return true;
|
|
333
|
+
if (elapsedMs >= CLAUDE_FRESH_SESSION_STARTUP_MAX_TIMEOUT_MS)
|
|
334
|
+
return false;
|
|
335
|
+
if (session.closed)
|
|
336
|
+
return false;
|
|
337
|
+
const postPromptProgressAtMs = Math.max(activeTurn.lastDataAtMs, activeTurn.lastMeaningfulPtyDataAtMs);
|
|
338
|
+
if (postPromptProgressAtMs > startedAtMs) {
|
|
339
|
+
return now - postPromptProgressAtMs <= CLAUDE_FRESH_SESSION_STARTUP_PROGRESS_STALL_MS;
|
|
340
|
+
}
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
function shouldContinueWaitingForFreshClaudeSessionForTest(input) {
|
|
344
|
+
const startedAtMs = input.startedAtMs;
|
|
345
|
+
return shouldContinueWaitingForFreshClaudeSession({
|
|
346
|
+
provider: 'claude-cli',
|
|
347
|
+
closed: !!input.sessionClosed,
|
|
348
|
+
readyResolved: !!input.readyResolved,
|
|
349
|
+
}, {
|
|
350
|
+
lastDataAtMs: input.lastDataAtMs ?? startedAtMs,
|
|
351
|
+
lastMeaningfulPtyDataAtMs: input.lastMeaningfulPtyDataAtMs ?? startedAtMs,
|
|
352
|
+
}, startedAtMs, input.nowMs);
|
|
353
|
+
}
|
|
76
354
|
function throwIfAborted(signal) {
|
|
77
355
|
if (!signal?.aborted)
|
|
78
356
|
return;
|
|
@@ -129,7 +407,6 @@ function loadNodePtyModule() {
|
|
|
129
407
|
if (_ptyModule)
|
|
130
408
|
return _ptyModule;
|
|
131
409
|
const dynamicRequire = eval('require');
|
|
132
|
-
const fileRequire = (0, module_1.createRequire)(path.join(process.cwd(), 'sea-entry.js'));
|
|
133
410
|
try {
|
|
134
411
|
_ptyModule = dynamicRequire('@homebridge/node-pty-prebuilt-multiarch');
|
|
135
412
|
return _ptyModule;
|
|
@@ -144,8 +421,18 @@ function loadNodePtyModule() {
|
|
|
144
421
|
for (const candidate of candidates) {
|
|
145
422
|
if (!fs.existsSync(candidate))
|
|
146
423
|
continue;
|
|
147
|
-
|
|
148
|
-
|
|
424
|
+
try {
|
|
425
|
+
// Use dynamic require with the absolute path — this preserves __dirname
|
|
426
|
+
// resolution inside the PTY module for finding native .node addons.
|
|
427
|
+
_ptyModule = dynamicRequire(candidate);
|
|
428
|
+
return _ptyModule;
|
|
429
|
+
}
|
|
430
|
+
catch {
|
|
431
|
+
// Try createRequire fallback if dynamic require fails
|
|
432
|
+
const ptyRequire = (0, module_1.createRequire)(candidate);
|
|
433
|
+
_ptyModule = ptyRequire(candidate);
|
|
434
|
+
return _ptyModule;
|
|
435
|
+
}
|
|
149
436
|
}
|
|
150
437
|
throw new Error(`Failed to load PTY runtime. Tried package import and packaged resource candidates. Original error: ${firstErr instanceof Error ? firstErr.message : String(firstErr)}`);
|
|
151
438
|
}
|
|
@@ -256,24 +543,181 @@ function buildCodexSeedPrompt(systemPrompt, messages) {
|
|
|
256
543
|
}
|
|
257
544
|
return fullPrompt;
|
|
258
545
|
}
|
|
259
|
-
function
|
|
546
|
+
function extractTextAndImagePaths(content) {
|
|
547
|
+
if (typeof content === 'string')
|
|
548
|
+
return { text: content, imagePaths: [] };
|
|
549
|
+
const textParts = [];
|
|
550
|
+
const imagePaths = [];
|
|
551
|
+
// Write images to OS temp dir, not project workspace
|
|
552
|
+
const tmpDir = path.join(os.tmpdir(), 'funolio-images');
|
|
553
|
+
for (const part of content) {
|
|
554
|
+
if (part.type === 'text') {
|
|
555
|
+
textParts.push(part.text);
|
|
556
|
+
}
|
|
557
|
+
else if (part.type === 'image' && part.data) {
|
|
558
|
+
const ext = (part.mimeType || 'image/png').split('/')[1] || 'png';
|
|
559
|
+
const filename = `funolio-img-${Date.now()}-${Math.random().toString(36).slice(2, 6)}.${ext}`;
|
|
560
|
+
const filePath = path.join(tmpDir, filename);
|
|
561
|
+
try {
|
|
562
|
+
if (!fs.existsSync(tmpDir))
|
|
563
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
564
|
+
fs.writeFileSync(filePath, Buffer.from(part.data, 'base64'));
|
|
565
|
+
imagePaths.push(filePath);
|
|
566
|
+
}
|
|
567
|
+
catch {
|
|
568
|
+
// best effort
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return { text: textParts.join('\n'), imagePaths };
|
|
573
|
+
}
|
|
574
|
+
function buildClaudeImagePathText(imagePaths) {
|
|
575
|
+
if (imagePaths.length === 0)
|
|
576
|
+
return '';
|
|
577
|
+
if (imagePaths.length === 1)
|
|
578
|
+
return `\n\nAnalyze this image: ${imagePaths[0]}.`;
|
|
579
|
+
return `\n\n${imagePaths.map((p, index) => `Analyze image ${index + 1}: ${p}.`).join(' ')}`;
|
|
580
|
+
}
|
|
581
|
+
function buildImageNote(provider, imagePaths) {
|
|
582
|
+
if (provider === 'claude-cli') {
|
|
583
|
+
return buildClaudeImagePathText(imagePaths);
|
|
584
|
+
}
|
|
585
|
+
if (imagePaths.length === 0)
|
|
586
|
+
return '';
|
|
587
|
+
return `\n\n[The user attached ${imagePaths.length} image(s). Read them with your Read tool:\n${imagePaths.map((p) => ` - ${p}`).join('\n')}\n]`;
|
|
588
|
+
}
|
|
589
|
+
function flattenMessagesForImages(provider, messages) {
|
|
590
|
+
// Extract images from the last user message and convert content to text + image note
|
|
591
|
+
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
592
|
+
if (!lastMessage || lastMessage.role !== 'user' || typeof lastMessage.content === 'string') {
|
|
593
|
+
return { messages, imageNote: '' };
|
|
594
|
+
}
|
|
595
|
+
const { text, imagePaths } = extractTextAndImagePaths(lastMessage.content);
|
|
596
|
+
const flatMessages = [
|
|
597
|
+
...messages.slice(0, -1),
|
|
598
|
+
{
|
|
599
|
+
...lastMessage,
|
|
600
|
+
content: provider === 'claude-cli'
|
|
601
|
+
? `${text}${buildClaudeImagePathText(imagePaths)}`
|
|
602
|
+
: text,
|
|
603
|
+
},
|
|
604
|
+
];
|
|
605
|
+
return {
|
|
606
|
+
messages: flatMessages,
|
|
607
|
+
imageNote: provider === 'claude-cli' ? '' : buildImageNote(provider, imagePaths),
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
function buildTurnPrompt(provider, systemPrompt, messages, freshSession, cwd = '.') {
|
|
260
611
|
if (!freshSession) {
|
|
261
612
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
262
613
|
if (lastMessage?.role === 'user') {
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
: JSON.stringify(lastMessage.content);
|
|
266
|
-
return `${prompt}\n\nRequired final line when the response is fully complete: ${completion_marker_1.CLI_COMPLETION_SENTINEL}`;
|
|
614
|
+
const { text, imagePaths } = extractTextAndImagePaths(lastMessage.content);
|
|
615
|
+
return `${text}${buildImageNote(provider, imagePaths)}\n\nRequired final line when the response is fully complete: ${completion_marker_1.CLI_COMPLETION_SENTINEL}`;
|
|
267
616
|
}
|
|
268
617
|
}
|
|
618
|
+
// For fresh sessions, flatten images before passing to prompt builders
|
|
619
|
+
const { messages: flatMessages, imageNote } = flattenMessagesForImages(provider, messages);
|
|
269
620
|
if (provider === 'claude-cli') {
|
|
270
|
-
|
|
621
|
+
const prompt = (0, claude_cli_prompt_1.buildClaudeCliStylePrompt)({
|
|
271
622
|
system: systemPrompt,
|
|
272
|
-
messages,
|
|
623
|
+
messages: flatMessages,
|
|
273
624
|
runtimeMode: 'local_desktop',
|
|
274
625
|
});
|
|
626
|
+
return imageNote ? `${prompt}${imageNote}` : prompt;
|
|
275
627
|
}
|
|
276
|
-
|
|
628
|
+
const codexPrompt = buildCodexSeedPrompt(systemPrompt, flatMessages);
|
|
629
|
+
return imageNote ? `${codexPrompt}${imageNote}` : codexPrompt;
|
|
630
|
+
}
|
|
631
|
+
function buildTurnPromptForTest(provider, systemPrompt, messages, freshSession, cwd = '.') {
|
|
632
|
+
return buildTurnPrompt(provider, systemPrompt, messages, freshSession, cwd);
|
|
633
|
+
}
|
|
634
|
+
function normalizePromptSnippet(text) {
|
|
635
|
+
return String(text || '')
|
|
636
|
+
.replace(/\s+/g, ' ')
|
|
637
|
+
.trim()
|
|
638
|
+
.toLowerCase();
|
|
639
|
+
}
|
|
640
|
+
function extractCurrentUserPromptSnippet(messages) {
|
|
641
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
642
|
+
const msg = messages[i];
|
|
643
|
+
if (msg?.role !== 'user')
|
|
644
|
+
continue;
|
|
645
|
+
const { text } = extractTextAndImagePaths(msg.content);
|
|
646
|
+
const normalized = normalizePromptSnippet(text);
|
|
647
|
+
if (normalized)
|
|
648
|
+
return normalized.slice(0, 240);
|
|
649
|
+
}
|
|
650
|
+
return '';
|
|
651
|
+
}
|
|
652
|
+
function parseRecordTimestampMs(record) {
|
|
653
|
+
if (!record || typeof record !== 'object' || typeof record.timestamp !== 'string')
|
|
654
|
+
return null;
|
|
655
|
+
const parsed = Date.parse(record.timestamp);
|
|
656
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
657
|
+
}
|
|
658
|
+
function extractClaudeUserRecordText(record) {
|
|
659
|
+
if (!record || record.type !== 'user')
|
|
660
|
+
return '';
|
|
661
|
+
const content = record?.message?.content;
|
|
662
|
+
if (typeof content === 'string')
|
|
663
|
+
return content;
|
|
664
|
+
if (!Array.isArray(content))
|
|
665
|
+
return '';
|
|
666
|
+
return content
|
|
667
|
+
.map((block) => {
|
|
668
|
+
if (!block || typeof block !== 'object')
|
|
669
|
+
return '';
|
|
670
|
+
if (typeof block.text === 'string')
|
|
671
|
+
return block.text;
|
|
672
|
+
if (typeof block.content === 'string')
|
|
673
|
+
return block.content;
|
|
674
|
+
return '';
|
|
675
|
+
})
|
|
676
|
+
.filter(Boolean)
|
|
677
|
+
.join('\n');
|
|
678
|
+
}
|
|
679
|
+
function updateClaudeTurnPromptSeen(tracker, record) {
|
|
680
|
+
if (tracker.sawCurrentTurnUserRecord)
|
|
681
|
+
return;
|
|
682
|
+
const expected = tracker.expectedUserPromptSnippet;
|
|
683
|
+
if (!expected || !record || record.type !== 'user')
|
|
684
|
+
return;
|
|
685
|
+
const normalized = normalizePromptSnippet(extractClaudeUserRecordText(record));
|
|
686
|
+
if (normalized && normalized.includes(expected)) {
|
|
687
|
+
tracker.sawCurrentTurnUserRecord = true;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function shouldIgnoreClaudeRecordForCurrentTurn(tracker, record) {
|
|
691
|
+
const timestampMs = parseRecordTimestampMs(record);
|
|
692
|
+
if (timestampMs != null && timestampMs < tracker.turnStartedAtMs - 2_000) {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
updateClaudeTurnPromptSeen(tracker, record);
|
|
696
|
+
if (record?.type === 'assistant' && tracker.expectedUserPromptSnippet && !tracker.sawCurrentTurnUserRecord) {
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
function shouldIgnoreClaudeRecordForCurrentTurnForTest(turnStartedAtMs, expectedUserPrompt, sawCurrentTurnUserRecord, record) {
|
|
702
|
+
const tracker = {
|
|
703
|
+
done: false,
|
|
704
|
+
sawExplicitCompletion: false,
|
|
705
|
+
finalContent: '',
|
|
706
|
+
usage: undefined,
|
|
707
|
+
lastAssistantText: '',
|
|
708
|
+
detailFingerprints: new Set(),
|
|
709
|
+
pendingToolUseIds: new Set(),
|
|
710
|
+
lastRecordAtMs: turnStartedAtMs,
|
|
711
|
+
sawCompletionSentinel: false,
|
|
712
|
+
turnStartedAtMs,
|
|
713
|
+
expectedUserPromptSnippet: normalizePromptSnippet(expectedUserPrompt).slice(0, 240),
|
|
714
|
+
sawCurrentTurnUserRecord,
|
|
715
|
+
};
|
|
716
|
+
const ignore = shouldIgnoreClaudeRecordForCurrentTurn(tracker, record);
|
|
717
|
+
return {
|
|
718
|
+
ignore,
|
|
719
|
+
sawCurrentTurnUserRecord: tracker.sawCurrentTurnUserRecord,
|
|
720
|
+
};
|
|
277
721
|
}
|
|
278
722
|
function stripAnsi(text) {
|
|
279
723
|
if (!text)
|
|
@@ -310,9 +754,42 @@ function resolveConptyOverwrites(text) {
|
|
|
310
754
|
}
|
|
311
755
|
function normalizeTerminalChunk(text) {
|
|
312
756
|
return resolveConptyOverwrites(stripAnsi(text))
|
|
757
|
+
.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
|
758
|
+
.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓▉▊▋▌▍▎▏▐█▓▒░■□▪▫◼◻◾◽]+/g, ' ')
|
|
313
759
|
.replace(/\u00a0/g, ' ')
|
|
314
760
|
.replace(/[^\S\n]+/g, ' ');
|
|
315
761
|
}
|
|
762
|
+
function isCliBootstrapChromeLine(provider, line) {
|
|
763
|
+
const normalized = String(line || '')
|
|
764
|
+
.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓▉▊▋▌▍▎▏▐█▓▒░■□▪▫◼◻◾◽]+/g, ' ')
|
|
765
|
+
.replace(/\s+/g, ' ')
|
|
766
|
+
.trim();
|
|
767
|
+
if (!normalized)
|
|
768
|
+
return false;
|
|
769
|
+
if (provider === 'claude-cli') {
|
|
770
|
+
let hints = 0;
|
|
771
|
+
if (/\bClaude Code v\d/i.test(normalized))
|
|
772
|
+
hints += 2;
|
|
773
|
+
if (/\bClaude Max\b/i.test(normalized))
|
|
774
|
+
hints += 1;
|
|
775
|
+
if (/\bBot Identity\b/i.test(normalized))
|
|
776
|
+
hints += 1;
|
|
777
|
+
if (/\b(?:Opus|Sonnet|Haiku)\b.*\b(?:effort|thinking)\b/i.test(normalized))
|
|
778
|
+
hints += 1;
|
|
779
|
+
if (/\bclaude\.md\b/i.test(normalized))
|
|
780
|
+
hints += 1;
|
|
781
|
+
if (/[A-Za-z]:\\/.test(normalized))
|
|
782
|
+
hints += 1;
|
|
783
|
+
if (hints >= 2)
|
|
784
|
+
return true;
|
|
785
|
+
}
|
|
786
|
+
if (provider === 'codex-cli') {
|
|
787
|
+
if (/\bcodex\b/i.test(normalized) && /\b(model|approval|sandbox|cwd)\b/i.test(normalized)) {
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
316
793
|
function isSyntheticPromptEchoLine(line) {
|
|
317
794
|
const trimmed = line.trim();
|
|
318
795
|
if (!trimmed)
|
|
@@ -326,12 +803,19 @@ function isSyntheticPromptEchoLine(line) {
|
|
|
326
803
|
}
|
|
327
804
|
function isAutomationNoiseLine(provider, line) {
|
|
328
805
|
const trimmed = line.trim();
|
|
806
|
+
const normalizedLead = trimmed.replace(/^[>›â»âµâ–¸â–¶]+\s*/, '');
|
|
329
807
|
if (!trimmed)
|
|
330
808
|
return false;
|
|
809
|
+
if (isCliBootstrapChromeLine(provider, trimmed))
|
|
810
|
+
return true;
|
|
331
811
|
if (/^Pasting text/i.test(trimmed))
|
|
332
812
|
return true;
|
|
333
813
|
if (/^\[Pasted.*lines?\]/i.test(trimmed))
|
|
334
814
|
return true;
|
|
815
|
+
if (/^\[?Pasted text #?\d+\b/i.test(normalizedLead))
|
|
816
|
+
return true;
|
|
817
|
+
if (/^(?:Cerebrating|Choreographing)\.\.\.?$/i.test(normalizedLead))
|
|
818
|
+
return true;
|
|
335
819
|
if (/^ctrl\+g to edit in notepad$/i.test(trimmed))
|
|
336
820
|
return true;
|
|
337
821
|
if (/^\? for shortcuts$/i.test(trimmed))
|
|
@@ -340,7 +824,15 @@ function isAutomationNoiseLine(provider, line) {
|
|
|
340
824
|
return true;
|
|
341
825
|
if (/^Improve documentation in @filename$/i.test(trimmed))
|
|
342
826
|
return true;
|
|
343
|
-
if (/^permissions on \(shift
|
|
827
|
+
if (/^permissions on \(shift(?:\+|-)tab to cycle\)$/i.test(trimmed))
|
|
828
|
+
return true;
|
|
829
|
+
if (/^bypass permissions on \(shift(?:\+|-)tab to cycle\)$/i.test(trimmed))
|
|
830
|
+
return true;
|
|
831
|
+
if (/^(?:[-+*]\s*)?Brewing(?:\.\.\.?|…)?(?:-+)?$/i.test(normalizedLead))
|
|
832
|
+
return true;
|
|
833
|
+
if (/^Reading \d+ file(?:s)?\.\.\.\s*\(ctrl\+o to expand\)/i.test(normalizedLead))
|
|
834
|
+
return true;
|
|
835
|
+
if (/^\[last-prompt\]$/i.test(normalizedLead))
|
|
344
836
|
return true;
|
|
345
837
|
if (/^[-_=]{4,}$/.test(trimmed))
|
|
346
838
|
return true;
|
|
@@ -503,10 +995,23 @@ async function emitPtyChunk(session, chunk) {
|
|
|
503
995
|
const visibleText = sanitizeVisibleChunk(session.provider, echoTrimmed.text, activeTurn.recentChromeLines);
|
|
504
996
|
if (!visibleText.trim())
|
|
505
997
|
return;
|
|
998
|
+
if (activeTurn.mode === 'passthrough') {
|
|
999
|
+
activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
|
|
1000
|
+
activeTurn.sawVisibleData = true;
|
|
1001
|
+
const passthroughText = visibleText.replace(/\n/g, '\r\n');
|
|
1002
|
+
activeTurn.visibleOutput += passthroughText;
|
|
1003
|
+
activeTurn.callbackChain = activeTurn.callbackChain
|
|
1004
|
+
.catch(() => undefined)
|
|
1005
|
+
.then(() => activeTurn.onRawChunk?.(passthroughText))
|
|
1006
|
+
.then(() => undefined);
|
|
1007
|
+
await activeTurn.callbackChain;
|
|
1008
|
+
return;
|
|
1009
|
+
}
|
|
506
1010
|
const terminalView = extractTerminalFacingText(visibleText, activeTurn.assistantOutputDetected);
|
|
507
1011
|
activeTurn.assistantOutputDetected = terminalView.assistantOutputDetected;
|
|
508
1012
|
if (terminalView.terminalText) {
|
|
509
1013
|
activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
|
|
1014
|
+
activeTurn.sawVisibleData = true;
|
|
510
1015
|
const terminalText = terminalView.terminalText.replace(/\n/g, '\r\n');
|
|
511
1016
|
activeTurn.callbackChain = activeTurn.callbackChain
|
|
512
1017
|
.catch(() => undefined)
|
|
@@ -515,14 +1020,23 @@ async function emitPtyChunk(session, chunk) {
|
|
|
515
1020
|
await activeTurn.callbackChain;
|
|
516
1021
|
}
|
|
517
1022
|
}
|
|
518
|
-
function
|
|
1023
|
+
function getDefaultProviderHome(provider) {
|
|
519
1024
|
if (provider === 'claude-cli') {
|
|
520
|
-
return path.join(os.homedir(), '.claude'
|
|
1025
|
+
return path.join(os.homedir(), '.claude');
|
|
521
1026
|
}
|
|
522
|
-
return path.join(os.homedir(), '.codex'
|
|
1027
|
+
return path.join(os.homedir(), '.codex');
|
|
1028
|
+
}
|
|
1029
|
+
function getProviderSessionRoot(provider, cliHomeDir) {
|
|
1030
|
+
const homeDir = cliHomeDir || getDefaultProviderHome(provider);
|
|
1031
|
+
return provider === 'claude-cli'
|
|
1032
|
+
? path.join(homeDir, 'projects')
|
|
1033
|
+
: path.join(homeDir, 'sessions');
|
|
1034
|
+
}
|
|
1035
|
+
function getProviderSessionRootForTest(provider, cliHomeDir) {
|
|
1036
|
+
return getProviderSessionRoot(provider, cliHomeDir);
|
|
523
1037
|
}
|
|
524
|
-
function listSessionFiles(provider) {
|
|
525
|
-
const root = getProviderSessionRoot(provider);
|
|
1038
|
+
function listSessionFiles(provider, sessionRoot) {
|
|
1039
|
+
const root = sessionRoot || getProviderSessionRoot(provider);
|
|
526
1040
|
if (!fs.existsSync(root))
|
|
527
1041
|
return [];
|
|
528
1042
|
const results = [];
|
|
@@ -623,8 +1137,8 @@ function recentFileContainsPrompt(provider, filePath, promptText, startedAtMs) {
|
|
|
623
1137
|
}
|
|
624
1138
|
return false;
|
|
625
1139
|
}
|
|
626
|
-
function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocator) {
|
|
627
|
-
const candidates = listSessionFiles(provider)
|
|
1140
|
+
function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocator, sessionRoot) {
|
|
1141
|
+
const candidates = listSessionFiles(provider, sessionRoot)
|
|
628
1142
|
.map((candidate) => {
|
|
629
1143
|
let mtimeMs = 0;
|
|
630
1144
|
try {
|
|
@@ -699,6 +1213,24 @@ function findCodexSessionFileBySessionId(sessionId) {
|
|
|
699
1213
|
}
|
|
700
1214
|
return null;
|
|
701
1215
|
}
|
|
1216
|
+
function describePtyExit(exitArgs) {
|
|
1217
|
+
const first = exitArgs[0];
|
|
1218
|
+
const code = first && typeof first === 'object'
|
|
1219
|
+
? (first.exitCode ?? first.code)
|
|
1220
|
+
: first;
|
|
1221
|
+
const signal = first && typeof first === 'object'
|
|
1222
|
+
? first.signal
|
|
1223
|
+
: exitArgs[1];
|
|
1224
|
+
const parts = [];
|
|
1225
|
+
if (code !== undefined && code !== null)
|
|
1226
|
+
parts.push(`exitCode=${String(code)}`);
|
|
1227
|
+
if (signal !== undefined && signal !== null)
|
|
1228
|
+
parts.push(`signal=${String(signal)}`);
|
|
1229
|
+
return parts.length > 0 ? parts.join(', ') : 'no exit details';
|
|
1230
|
+
}
|
|
1231
|
+
function formatClosedSessionError(session, context) {
|
|
1232
|
+
return new Error(`${session.provider} PTY session ${context}${session.exitReason ? ` (${session.exitReason})` : ''}`);
|
|
1233
|
+
}
|
|
702
1234
|
function formatToolUseDetail(toolName, input) {
|
|
703
1235
|
if (!input || typeof input !== 'object')
|
|
704
1236
|
return `🔧 ${toolName}`;
|
|
@@ -818,16 +1350,49 @@ async function emitAssistantChunk(tracker, nextText, cb) {
|
|
|
818
1350
|
if (delta) {
|
|
819
1351
|
await cb(delta);
|
|
820
1352
|
}
|
|
1353
|
+
return;
|
|
821
1354
|
}
|
|
1355
|
+
const separator = previous.match(/\n\s*\n\s*$/) ? '' : '\n\n';
|
|
1356
|
+
await cb(`${separator}${normalized}`);
|
|
1357
|
+
}
|
|
1358
|
+
async function emitAssistantChunkSequenceForTest(chunks) {
|
|
1359
|
+
const tracker = {
|
|
1360
|
+
done: false,
|
|
1361
|
+
sawExplicitCompletion: false,
|
|
1362
|
+
finalContent: '',
|
|
1363
|
+
detailFingerprints: new Set(),
|
|
1364
|
+
lastAssistantText: '',
|
|
1365
|
+
pendingToolUseIds: new Set(),
|
|
1366
|
+
lastRecordAtMs: 0,
|
|
1367
|
+
sawCompletionSentinel: false,
|
|
1368
|
+
turnStartedAtMs: 0,
|
|
1369
|
+
expectedUserPromptSnippet: '',
|
|
1370
|
+
sawCurrentTurnUserRecord: false,
|
|
1371
|
+
};
|
|
1372
|
+
const emitted = [];
|
|
1373
|
+
for (const chunk of chunks) {
|
|
1374
|
+
await emitAssistantChunk(tracker, chunk, async (delta) => {
|
|
1375
|
+
emitted.push(delta);
|
|
1376
|
+
});
|
|
1377
|
+
}
|
|
1378
|
+
return emitted;
|
|
822
1379
|
}
|
|
823
1380
|
function parseClaudeSessionRecord(record) {
|
|
824
1381
|
const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : undefined;
|
|
825
1382
|
if (record?.type === 'assistant' && record?.message) {
|
|
1383
|
+
// token.txt Change 1: keep `inputTokens` as the sum (back-compat
|
|
1384
|
+
// with downstream readers — SSE meta events, ChatJob persistence,
|
|
1385
|
+
// etc.) AND populate the three breakdown fields so the LlmUsageLog
|
|
1386
|
+
// path can record cache-aware costs.
|
|
1387
|
+
const fresh = record.message.usage?.input_tokens || 0;
|
|
1388
|
+
const cacheCreation = record.message.usage?.cache_creation_input_tokens || 0;
|
|
1389
|
+
const cacheRead = record.message.usage?.cache_read_input_tokens || 0;
|
|
826
1390
|
const usage = record.message.usage
|
|
827
1391
|
? {
|
|
828
|
-
inputTokens:
|
|
829
|
-
|
|
830
|
-
|
|
1392
|
+
inputTokens: fresh + cacheCreation + cacheRead,
|
|
1393
|
+
inputTokensFresh: fresh,
|
|
1394
|
+
inputTokensCacheCreation: cacheCreation,
|
|
1395
|
+
inputTokensCacheRead: cacheRead,
|
|
831
1396
|
outputTokens: record.message.usage.output_tokens || 0,
|
|
832
1397
|
}
|
|
833
1398
|
: undefined;
|
|
@@ -847,6 +1412,14 @@ function parseClaudeSessionRecord(record) {
|
|
|
847
1412
|
if (typeof block.id === 'string' && block.id.trim()) {
|
|
848
1413
|
openedToolUseIds.push(block.id);
|
|
849
1414
|
}
|
|
1415
|
+
// Always emit a structured tool_call activity for every tool_use block.
|
|
1416
|
+
// This is consumed by workflow-engine to produce worker_tool_call events.
|
|
1417
|
+
activities.push({
|
|
1418
|
+
type: 'tool_call',
|
|
1419
|
+
label: toolName,
|
|
1420
|
+
key: block.id || undefined,
|
|
1421
|
+
timestamp: Date.now(),
|
|
1422
|
+
});
|
|
850
1423
|
// Emit structured live activity events
|
|
851
1424
|
if (toolName === 'Agent') {
|
|
852
1425
|
const activityLabel = (0, live_activity_1.agentToolUseToLabel)(block.input || {});
|
|
@@ -911,8 +1484,39 @@ function parseClaudeSessionRecord(record) {
|
|
|
911
1484
|
const resolvedToolUseIds = record.message.content
|
|
912
1485
|
.map((block) => (typeof block?.tool_use_id === 'string' ? block.tool_use_id : ''))
|
|
913
1486
|
.filter((toolUseId) => !!toolUseId);
|
|
1487
|
+
// Build tool_result activities so workflow-engine can emit worker_tool_result events
|
|
1488
|
+
const resultActivities = [];
|
|
1489
|
+
for (const block of record.message.content) {
|
|
1490
|
+
if (!block || typeof block !== 'object')
|
|
1491
|
+
continue;
|
|
1492
|
+
if (typeof block.tool_use_id !== 'string' || !block.tool_use_id)
|
|
1493
|
+
continue;
|
|
1494
|
+
const isError = !!block.is_error;
|
|
1495
|
+
// Extract a short summary from the result content (could be string or array of blocks)
|
|
1496
|
+
let summary = '';
|
|
1497
|
+
if (typeof block.content === 'string') {
|
|
1498
|
+
summary = block.content.replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
1499
|
+
}
|
|
1500
|
+
else if (Array.isArray(block.content)) {
|
|
1501
|
+
const textBlocks = block.content
|
|
1502
|
+
.map((c) => (c && typeof c === 'object' && typeof c.text === 'string' ? c.text : ''))
|
|
1503
|
+
.filter(Boolean);
|
|
1504
|
+
summary = textBlocks.join(' ').replace(/\s+/g, ' ').trim().slice(0, 200);
|
|
1505
|
+
}
|
|
1506
|
+
resultActivities.push({
|
|
1507
|
+
type: 'tool_result',
|
|
1508
|
+
label: summary || (isError ? 'tool error' : 'tool completed'),
|
|
1509
|
+
key: block.tool_use_id,
|
|
1510
|
+
isError,
|
|
1511
|
+
timestamp: Date.now(),
|
|
1512
|
+
});
|
|
1513
|
+
}
|
|
914
1514
|
if (resolvedToolUseIds.length > 0) {
|
|
915
|
-
return {
|
|
1515
|
+
return {
|
|
1516
|
+
sessionId,
|
|
1517
|
+
resolvedToolUseIds,
|
|
1518
|
+
...(resultActivities.length > 0 ? { activities: resultActivities } : {}),
|
|
1519
|
+
};
|
|
916
1520
|
}
|
|
917
1521
|
}
|
|
918
1522
|
// system/turn_duration is a fallback completion signal from the CLI
|
|
@@ -957,18 +1561,31 @@ function parseCodexSessionRecord(record) {
|
|
|
957
1561
|
if (record.type === 'event_msg' && record.payload?.type === 'token_count' && record.payload?.info?.total_token_usage) {
|
|
958
1562
|
const lastUsage = record.payload.info.last_token_usage;
|
|
959
1563
|
if (lastUsage) {
|
|
1564
|
+
// token.txt Change 1: keep inputTokens sum + add breakdown.
|
|
1565
|
+
// Codex exposes only one cache field (cached_input_tokens) —
|
|
1566
|
+
// we map it to inputTokensCacheRead since "cached" semantically
|
|
1567
|
+
// means "served from cache." inputTokensCacheCreation is always
|
|
1568
|
+
// 0 for Codex (no separate write billing on OpenAI).
|
|
1569
|
+
const fresh = lastUsage.input_tokens || 0;
|
|
1570
|
+
const cacheRead = lastUsage.cached_input_tokens || 0;
|
|
960
1571
|
return {
|
|
961
1572
|
usage: {
|
|
962
|
-
inputTokens:
|
|
963
|
-
|
|
1573
|
+
inputTokens: fresh + cacheRead,
|
|
1574
|
+
inputTokensFresh: fresh,
|
|
1575
|
+
inputTokensCacheCreation: 0,
|
|
1576
|
+
inputTokensCacheRead: cacheRead,
|
|
964
1577
|
outputTokens: lastUsage.output_tokens || 0,
|
|
965
1578
|
},
|
|
966
1579
|
};
|
|
967
1580
|
}
|
|
1581
|
+
const fresh = record.payload.info.total_token_usage.input_tokens || 0;
|
|
1582
|
+
const cacheRead = record.payload.info.total_token_usage.cached_input_tokens || 0;
|
|
968
1583
|
return {
|
|
969
1584
|
usage: {
|
|
970
|
-
inputTokens:
|
|
971
|
-
|
|
1585
|
+
inputTokens: fresh + cacheRead,
|
|
1586
|
+
inputTokensFresh: fresh,
|
|
1587
|
+
inputTokensCacheCreation: 0,
|
|
1588
|
+
inputTokensCacheRead: cacheRead,
|
|
972
1589
|
outputTokens: record.payload.info.total_token_usage.output_tokens || 0,
|
|
973
1590
|
},
|
|
974
1591
|
};
|
|
@@ -992,7 +1609,66 @@ function parseCodexSessionRecord(record) {
|
|
|
992
1609
|
}
|
|
993
1610
|
async function waitForFirstTerminalData(session) {
|
|
994
1611
|
await Promise.race([session.readyPromise, delay(5000)]);
|
|
995
|
-
|
|
1612
|
+
if (!session.startupDelayApplied) {
|
|
1613
|
+
await delay(session.startupDelayMs);
|
|
1614
|
+
session.startupDelayApplied = true;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
async function waitForOutputToSettle(session, quietMs = 500, maxMs = 4000) {
|
|
1618
|
+
const start = Date.now();
|
|
1619
|
+
let lastLen = session.recentTerminalOutput.length;
|
|
1620
|
+
while (Date.now() - start < maxMs) {
|
|
1621
|
+
await delay(quietMs);
|
|
1622
|
+
const currentLen = session.recentTerminalOutput.length;
|
|
1623
|
+
if (currentLen === lastLen)
|
|
1624
|
+
return;
|
|
1625
|
+
lastLen = currentLen;
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
function buildWarmTimeoutError(provider, timeoutMs) {
|
|
1629
|
+
const err = new Error(`${provider} warm session timed out after ${Math.floor(timeoutMs / 1000)}s`);
|
|
1630
|
+
err.code = 'CLI_WARM_TIMEOUT';
|
|
1631
|
+
err.name = 'CliWarmTimeoutError';
|
|
1632
|
+
return err;
|
|
1633
|
+
}
|
|
1634
|
+
function withWarmTimeout(promise, timeoutMs, onTimeout, provider) {
|
|
1635
|
+
let timeout = null;
|
|
1636
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
1637
|
+
timeout = setTimeout(() => {
|
|
1638
|
+
try {
|
|
1639
|
+
onTimeout();
|
|
1640
|
+
}
|
|
1641
|
+
finally {
|
|
1642
|
+
reject(buildWarmTimeoutError(provider, timeoutMs));
|
|
1643
|
+
}
|
|
1644
|
+
}, timeoutMs);
|
|
1645
|
+
});
|
|
1646
|
+
return Promise.race([promise, timeoutPromise]).finally(() => {
|
|
1647
|
+
if (timeout)
|
|
1648
|
+
clearTimeout(timeout);
|
|
1649
|
+
});
|
|
1650
|
+
}
|
|
1651
|
+
function logCliWarmEvent(event, fields) {
|
|
1652
|
+
const suffix = Object.entries(fields)
|
|
1653
|
+
.filter(([, value]) => value !== undefined && value !== null && value !== '')
|
|
1654
|
+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`)
|
|
1655
|
+
.join(' ');
|
|
1656
|
+
console.info(`[cli-warm] ${event}${suffix ? ` ${suffix}` : ''}`);
|
|
1657
|
+
}
|
|
1658
|
+
function buildCliWarmDiscardFailureReason(err) {
|
|
1659
|
+
const error = err;
|
|
1660
|
+
if (error?.code === 'CLI_WARM_TIMEOUT')
|
|
1661
|
+
return 'warm_timeout';
|
|
1662
|
+
if (error?.authRequired === true)
|
|
1663
|
+
return 'auth_required';
|
|
1664
|
+
if (error?.code === 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT')
|
|
1665
|
+
return 'warm_startup_timeout';
|
|
1666
|
+
if (error?.name === 'AbortError')
|
|
1667
|
+
return 'warm_aborted';
|
|
1668
|
+
const message = String(error?.message || '').toLowerCase();
|
|
1669
|
+
if (message.includes('closed before it was ready'))
|
|
1670
|
+
return 'session_closed_before_ready';
|
|
1671
|
+
return 'warm_failed';
|
|
996
1672
|
}
|
|
997
1673
|
async function writePromptInChunks(pty, promptText, chunkSize = 512, chunkDelayMs = 15) {
|
|
998
1674
|
for (let offset = 0; offset < promptText.length; offset += chunkSize) {
|
|
@@ -1025,41 +1701,389 @@ async function writeInteractivePrompt(session, promptText) {
|
|
|
1025
1701
|
await delay(session.submitDelayMs);
|
|
1026
1702
|
session.pty.write('\r');
|
|
1027
1703
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1704
|
+
async function writeNakedPrompt(session, text) {
|
|
1705
|
+
await writePromptInChunks(session.pty, text);
|
|
1706
|
+
await delay(session.submitDelayMs);
|
|
1707
|
+
session.pty.write('\r');
|
|
1708
|
+
}
|
|
1709
|
+
function parseJsonObject(raw) {
|
|
1710
|
+
const trimmed = String(raw || '').trim();
|
|
1711
|
+
if (!trimmed)
|
|
1712
|
+
return null;
|
|
1713
|
+
try {
|
|
1714
|
+
const parsed = JSON.parse(trimmed);
|
|
1715
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1716
|
+
return parsed;
|
|
1717
|
+
}
|
|
1718
|
+
}
|
|
1719
|
+
catch {
|
|
1720
|
+
// ignore invalid JSON and fall back to the default CLI behavior
|
|
1721
|
+
}
|
|
1722
|
+
return null;
|
|
1723
|
+
}
|
|
1724
|
+
function buildClaudeCliArgs(resumeSessionId, newSessionId, settings) {
|
|
1725
|
+
const args = ['--dangerously-skip-permissions'];
|
|
1726
|
+
const model = String(settings?.model || '').trim();
|
|
1727
|
+
if (model && model.toLowerCase() !== 'default') {
|
|
1728
|
+
args.push('--model', model);
|
|
1729
|
+
}
|
|
1730
|
+
const effortLevel = String(settings?.effortLevel || '').trim().toLowerCase();
|
|
1731
|
+
if (['low', 'medium', 'high', 'max'].includes(effortLevel)) {
|
|
1732
|
+
args.push('--effort', effortLevel);
|
|
1733
|
+
}
|
|
1734
|
+
const runtimeSettings = {};
|
|
1735
|
+
const outputStyle = String(settings?.outputStyle || '').trim();
|
|
1736
|
+
if (outputStyle && outputStyle.toLowerCase() !== 'default')
|
|
1737
|
+
runtimeSettings.outputStyle = outputStyle;
|
|
1738
|
+
if (settings?.fastMode === true)
|
|
1739
|
+
runtimeSettings.fastMode = true;
|
|
1740
|
+
const permissions = parseJsonObject(settings?.permissionsJson);
|
|
1741
|
+
if (permissions)
|
|
1742
|
+
runtimeSettings.permissions = permissions;
|
|
1743
|
+
if (Object.keys(runtimeSettings).length > 0) {
|
|
1744
|
+
args.push('--settings', JSON.stringify(runtimeSettings));
|
|
1745
|
+
}
|
|
1746
|
+
if (resumeSessionId) {
|
|
1747
|
+
args.push('--resume', resumeSessionId);
|
|
1748
|
+
}
|
|
1749
|
+
else if (newSessionId) {
|
|
1750
|
+
args.push('--session-id', newSessionId);
|
|
1751
|
+
}
|
|
1752
|
+
return args;
|
|
1753
|
+
}
|
|
1754
|
+
function appendCodexConfigOverride(args, key, value) {
|
|
1755
|
+
const trimmed = String(value || '').trim();
|
|
1756
|
+
if (!trimmed)
|
|
1757
|
+
return;
|
|
1758
|
+
args.push('-c', `${key}=${JSON.stringify(trimmed)}`);
|
|
1759
|
+
}
|
|
1760
|
+
function buildCodexCliArgs(resumeSessionId, settings) {
|
|
1761
|
+
const model = String(settings?.model || '').trim();
|
|
1762
|
+
const sandboxPolicy = String(settings?.sandboxPolicy || '').trim() || 'danger-full-access';
|
|
1763
|
+
const approvalPolicy = String(settings?.approvalPolicy || '').trim() || 'never';
|
|
1764
|
+
const args = resumeSessionId ? ['resume', resumeSessionId] : [];
|
|
1765
|
+
args.push('--no-alt-screen');
|
|
1766
|
+
if (model && model.toLowerCase() !== 'default') {
|
|
1767
|
+
args.push('-m', model);
|
|
1768
|
+
}
|
|
1769
|
+
args.push('-s', sandboxPolicy, '-a', approvalPolicy);
|
|
1770
|
+
appendCodexConfigOverride(args, 'shell_environment_policy.inherit', 'all');
|
|
1771
|
+
appendCodexConfigOverride(args, 'model_reasoning_effort', settings?.reasoningEffort);
|
|
1772
|
+
appendCodexConfigOverride(args, 'personality', settings?.personality);
|
|
1773
|
+
appendCodexConfigOverride(args, 'service_tier', settings?.serviceTier);
|
|
1774
|
+
return args;
|
|
1775
|
+
}
|
|
1776
|
+
function trimPassthroughSlashChrome(command, rawText) {
|
|
1777
|
+
const normalized = rawText.replace(/\r\n/g, '\n').trim();
|
|
1778
|
+
if (!command.startsWith('/'))
|
|
1779
|
+
return normalized;
|
|
1780
|
+
const markers = [
|
|
1781
|
+
`\n> ${command}\n\n`,
|
|
1782
|
+
`\n${command}\n\n`,
|
|
1783
|
+
`\n> ${command}\n`,
|
|
1784
|
+
`\n${command}\n`,
|
|
1785
|
+
`> ${command}\n\n`,
|
|
1786
|
+
`${command}\n\n`,
|
|
1787
|
+
];
|
|
1788
|
+
for (const marker of markers) {
|
|
1789
|
+
const idx = normalized.indexOf(marker);
|
|
1790
|
+
if (idx < 0)
|
|
1791
|
+
continue;
|
|
1792
|
+
const candidate = normalized.slice(idx + marker.length).trim();
|
|
1793
|
+
if (candidate)
|
|
1794
|
+
return cleanPassthroughSlashText(command, candidate);
|
|
1795
|
+
}
|
|
1796
|
+
return cleanPassthroughSlashText(command, normalized);
|
|
1797
|
+
}
|
|
1798
|
+
function finalizePassthroughContent(command, visibleOutput, rawOutput) {
|
|
1799
|
+
const visible = String(visibleOutput || '').trim();
|
|
1800
|
+
if (visible) {
|
|
1801
|
+
return trimPassthroughSlashChrome(command, visible);
|
|
1802
|
+
}
|
|
1803
|
+
const rawNormalized = normalizeTerminalChunk(rawOutput || '');
|
|
1804
|
+
return trimPassthroughSlashChrome(command, rawNormalized);
|
|
1805
|
+
}
|
|
1806
|
+
function cleanPassthroughSlashText(command, text) {
|
|
1807
|
+
const normalizedCommand = String(command || '').trim().toLowerCase();
|
|
1808
|
+
if (!text.trim()) {
|
|
1809
|
+
return text.trim();
|
|
1810
|
+
}
|
|
1811
|
+
const kept = [];
|
|
1812
|
+
let lastFingerprint = '';
|
|
1813
|
+
for (const rawLine of text.replace(/\r\n/g, '\n').split('\n')) {
|
|
1814
|
+
const cleanedLine = cleanNormalizedSlashPassthroughLine(normalizedCommand, rawLine);
|
|
1815
|
+
if (cleanedLine === null)
|
|
1816
|
+
continue;
|
|
1817
|
+
if (!cleanedLine) {
|
|
1818
|
+
if (kept.length > 0 && kept[kept.length - 1] !== '')
|
|
1819
|
+
kept.push('');
|
|
1820
|
+
continue;
|
|
1821
|
+
}
|
|
1822
|
+
const fingerprint = cleanedLine.toLowerCase();
|
|
1823
|
+
if (fingerprint === lastFingerprint)
|
|
1824
|
+
continue;
|
|
1825
|
+
kept.push(cleanedLine);
|
|
1826
|
+
lastFingerprint = fingerprint;
|
|
1827
|
+
}
|
|
1828
|
+
return kept.join('\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
1829
|
+
}
|
|
1830
|
+
function cleanSlashPassthroughLine(command, rawLine) {
|
|
1831
|
+
const trimmedRight = rawLine.replace(/\s+$/g, '');
|
|
1832
|
+
const trimmed = trimmedRight.trim();
|
|
1833
|
+
if (!trimmed)
|
|
1834
|
+
return '';
|
|
1835
|
+
let withoutChrome = trimmedRight
|
|
1836
|
+
.replace(/^\s*Esc to cancel\s+/i, '')
|
|
1837
|
+
.replace(/(?:^|\s)(?:[>›â»âµâ–¸â–¶]+\s*)?bypass permissions on \(shift(?:\+|-)tab to cycle\).*$/i, '')
|
|
1838
|
+
.replace(/\s+[•â—·-]+\s*\/effort\s*$/i, '')
|
|
1839
|
+
.replace(/\s+\/effort\s*$/i, '')
|
|
1840
|
+
.trim();
|
|
1841
|
+
if (!withoutChrome)
|
|
1842
|
+
return null;
|
|
1843
|
+
if (/^[─â”│â•┄┈]{4,}$/u.test(withoutChrome))
|
|
1844
|
+
return null;
|
|
1845
|
+
if (/^Status(?:\s+Config)?(?:\s+Usage)?(?:\s+Stats)?$/i.test(withoutChrome))
|
|
1846
|
+
return null;
|
|
1847
|
+
if (/^(?:Loading usage data|Scanning local sessions)$/i.test(withoutChrome))
|
|
1848
|
+
return null;
|
|
1849
|
+
if (command === '/usage' && /^[dw]\s+to\s+\w+/i.test(withoutChrome))
|
|
1850
|
+
return null;
|
|
1851
|
+
if (/^(?:[>›â»âµâ–¸â–¶]+\s*)?bypass permissions on \(shift(?:\+|-)tab to cycle\)/i.test(withoutChrome))
|
|
1852
|
+
return null;
|
|
1853
|
+
if (/^permissions on \(shift(?:\+|-)tab to cycle\)/i.test(withoutChrome))
|
|
1854
|
+
return null;
|
|
1855
|
+
return withoutChrome || null;
|
|
1856
|
+
}
|
|
1857
|
+
function cleanNormalizedSlashPassthroughLine(command, rawLine) {
|
|
1858
|
+
const trimmedRight = rawLine.replace(/\s+$/g, '');
|
|
1859
|
+
const trimmed = trimmedRight.trim();
|
|
1860
|
+
if (!trimmed)
|
|
1861
|
+
return '';
|
|
1862
|
+
let withoutChrome = trimmedRight
|
|
1863
|
+
.replace(/^\s*Esc to cancel\s+/i, '')
|
|
1864
|
+
.replace(/(?:^|\s)(?:>+\s*)?bypass permissions on \(shift(?:\+|-)tab to cycle\).*$/i, '')
|
|
1865
|
+
.replace(/\s+[•●·-]+\s*\/effort\s*$/i, '')
|
|
1866
|
+
.replace(/\s+\/effort\s*$/i, '')
|
|
1867
|
+
.trim();
|
|
1868
|
+
if (!withoutChrome)
|
|
1869
|
+
return null;
|
|
1870
|
+
if (/^[\u2500-\u257F\u2580-\u259F=_-]{4,}$/u.test(withoutChrome))
|
|
1871
|
+
return null;
|
|
1872
|
+
if (/^>+\s*$/.test(withoutChrome))
|
|
1873
|
+
return null;
|
|
1874
|
+
if (/^permissions on \(shift(?:\+|-)tab to cycle\)/i.test(withoutChrome))
|
|
1875
|
+
return null;
|
|
1876
|
+
if (/^bypass permissions on \(shift(?:\+|-)tab to cycle\)/i.test(withoutChrome))
|
|
1877
|
+
return null;
|
|
1878
|
+
if (command === '/cost' || command === '/usage') {
|
|
1879
|
+
if (/^Status(?:\s+Config)?(?:\s+Usage)?(?:\s+Stats)?$/i.test(withoutChrome))
|
|
1880
|
+
return null;
|
|
1881
|
+
if (/^(?:Loading usage data|Scanning local sessions)$/i.test(withoutChrome))
|
|
1882
|
+
return null;
|
|
1883
|
+
if (command === '/usage' && /^[dw]\s+to\s+\w+/i.test(withoutChrome))
|
|
1884
|
+
return null;
|
|
1885
|
+
}
|
|
1886
|
+
return withoutChrome || null;
|
|
1887
|
+
}
|
|
1888
|
+
function trimPassthroughSlashChromeForTest(command, rawText) {
|
|
1889
|
+
return trimPassthroughSlashChrome(command, rawText);
|
|
1890
|
+
}
|
|
1891
|
+
function finalizePassthroughContentForTest(command, visibleOutput, rawOutput) {
|
|
1892
|
+
return finalizePassthroughContent(command, visibleOutput, rawOutput);
|
|
1893
|
+
}
|
|
1030
1894
|
class LocalCliPtySessionManager {
|
|
1031
1895
|
sessions = new Map();
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1896
|
+
async warmSession(opts) {
|
|
1897
|
+
const key = sessionKey(opts.conversationId, opts.botId);
|
|
1898
|
+
const isServiceMode = process.env.FUNOLIO_RUN_CONTEXT === 'windows-service'
|
|
1899
|
+
|| process.argv.includes('--mode') && (process.argv.includes('service') || process.argv.includes('windows-service'));
|
|
1900
|
+
const requestedUseConpty = opts.useConpty ?? !isServiceMode;
|
|
1901
|
+
let session = this.sessions.get(key);
|
|
1902
|
+
if (session
|
|
1903
|
+
&& (session.provider !== opts.provider
|
|
1904
|
+
|| session.cwd !== opts.cwd
|
|
1905
|
+
|| session.useConpty !== requestedUseConpty
|
|
1906
|
+
|| shouldRecycleClaudeSessionForFreshAuth(session)
|
|
1907
|
+
|| shouldResetClaudeSessionForAuthChange(session)
|
|
1908
|
+
|| session.closed)) {
|
|
1909
|
+
if (session.provider === 'claude-cli' && shouldRecycleClaudeSessionForFreshAuth(session)) {
|
|
1910
|
+
console.info(`[claude-auth] recycling warm session before resume (${opts.botId} ${opts.conversationId})`);
|
|
1911
|
+
}
|
|
1912
|
+
else if (session.provider === 'claude-cli' && shouldResetClaudeSessionForAuthChange(session)) {
|
|
1913
|
+
console.info(`[claude-auth] recycling warm session after auth change (${opts.botId})`);
|
|
1914
|
+
}
|
|
1915
|
+
this.closeSession(key);
|
|
1916
|
+
session = undefined;
|
|
1917
|
+
}
|
|
1918
|
+
const reusedExistingSession = !!session;
|
|
1919
|
+
if (!session) {
|
|
1920
|
+
if (opts.provider === 'claude-cli') {
|
|
1921
|
+
await ensureClaudeAuthReadyForFreshSession();
|
|
1044
1922
|
}
|
|
1923
|
+
session = this.createSession(key, opts.botId, opts.conversationId, opts.provider, opts.botSettings, opts.cwd, requestedUseConpty, {
|
|
1924
|
+
actorId: opts.toolActorId,
|
|
1925
|
+
projectId: opts.toolProjectId,
|
|
1926
|
+
todoTaskId: opts.currentTodoTaskId,
|
|
1927
|
+
}, opts.resumeSessionId, opts.newSessionId);
|
|
1928
|
+
this.sessions.set(key, session);
|
|
1045
1929
|
}
|
|
1930
|
+
if (opts.topicId !== undefined) {
|
|
1931
|
+
session.topicId = opts.topicId || null;
|
|
1932
|
+
}
|
|
1933
|
+
if (opts.runtimeMode !== undefined) {
|
|
1934
|
+
session.warmRuntimeMode = opts.runtimeMode || null;
|
|
1935
|
+
}
|
|
1936
|
+
if (reusedExistingSession && !session.warmPromise && !session.warmReadyAtMs && session.readyResolved) {
|
|
1937
|
+
(0, managed_process_registry_1.markReused)(key);
|
|
1938
|
+
logCliWarmEvent('warm_reuse_existing', {
|
|
1939
|
+
conversationId: opts.conversationId,
|
|
1940
|
+
botId: opts.botId,
|
|
1941
|
+
provider: opts.provider,
|
|
1942
|
+
runtimeMode: session.warmRuntimeMode || opts.runtimeMode || 'local_desktop',
|
|
1943
|
+
topicId: session.topicId,
|
|
1944
|
+
reusedExistingSession,
|
|
1945
|
+
ageMs: Date.now() - session.lastUsedAtMs,
|
|
1946
|
+
});
|
|
1947
|
+
return {
|
|
1948
|
+
sessionId: session.sessionId,
|
|
1949
|
+
reusedExistingSession,
|
|
1950
|
+
readyAgeMs: Math.max(0, Date.now() - session.lastUsedAtMs),
|
|
1951
|
+
};
|
|
1952
|
+
}
|
|
1953
|
+
if (!session.warmPromise && !session.warmReadyAtMs) {
|
|
1954
|
+
const warmStartedAtMs = Date.now();
|
|
1955
|
+
session.warmRequestedAtMs = warmStartedAtMs;
|
|
1956
|
+
const warmTask = (async () => {
|
|
1957
|
+
await waitForFirstTerminalData(session);
|
|
1958
|
+
if (session.closed) {
|
|
1959
|
+
throw new Error(`${opts.provider} warm session closed before it was ready`);
|
|
1960
|
+
}
|
|
1961
|
+
if (!session.readyResolved) {
|
|
1962
|
+
throw new Error(`${opts.provider} warm session did not produce startup output`);
|
|
1963
|
+
}
|
|
1964
|
+
const authFailure = buildCliInteractiveAuthError(session.provider, session.recentTerminalOutput);
|
|
1965
|
+
if (authFailure) {
|
|
1966
|
+
throw authFailure;
|
|
1967
|
+
}
|
|
1968
|
+
await waitForOutputToSettle(session);
|
|
1969
|
+
session.warmReadyAtMs = Date.now();
|
|
1970
|
+
})();
|
|
1971
|
+
session.warmPromise = withWarmTimeout(warmTask, opts.timeoutMs ?? CLI_WARM_TIMEOUT_MS, () => this.closeSession(key), opts.provider).catch((err) => {
|
|
1972
|
+
if (this.sessions.get(key) === session) {
|
|
1973
|
+
this.closeSession(key);
|
|
1974
|
+
}
|
|
1975
|
+
throw err;
|
|
1976
|
+
});
|
|
1977
|
+
}
|
|
1978
|
+
await session.warmPromise;
|
|
1979
|
+
const readyAtMs = session.warmReadyAtMs || Date.now();
|
|
1980
|
+
return {
|
|
1981
|
+
sessionId: session.sessionId,
|
|
1982
|
+
reusedExistingSession,
|
|
1983
|
+
readyAgeMs: Math.max(0, Date.now() - readyAtMs),
|
|
1984
|
+
};
|
|
1046
1985
|
}
|
|
1047
1986
|
async runTurn(opts) {
|
|
1048
1987
|
const key = sessionKey(opts.conversationId, opts.botId);
|
|
1988
|
+
// Default to WinPTY (useConpty: false) when running as a detached sidecar
|
|
1989
|
+
// without a console window. ConPTY requires a console host and fails silently
|
|
1990
|
+
// when the process was launched with CREATE_NO_WINDOW.
|
|
1991
|
+
const isServiceMode = process.env.FUNOLIO_RUN_CONTEXT === 'windows-service'
|
|
1992
|
+
|| process.argv.includes('--mode') && (process.argv.includes('service') || process.argv.includes('windows-service'));
|
|
1993
|
+
const requestedUseConpty = opts.useConpty ?? !isServiceMode;
|
|
1049
1994
|
let session = this.sessions.get(key);
|
|
1995
|
+
const preserveFreshWarm = !!session
|
|
1996
|
+
&& !!opts.forceFreshSession
|
|
1997
|
+
&& session.provider === 'codex-cli'
|
|
1998
|
+
&& !!session.warmReadyAtMs
|
|
1999
|
+
&& !session.sessionId;
|
|
1050
2000
|
if (session
|
|
1051
|
-
&& (opts.forceFreshSession
|
|
2001
|
+
&& ((opts.forceFreshSession && !preserveFreshWarm)
|
|
2002
|
+
|| session.provider !== opts.provider
|
|
2003
|
+
|| session.cwd !== opts.cwd
|
|
2004
|
+
|| session.useConpty !== requestedUseConpty
|
|
2005
|
+
|| shouldRecycleClaudeSessionForFreshAuth(session)
|
|
2006
|
+
|| shouldResetClaudeSessionForAuthChange(session)
|
|
2007
|
+
|| session.closed)) {
|
|
2008
|
+
if (session.provider === 'claude-cli' && shouldRecycleClaudeSessionForFreshAuth(session)) {
|
|
2009
|
+
console.info(`[claude-auth] recycling turn session before resume (${opts.botId} ${opts.conversationId})`);
|
|
2010
|
+
}
|
|
2011
|
+
else if (session.provider === 'claude-cli' && shouldResetClaudeSessionForAuthChange(session)) {
|
|
2012
|
+
console.info(`[claude-auth] recycling turn session after auth change (${opts.botId})`);
|
|
2013
|
+
}
|
|
1052
2014
|
this.closeSession(key);
|
|
1053
2015
|
session = undefined;
|
|
1054
2016
|
}
|
|
2017
|
+
if (session?.warmPromise) {
|
|
2018
|
+
try {
|
|
2019
|
+
await session.warmPromise;
|
|
2020
|
+
logCliWarmEvent('warm_reused', {
|
|
2021
|
+
conversationId: opts.conversationId,
|
|
2022
|
+
botId: opts.botId,
|
|
2023
|
+
provider: opts.provider,
|
|
2024
|
+
runtimeMode: session.warmRuntimeMode || 'local_desktop',
|
|
2025
|
+
topicId: session.topicId,
|
|
2026
|
+
reusedExistingSession: true,
|
|
2027
|
+
ageMs: session.warmReadyAtMs ? Date.now() - session.warmReadyAtMs : 0,
|
|
2028
|
+
});
|
|
2029
|
+
session.warmPromise = null;
|
|
2030
|
+
session.warmRequestedAtMs = null;
|
|
2031
|
+
session.warmReadyAtMs = null;
|
|
2032
|
+
}
|
|
2033
|
+
catch (err) {
|
|
2034
|
+
logCliWarmEvent('warm_discarded', {
|
|
2035
|
+
conversationId: opts.conversationId,
|
|
2036
|
+
botId: opts.botId,
|
|
2037
|
+
provider: opts.provider,
|
|
2038
|
+
runtimeMode: session.warmRuntimeMode || 'local_desktop',
|
|
2039
|
+
topicId: session.topicId,
|
|
2040
|
+
reusedExistingSession: true,
|
|
2041
|
+
failureReason: buildCliWarmDiscardFailureReason(err),
|
|
2042
|
+
});
|
|
2043
|
+
if (this.sessions.get(key) === session) {
|
|
2044
|
+
this.closeSession(key);
|
|
2045
|
+
}
|
|
2046
|
+
session = undefined;
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
else if (session?.warmReadyAtMs) {
|
|
2050
|
+
logCliWarmEvent('warm_reused', {
|
|
2051
|
+
conversationId: opts.conversationId,
|
|
2052
|
+
botId: opts.botId,
|
|
2053
|
+
provider: opts.provider,
|
|
2054
|
+
runtimeMode: session.warmRuntimeMode || 'local_desktop',
|
|
2055
|
+
topicId: session.topicId,
|
|
2056
|
+
reusedExistingSession: true,
|
|
2057
|
+
ageMs: Date.now() - session.warmReadyAtMs,
|
|
2058
|
+
});
|
|
2059
|
+
session.warmPromise = null;
|
|
2060
|
+
session.warmRequestedAtMs = null;
|
|
2061
|
+
session.warmReadyAtMs = null;
|
|
2062
|
+
}
|
|
1055
2063
|
if (!session) {
|
|
2064
|
+
if (opts.provider === 'claude-cli') {
|
|
2065
|
+
await ensureClaudeAuthReadyForFreshSession();
|
|
2066
|
+
}
|
|
2067
|
+
logCliWarmEvent('send_cold', {
|
|
2068
|
+
conversationId: opts.conversationId,
|
|
2069
|
+
botId: opts.botId,
|
|
2070
|
+
provider: opts.provider,
|
|
2071
|
+
});
|
|
1056
2072
|
try {
|
|
1057
|
-
session = this.createSession(key, opts.
|
|
2073
|
+
session = this.createSession(key, opts.botId, opts.conversationId, opts.provider, opts.botSettings, opts.cwd, requestedUseConpty, {
|
|
2074
|
+
actorId: opts.toolActorId,
|
|
2075
|
+
projectId: opts.toolProjectId,
|
|
2076
|
+
todoTaskId: opts.currentTodoTaskId,
|
|
2077
|
+
}, opts.resumeSessionId, opts.newSessionId);
|
|
1058
2078
|
}
|
|
1059
2079
|
catch (firstErr) {
|
|
1060
2080
|
console.warn(`[pty] Session creation failed, retrying in 1s: ${firstErr instanceof Error ? firstErr.message : String(firstErr)}`);
|
|
1061
2081
|
await delay(1000);
|
|
1062
|
-
session = this.createSession(key, opts.
|
|
2082
|
+
session = this.createSession(key, opts.botId, opts.conversationId, opts.provider, opts.botSettings, opts.cwd, requestedUseConpty, {
|
|
2083
|
+
actorId: opts.toolActorId,
|
|
2084
|
+
projectId: opts.toolProjectId,
|
|
2085
|
+
todoTaskId: opts.currentTodoTaskId,
|
|
2086
|
+
}, opts.resumeSessionId, opts.newSessionId);
|
|
1063
2087
|
}
|
|
1064
2088
|
this.sessions.set(key, session);
|
|
1065
2089
|
}
|
|
@@ -1068,9 +2092,107 @@ class LocalCliPtySessionManager {
|
|
|
1068
2092
|
session.chain = queued.then(() => undefined, () => undefined);
|
|
1069
2093
|
return queued;
|
|
1070
2094
|
}
|
|
2095
|
+
async runPassthroughCommand(opts) {
|
|
2096
|
+
const key = sessionKey(opts.conversationId, opts.botId);
|
|
2097
|
+
const isServiceMode = process.env.FUNOLIO_RUN_CONTEXT === 'windows-service'
|
|
2098
|
+
|| process.argv.includes('--mode') && (process.argv.includes('service') || process.argv.includes('windows-service'));
|
|
2099
|
+
const requestedUseConpty = opts.useConpty ?? !isServiceMode;
|
|
2100
|
+
let session = this.sessions.get(key);
|
|
2101
|
+
if (session
|
|
2102
|
+
&& (opts.forceFreshSession
|
|
2103
|
+
|| session.provider !== opts.provider
|
|
2104
|
+
|| session.cwd !== opts.cwd
|
|
2105
|
+
|| session.useConpty !== requestedUseConpty
|
|
2106
|
+
|| shouldRecycleClaudeSessionForFreshAuth(session)
|
|
2107
|
+
|| shouldResetClaudeSessionForAuthChange(session)
|
|
2108
|
+
|| session.closed)) {
|
|
2109
|
+
if (session.provider === 'claude-cli' && shouldRecycleClaudeSessionForFreshAuth(session)) {
|
|
2110
|
+
console.info(`[claude-auth] recycling passthrough session before resume (${opts.botId} ${opts.conversationId})`);
|
|
2111
|
+
}
|
|
2112
|
+
else if (session.provider === 'claude-cli' && shouldResetClaudeSessionForAuthChange(session)) {
|
|
2113
|
+
console.info(`[claude-auth] recycling passthrough session after auth change (${opts.botId})`);
|
|
2114
|
+
}
|
|
2115
|
+
this.closeSession(key);
|
|
2116
|
+
session = undefined;
|
|
2117
|
+
}
|
|
2118
|
+
if (!session) {
|
|
2119
|
+
if (opts.provider === 'claude-cli') {
|
|
2120
|
+
await ensureClaudeAuthReadyForFreshSession();
|
|
2121
|
+
}
|
|
2122
|
+
session = this.createSession(key, opts.botId, opts.conversationId, opts.provider, opts.botSettings, opts.cwd, requestedUseConpty, {
|
|
2123
|
+
actorId: opts.toolActorId,
|
|
2124
|
+
projectId: opts.toolProjectId,
|
|
2125
|
+
todoTaskId: opts.currentTodoTaskId,
|
|
2126
|
+
}, opts.resumeSessionId, opts.newSessionId);
|
|
2127
|
+
this.sessions.set(key, session);
|
|
2128
|
+
}
|
|
2129
|
+
const run = async () => this.runPassthroughInternal(session, opts);
|
|
2130
|
+
const queued = session.chain.then(run, run);
|
|
2131
|
+
session.chain = queued.then(() => undefined, () => undefined);
|
|
2132
|
+
return queued;
|
|
2133
|
+
}
|
|
2134
|
+
hasActiveSession(conversationId, botId) {
|
|
2135
|
+
const session = this.sessions.get(sessionKey(conversationId, botId));
|
|
2136
|
+
return !!session && !session.closed;
|
|
2137
|
+
}
|
|
1071
2138
|
closeSessionByConversation(conversationId, botId) {
|
|
1072
2139
|
this.closeSession(sessionKey(conversationId, botId));
|
|
1073
2140
|
}
|
|
2141
|
+
logSessionFailureByConversation(conversationId, botId, context, error) {
|
|
2142
|
+
const session = this.sessions.get(sessionKey(conversationId, botId));
|
|
2143
|
+
if (!session)
|
|
2144
|
+
return;
|
|
2145
|
+
const ptyBuffer = session.recentTerminalOutput || session.activeTurn?.rawOutput || '';
|
|
2146
|
+
const ptyPid = session.pty.pid;
|
|
2147
|
+
console.warn(`[local-cli-pty] ${context}`
|
|
2148
|
+
+ ` | sessionId=${session.sessionId || 'unknown'}`
|
|
2149
|
+
+ ` | botId=${botId}`
|
|
2150
|
+
+ ` | pid=${ptyPid || 'unknown'}`
|
|
2151
|
+
+ (error ? ` | error=${String(error?.message || error)}` : '')
|
|
2152
|
+
+ (ptyBuffer
|
|
2153
|
+
? `\n--- PTY buffer (last ${ptyBuffer.length} chars) ---\n${ptyBuffer}\n--- end PTY buffer ---`
|
|
2154
|
+
: ''));
|
|
2155
|
+
}
|
|
2156
|
+
closeSessionsByConversation(conversationId) {
|
|
2157
|
+
const prefix = `${conversationId}::`;
|
|
2158
|
+
let closed = 0;
|
|
2159
|
+
for (const key of [...this.sessions.keys()]) {
|
|
2160
|
+
if (key.startsWith(prefix)) {
|
|
2161
|
+
this.closeSession(key);
|
|
2162
|
+
closed++;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return closed;
|
|
2166
|
+
}
|
|
2167
|
+
closeWarmSessionsByConversation(conversationId) {
|
|
2168
|
+
const prefix = `${conversationId}::`;
|
|
2169
|
+
let closed = 0;
|
|
2170
|
+
for (const [key, session] of [...this.sessions.entries()]) {
|
|
2171
|
+
if (key.startsWith(prefix) && (session.warmPromise || session.warmReadyAtMs)) {
|
|
2172
|
+
this.closeSession(key);
|
|
2173
|
+
closed++;
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
return closed;
|
|
2177
|
+
}
|
|
2178
|
+
closeIdleClaudeSessions() {
|
|
2179
|
+
let closed = 0;
|
|
2180
|
+
for (const [key, session] of [...this.sessions.entries()]) {
|
|
2181
|
+
if (!isIdleClaudeSession(session))
|
|
2182
|
+
continue;
|
|
2183
|
+
this.closeSession(key);
|
|
2184
|
+
closed++;
|
|
2185
|
+
}
|
|
2186
|
+
return closed;
|
|
2187
|
+
}
|
|
2188
|
+
closeSessionsByBotId(botId) {
|
|
2189
|
+
const suffix = `::${botId}`;
|
|
2190
|
+
for (const key of [...this.sessions.keys()]) {
|
|
2191
|
+
if (key.endsWith(suffix)) {
|
|
2192
|
+
this.closeSession(key);
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
1074
2196
|
closeAll() {
|
|
1075
2197
|
for (const key of [...this.sessions.keys()]) {
|
|
1076
2198
|
this.closeSession(key);
|
|
@@ -1081,50 +2203,86 @@ class LocalCliPtySessionManager {
|
|
|
1081
2203
|
if (!session)
|
|
1082
2204
|
return;
|
|
1083
2205
|
session.closed = true;
|
|
1084
|
-
|
|
1085
|
-
|
|
2206
|
+
clearSessionWarmState(session);
|
|
2207
|
+
const ptyPid = session.pty.pid;
|
|
2208
|
+
let closeFailed = false;
|
|
2209
|
+
if (ptyPid) {
|
|
2210
|
+
const killResult = (0, managed_process_registry_1.killProcessTreeDetailed)(ptyPid);
|
|
2211
|
+
closeFailed = !killResult.killed || !!killResult.error;
|
|
1086
2212
|
}
|
|
1087
|
-
|
|
1088
|
-
|
|
2213
|
+
else {
|
|
2214
|
+
try {
|
|
2215
|
+
session.pty.kill();
|
|
2216
|
+
}
|
|
2217
|
+
catch {
|
|
2218
|
+
closeFailed = true;
|
|
2219
|
+
}
|
|
1089
2220
|
}
|
|
1090
2221
|
this.sessions.delete(key);
|
|
2222
|
+
(0, managed_process_registry_1.unregisterProcess)(key, false, closeFailed);
|
|
2223
|
+
}
|
|
2224
|
+
closeSessionByKey(key) {
|
|
2225
|
+
this.closeSession(key);
|
|
1091
2226
|
}
|
|
1092
|
-
createSession(key, provider, cwd, resumeSessionId, newSessionId) {
|
|
2227
|
+
createSession(key, botId, conversationId, provider, botSettings, cwd, useConpty, toolEnv, resumeSessionId, newSessionId) {
|
|
1093
2228
|
const ptyModule = loadNodePtyModule();
|
|
2229
|
+
const claudeSessionHome = provider === 'claude-cli'
|
|
2230
|
+
? (0, sync_cli_config_1.ensureCliSessionRuntimeHome)('claude-cli', botId, conversationId)
|
|
2231
|
+
: null;
|
|
2232
|
+
const sessionFilesRoot = getProviderSessionRoot(provider, claudeSessionHome || undefined);
|
|
1094
2233
|
const cleanEnv = { ...process.env };
|
|
1095
2234
|
for (const envKey of Object.keys(cleanEnv)) {
|
|
1096
2235
|
if (envKey === 'CLAUDECODE' || envKey.startsWith('CLAUDE_CODE_')) {
|
|
1097
2236
|
delete cleanEnv[envKey];
|
|
1098
2237
|
}
|
|
1099
2238
|
}
|
|
2239
|
+
if (claudeSessionHome) {
|
|
2240
|
+
cleanEnv.CLAUDE_CONFIG_DIR = claudeSessionHome;
|
|
2241
|
+
}
|
|
2242
|
+
if (toolEnv?.actorId?.trim())
|
|
2243
|
+
cleanEnv.FUNOLIO_TOOL_ACTOR_ID = toolEnv.actorId.trim();
|
|
2244
|
+
if (toolEnv?.projectId !== undefined && toolEnv.projectId !== null)
|
|
2245
|
+
cleanEnv.FUNOLIO_TOOL_PROJECT_ID = String(toolEnv.projectId);
|
|
2246
|
+
if (toolEnv?.todoTaskId !== undefined && toolEnv.todoTaskId !== null)
|
|
2247
|
+
cleanEnv.FUNOLIO_TOOL_TODO_ID = String(toolEnv.todoTaskId);
|
|
2248
|
+
const markerEnv = (0, managed_process_registry_1.getMarkerEnv)(key);
|
|
2249
|
+
for (const [mk, mv] of Object.entries(markerEnv)) {
|
|
2250
|
+
cleanEnv[mk] = mv;
|
|
2251
|
+
}
|
|
2252
|
+
if (provider === 'claude-cli') {
|
|
2253
|
+
(0, sync_cli_config_1.syncClaudeJsonConfig)({
|
|
2254
|
+
...(toolEnv?.actorId?.trim() ? { FUNOLIO_TOOL_ACTOR_ID: toolEnv.actorId.trim() } : {}),
|
|
2255
|
+
...(toolEnv?.projectId !== undefined && toolEnv.projectId !== null ? { FUNOLIO_TOOL_PROJECT_ID: String(toolEnv.projectId) } : {}),
|
|
2256
|
+
}, (0, sync_cli_config_1.claudeJsonConfigPath)(claudeSessionHome || undefined));
|
|
2257
|
+
}
|
|
2258
|
+
else {
|
|
2259
|
+
void (0, sync_cli_config_1.syncMcpToCliConfig)('codex-cli', {
|
|
2260
|
+
...(toolEnv?.actorId?.trim() ? { FUNOLIO_TOOL_ACTOR_ID: toolEnv.actorId.trim() } : {}),
|
|
2261
|
+
...(toolEnv?.projectId !== undefined && toolEnv.projectId !== null ? { FUNOLIO_TOOL_PROJECT_ID: String(toolEnv.projectId) } : {}),
|
|
2262
|
+
}).catch(() => { });
|
|
2263
|
+
}
|
|
1100
2264
|
// Snapshot existing session files BEFORE spawning the PTY.
|
|
1101
2265
|
// Only needed for Pattern B providers (no --session-id support).
|
|
1102
2266
|
// Pattern A providers know their file path from the generated ID.
|
|
1103
|
-
const preSpawnSnapshot = new Set(listSessionFiles(provider));
|
|
2267
|
+
const preSpawnSnapshot = new Set(listSessionFiles(provider, sessionFilesRoot));
|
|
1104
2268
|
let readyResolve = null;
|
|
1105
2269
|
const readyPromise = new Promise((resolve) => {
|
|
1106
2270
|
readyResolve = resolve;
|
|
1107
2271
|
});
|
|
1108
2272
|
// Build Claude CLI args with session control:
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
else if (newSessionId) {
|
|
1117
|
-
claudeArgs.push('--session-id', newSessionId);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
// Determine known session info for Pattern A
|
|
1121
|
-
const knownSessionId = resumeSessionId || newSessionId || null;
|
|
2273
|
+
// Claude: when launching a brand-new process, prefer a fresh Claude
|
|
2274
|
+
// session-id over reviving a dead Claude-side session with --resume.
|
|
2275
|
+
// Codex: no session flags on new, codex resume <id> for existing.
|
|
2276
|
+
const claudeLaunch = resolveClaudeLaunchSessionIds(resumeSessionId, newSessionId);
|
|
2277
|
+
const claudeArgs = buildClaudeCliArgs(claudeLaunch.resumeSessionId, claudeLaunch.newSessionId, botSettings?.claude);
|
|
2278
|
+
// Determine known session info for transcript discovery.
|
|
2279
|
+
const knownSessionId = claudeLaunch.knownSessionId;
|
|
1122
2280
|
let knownSessionFilePath = null;
|
|
1123
2281
|
if (knownSessionId && provider === 'claude-cli') {
|
|
1124
2282
|
// Search all project directories for the session file.
|
|
1125
2283
|
// Don't try to derive the directory name — Claude's naming convention
|
|
1126
2284
|
// (e.g., C--Projects-Funolio) doesn't match simple path normalization.
|
|
1127
|
-
const projectRoot =
|
|
2285
|
+
const projectRoot = sessionFilesRoot;
|
|
1128
2286
|
if (fs.existsSync(projectRoot)) {
|
|
1129
2287
|
try {
|
|
1130
2288
|
const projectDirs = fs.readdirSync(projectRoot);
|
|
@@ -1144,28 +2302,14 @@ class LocalCliPtySessionManager {
|
|
|
1144
2302
|
else if (knownSessionId && provider === 'codex-cli') {
|
|
1145
2303
|
knownSessionFilePath = findCodexSessionFileBySessionId(knownSessionId);
|
|
1146
2304
|
}
|
|
1147
|
-
const codexArgs = resumeSessionId
|
|
1148
|
-
? [
|
|
1149
|
-
'resume',
|
|
1150
|
-
resumeSessionId,
|
|
1151
|
-
'--no-alt-screen',
|
|
1152
|
-
'--dangerously-bypass-approvals-and-sandbox',
|
|
1153
|
-
'-c',
|
|
1154
|
-
'shell_environment_policy.inherit=all',
|
|
1155
|
-
]
|
|
1156
|
-
: [
|
|
1157
|
-
'--no-alt-screen',
|
|
1158
|
-
'--dangerously-bypass-approvals-and-sandbox',
|
|
1159
|
-
'-c',
|
|
1160
|
-
'shell_environment_policy.inherit=all',
|
|
1161
|
-
];
|
|
2305
|
+
const codexArgs = buildCodexCliArgs(resumeSessionId, botSettings?.codex);
|
|
1162
2306
|
const pty = provider === 'codex-cli'
|
|
1163
2307
|
? ptyModule.spawn(findExecutableOnPath('codex.cmd') || 'codex.cmd', codexArgs, {
|
|
1164
2308
|
cwd,
|
|
1165
2309
|
cols: 160,
|
|
1166
2310
|
rows: 48,
|
|
1167
2311
|
env: cleanEnv,
|
|
1168
|
-
useConpty
|
|
2312
|
+
useConpty,
|
|
1169
2313
|
name: 'xterm-color',
|
|
1170
2314
|
})
|
|
1171
2315
|
: ptyModule.spawn(findExecutableOnPath('claude.exe') || findExecutableOnPath('claude.cmd') || 'claude', claudeArgs, {
|
|
@@ -1173,35 +2317,66 @@ class LocalCliPtySessionManager {
|
|
|
1173
2317
|
cols: 160,
|
|
1174
2318
|
rows: 48,
|
|
1175
2319
|
env: cleanEnv,
|
|
1176
|
-
useConpty
|
|
2320
|
+
useConpty,
|
|
1177
2321
|
name: 'xterm-color',
|
|
1178
2322
|
});
|
|
1179
2323
|
const session = {
|
|
1180
2324
|
key,
|
|
2325
|
+
conversationId,
|
|
2326
|
+
botId,
|
|
2327
|
+
topicId: null,
|
|
2328
|
+
warmRuntimeMode: null,
|
|
1181
2329
|
provider,
|
|
1182
2330
|
cwd,
|
|
2331
|
+
useConpty,
|
|
1183
2332
|
pty,
|
|
1184
2333
|
createdAtMs: Date.now(),
|
|
1185
2334
|
lastUsedAtMs: Date.now(),
|
|
1186
2335
|
launchSnapshot: preSpawnSnapshot,
|
|
2336
|
+
sessionFilesRoot,
|
|
1187
2337
|
sessionId: knownSessionId,
|
|
1188
2338
|
sessionFilePath: knownSessionFilePath,
|
|
1189
2339
|
sessionFileOffset: 0,
|
|
1190
2340
|
sessionFileCarry: '',
|
|
1191
2341
|
readyPromise,
|
|
1192
2342
|
readyResolved: false,
|
|
1193
|
-
waitForNextSendMs:
|
|
2343
|
+
waitForNextSendMs: 100,
|
|
1194
2344
|
startupDelayMs: 1200,
|
|
1195
|
-
|
|
2345
|
+
startupDelayApplied: false,
|
|
2346
|
+
submitDelayMs: provider === 'codex-cli' ? 175 : 200,
|
|
1196
2347
|
currentPromptLocator: null,
|
|
1197
2348
|
currentPromptStartedAtMs: 0,
|
|
1198
2349
|
activeTurn: null,
|
|
2350
|
+
warmPromise: null,
|
|
2351
|
+
warmRequestedAtMs: null,
|
|
2352
|
+
warmReadyAtMs: null,
|
|
2353
|
+
recentTerminalOutput: '',
|
|
1199
2354
|
closed: false,
|
|
2355
|
+
exitReason: null,
|
|
1200
2356
|
chain: Promise.resolve(),
|
|
1201
2357
|
childFollowers: new Map(),
|
|
1202
2358
|
childSnapshot: new Set(),
|
|
2359
|
+
claudeAuthFingerprint: provider === 'claude-cli' ? currentClaudeAuthFingerprint() : null,
|
|
2360
|
+
runtimeHomeDir: claudeSessionHome,
|
|
1203
2361
|
};
|
|
2362
|
+
const ptyPid = pty.pid;
|
|
2363
|
+
if (ptyPid) {
|
|
2364
|
+
(0, managed_process_registry_1.registerProcess)({
|
|
2365
|
+
sessionKey: key,
|
|
2366
|
+
provider,
|
|
2367
|
+
conversationId,
|
|
2368
|
+
botId,
|
|
2369
|
+
pid: ptyPid,
|
|
2370
|
+
cwd,
|
|
2371
|
+
createdAt: new Date(session.createdAtMs).toISOString(),
|
|
2372
|
+
lastUsedAt: new Date(session.lastUsedAtMs).toISOString(),
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
1204
2375
|
pty.on('data', (chunk) => {
|
|
2376
|
+
if (chunk) {
|
|
2377
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'pty_data');
|
|
2378
|
+
session.recentTerminalOutput = appendRecentTerminalOutput(session.recentTerminalOutput, chunk);
|
|
2379
|
+
}
|
|
1205
2380
|
if (!session.readyResolved && chunk && chunk.trim()) {
|
|
1206
2381
|
session.readyResolved = true;
|
|
1207
2382
|
readyResolve?.();
|
|
@@ -1210,12 +2385,88 @@ class LocalCliPtySessionManager {
|
|
|
1210
2385
|
void emitPtyChunk(session, chunk);
|
|
1211
2386
|
}
|
|
1212
2387
|
});
|
|
1213
|
-
pty.on('exit', () => {
|
|
2388
|
+
pty.on('exit', (...exitArgs) => {
|
|
1214
2389
|
session.closed = true;
|
|
2390
|
+
session.exitReason = describePtyExit(exitArgs);
|
|
2391
|
+
console.warn(`[local-cli-pty] ${provider} PTY exited (${session.exitReason}) cwd=${cwd}`);
|
|
1215
2392
|
this.sessions.delete(key);
|
|
2393
|
+
(0, managed_process_registry_1.unregisterProcess)(key, false);
|
|
1216
2394
|
});
|
|
1217
2395
|
return session;
|
|
1218
2396
|
}
|
|
2397
|
+
async runPassthroughInternal(session, opts) {
|
|
2398
|
+
const abortSignal = opts.abortSignal;
|
|
2399
|
+
const abortHandler = () => {
|
|
2400
|
+
this.closeSession(session.key);
|
|
2401
|
+
};
|
|
2402
|
+
abortSignal?.addEventListener('abort', abortHandler, { once: true });
|
|
2403
|
+
try {
|
|
2404
|
+
session.lastUsedAtMs = Date.now();
|
|
2405
|
+
(0, managed_process_registry_1.updateLastUsed)(session.key);
|
|
2406
|
+
(0, managed_process_registry_1.markTurnStarted)(session.key, 'pty_passthrough');
|
|
2407
|
+
throwIfAborted(abortSignal);
|
|
2408
|
+
await waitForFirstTerminalData(session);
|
|
2409
|
+
throwIfAborted(abortSignal);
|
|
2410
|
+
if (session.closed) {
|
|
2411
|
+
throw formatClosedSessionError(session, `closed before ${opts.command} was sent`);
|
|
2412
|
+
}
|
|
2413
|
+
if (session.waitForNextSendMs > 0) {
|
|
2414
|
+
await delayWithAbort(session.waitForNextSendMs, abortSignal);
|
|
2415
|
+
}
|
|
2416
|
+
const timeoutMs = opts.timeoutMs ?? 30_000;
|
|
2417
|
+
const startedAtMs = Date.now();
|
|
2418
|
+
const activeTurn = {
|
|
2419
|
+
mode: 'passthrough',
|
|
2420
|
+
promptEchoRemainder: normalizeTerminalChunk(opts.command),
|
|
2421
|
+
rawOutput: '',
|
|
2422
|
+
visibleOutput: '',
|
|
2423
|
+
lastDataAtMs: startedAtMs,
|
|
2424
|
+
lastMeaningfulPtyDataAtMs: startedAtMs,
|
|
2425
|
+
callbackChain: Promise.resolve(),
|
|
2426
|
+
onRawChunk: opts.onRawChunk,
|
|
2427
|
+
recentChromeLines: [],
|
|
2428
|
+
assistantOutputDetected: false,
|
|
2429
|
+
sawVisibleData: false,
|
|
2430
|
+
};
|
|
2431
|
+
session.activeTurn = activeTurn;
|
|
2432
|
+
throwIfAborted(abortSignal);
|
|
2433
|
+
await writeNakedPrompt(session, opts.command);
|
|
2434
|
+
while (true) {
|
|
2435
|
+
throwIfAborted(abortSignal);
|
|
2436
|
+
const now = Date.now();
|
|
2437
|
+
const authFailure = getTerminalAuthFailure(session, activeTurn);
|
|
2438
|
+
if (authFailure) {
|
|
2439
|
+
if (session.provider === 'claude-cli')
|
|
2440
|
+
this.closeSession(session.key);
|
|
2441
|
+
throw authFailure;
|
|
2442
|
+
}
|
|
2443
|
+
if (!activeTurn.rawOutput) {
|
|
2444
|
+
if (now - startedAtMs > PASSTHROUGH_FIRST_BYTE_TIMEOUT_MS) {
|
|
2445
|
+
throw new Error(`${session.provider} PTY session returned no output for ${opts.command}`);
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
else if (now - activeTurn.lastDataAtMs >= PASSTHROUGH_TRAILING_IDLE_MS) {
|
|
2449
|
+
break;
|
|
2450
|
+
}
|
|
2451
|
+
if (now - startedAtMs > timeoutMs) {
|
|
2452
|
+
throw new Error(`${session.provider} PTY session timed out waiting for ${opts.command}`);
|
|
2453
|
+
}
|
|
2454
|
+
await delayWithAbort(50, abortSignal);
|
|
2455
|
+
}
|
|
2456
|
+
await activeTurn.callbackChain.catch(() => undefined);
|
|
2457
|
+
return {
|
|
2458
|
+
content: finalizePassthroughContent(opts.command, activeTurn.visibleOutput, activeTurn.rawOutput),
|
|
2459
|
+
sessionId: session.sessionId,
|
|
2460
|
+
rawOutput: activeTurn.rawOutput,
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
finally {
|
|
2464
|
+
syncClaudeSessionCredentialsBackToCanonical(session);
|
|
2465
|
+
session.activeTurn = null;
|
|
2466
|
+
(0, managed_process_registry_1.markTurnFinished)(session.key, 'pty_passthrough_finished');
|
|
2467
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
1219
2470
|
async runTurnInternal(session, opts) {
|
|
1220
2471
|
const abortSignal = opts.abortSignal;
|
|
1221
2472
|
const abortHandler = () => {
|
|
@@ -1224,27 +2475,33 @@ class LocalCliPtySessionManager {
|
|
|
1224
2475
|
abortSignal?.addEventListener('abort', abortHandler, { once: true });
|
|
1225
2476
|
try {
|
|
1226
2477
|
session.lastUsedAtMs = Date.now();
|
|
2478
|
+
(0, managed_process_registry_1.updateLastUsed)(session.key);
|
|
2479
|
+
(0, managed_process_registry_1.markTurnStarted)(session.key, 'pty_turn');
|
|
1227
2480
|
throwIfAborted(abortSignal);
|
|
1228
2481
|
await waitForFirstTerminalData(session);
|
|
1229
2482
|
throwIfAborted(abortSignal);
|
|
1230
2483
|
if (session.closed) {
|
|
1231
|
-
throw
|
|
2484
|
+
throw formatClosedSessionError(session, 'closed before prompt was sent');
|
|
1232
2485
|
}
|
|
1233
|
-
//
|
|
1234
|
-
//
|
|
1235
|
-
//
|
|
1236
|
-
|
|
2486
|
+
// Reset the read offset at the start of every turn so we only consume records
|
|
2487
|
+
// appended after this prompt. A shared Claude session file can be advanced by
|
|
2488
|
+
// another caller between turns, and replaying that backlog can finalize the
|
|
2489
|
+
// wrong assistant response before the current prompt lands.
|
|
2490
|
+
if (session.sessionFilePath) {
|
|
1237
2491
|
try {
|
|
1238
2492
|
const currentSize = fs.statSync(session.sessionFilePath).size;
|
|
1239
2493
|
session.sessionFileOffset = currentSize;
|
|
2494
|
+
session.sessionFileCarry = '';
|
|
1240
2495
|
}
|
|
1241
2496
|
catch {
|
|
1242
|
-
// File may not exist yet for new sessions — that's fine
|
|
2497
|
+
// File may not exist yet for new sessions — that's fine.
|
|
1243
2498
|
}
|
|
1244
2499
|
}
|
|
1245
2500
|
if (session.waitForNextSendMs > 0) {
|
|
1246
2501
|
await delayWithAbort(session.waitForNextSendMs, abortSignal);
|
|
1247
2502
|
}
|
|
2503
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
|
|
2504
|
+
const startedAtMs = Date.now();
|
|
1248
2505
|
const tracker = {
|
|
1249
2506
|
done: false,
|
|
1250
2507
|
sawExplicitCompletion: false,
|
|
@@ -1255,18 +2512,19 @@ class LocalCliPtySessionManager {
|
|
|
1255
2512
|
pendingToolUseIds: new Set(),
|
|
1256
2513
|
lastRecordAtMs: Date.now(),
|
|
1257
2514
|
sawCompletionSentinel: false,
|
|
2515
|
+
turnStartedAtMs: startedAtMs,
|
|
2516
|
+
expectedUserPromptSnippet: extractCurrentUserPromptSnippet(opts.messages),
|
|
2517
|
+
sawCurrentTurnUserRecord: false,
|
|
1258
2518
|
};
|
|
1259
|
-
const
|
|
1260
|
-
const
|
|
1261
|
-
const freshClaudeStartupDeadlineMs = opts.forceFreshSession && session.provider === 'claude-cli'
|
|
1262
|
-
? startedAtMs + CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT_MS
|
|
1263
|
-
: null;
|
|
1264
|
-
const promptText = buildTurnPrompt(opts.provider, opts.systemPrompt, opts.messages, opts.forceFreshSession || !session.sessionFilePath);
|
|
2519
|
+
const waitForFreshClaudeTranscript = opts.forceFreshSession && session.provider === 'claude-cli';
|
|
2520
|
+
const promptText = buildTurnPrompt(opts.provider, opts.systemPrompt, opts.messages, opts.forceFreshSession || !session.sessionFilePath, opts.cwd);
|
|
1265
2521
|
session.currentPromptLocator = promptText.trim();
|
|
1266
2522
|
session.currentPromptStartedAtMs = startedAtMs;
|
|
1267
2523
|
const activeTurn = {
|
|
2524
|
+
mode: 'default',
|
|
1268
2525
|
promptEchoRemainder: normalizeTerminalChunk(promptText),
|
|
1269
2526
|
rawOutput: '',
|
|
2527
|
+
visibleOutput: '',
|
|
1270
2528
|
lastDataAtMs: startedAtMs,
|
|
1271
2529
|
lastMeaningfulPtyDataAtMs: startedAtMs,
|
|
1272
2530
|
callbackChain: Promise.resolve(),
|
|
@@ -1274,6 +2532,7 @@ class LocalCliPtySessionManager {
|
|
|
1274
2532
|
onRawChunk: opts.onRawChunk,
|
|
1275
2533
|
recentChromeLines: [],
|
|
1276
2534
|
assistantOutputDetected: false,
|
|
2535
|
+
sawVisibleData: false,
|
|
1277
2536
|
};
|
|
1278
2537
|
session.activeTurn = activeTurn;
|
|
1279
2538
|
// Snapshot existing child subagent files before this turn
|
|
@@ -1296,6 +2555,7 @@ class LocalCliPtySessionManager {
|
|
|
1296
2555
|
}
|
|
1297
2556
|
throwIfAborted(abortSignal);
|
|
1298
2557
|
await writeInteractivePrompt(session, promptText);
|
|
2558
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'prompt_sent');
|
|
1299
2559
|
let lastHeartbeatAtMs = Date.now();
|
|
1300
2560
|
const HEARTBEAT_INTERVAL_MS = 120_000; // 2 minutes
|
|
1301
2561
|
while (!tracker.done) {
|
|
@@ -1303,6 +2563,14 @@ class LocalCliPtySessionManager {
|
|
|
1303
2563
|
if (Date.now() - startedAtMs > timeoutMs) {
|
|
1304
2564
|
throw new Error(`${opts.provider} PTY session timed out waiting for a response`);
|
|
1305
2565
|
}
|
|
2566
|
+
if (!activeTurn.assistantOutputDetected && !tracker.lastAssistantText.trim()) {
|
|
2567
|
+
const authFailure = getTerminalAuthFailure(session, activeTurn);
|
|
2568
|
+
if (authFailure) {
|
|
2569
|
+
if (session.provider === 'claude-cli')
|
|
2570
|
+
this.closeSession(session.key);
|
|
2571
|
+
throw authFailure;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
1306
2574
|
const inactivityFailTimeoutMs = getPtyInactivityFailTimeoutMs(session.provider);
|
|
1307
2575
|
if (inactivityFailTimeoutMs != null
|
|
1308
2576
|
&& Date.now() - activeTurn.lastMeaningfulPtyDataAtMs > inactivityFailTimeoutMs) {
|
|
@@ -1323,7 +2591,7 @@ class LocalCliPtySessionManager {
|
|
|
1323
2591
|
if (!session.sessionFilePath) {
|
|
1324
2592
|
// Pattern A (known session ID): search for our specific file by ID
|
|
1325
2593
|
if (session.sessionId && session.provider === 'claude-cli') {
|
|
1326
|
-
const projectRoot =
|
|
2594
|
+
const projectRoot = session.sessionFilesRoot;
|
|
1327
2595
|
if (fs.existsSync(projectRoot)) {
|
|
1328
2596
|
try {
|
|
1329
2597
|
for (const dir of fs.readdirSync(projectRoot)) {
|
|
@@ -1341,7 +2609,7 @@ class LocalCliPtySessionManager {
|
|
|
1341
2609
|
}
|
|
1342
2610
|
// Pattern B (unknown ID): discover by snapshot diff
|
|
1343
2611
|
if (!session.sessionFilePath) {
|
|
1344
|
-
const discovered = discoverSessionFile(session.provider, session.launchSnapshot, session.currentPromptStartedAtMs || session.createdAtMs, session.currentPromptLocator);
|
|
2612
|
+
const discovered = discoverSessionFile(session.provider, session.launchSnapshot, session.currentPromptStartedAtMs || session.createdAtMs, session.currentPromptLocator, session.sessionFilesRoot);
|
|
1345
2613
|
if (discovered) {
|
|
1346
2614
|
session.sessionFilePath = discovered;
|
|
1347
2615
|
session.sessionFileOffset = 0;
|
|
@@ -1352,11 +2620,33 @@ class LocalCliPtySessionManager {
|
|
|
1352
2620
|
if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
|
|
1353
2621
|
await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
|
|
1354
2622
|
}
|
|
1355
|
-
if (
|
|
1356
|
-
&& Date.now() >= freshClaudeStartupDeadlineMs
|
|
2623
|
+
if (waitForFreshClaudeTranscript
|
|
1357
2624
|
&& !(session.sessionFilePath && fs.existsSync(session.sessionFilePath))) {
|
|
1358
|
-
|
|
1359
|
-
|
|
2625
|
+
const now = Date.now();
|
|
2626
|
+
if (!shouldContinueWaitingForFreshClaudeSession(session, activeTurn, startedAtMs, now)) {
|
|
2627
|
+
const ptyBuffer = session.recentTerminalOutput || activeTurn.rawOutput || '';
|
|
2628
|
+
const ptyPid = session.pty.pid;
|
|
2629
|
+
if (ptyBuffer) {
|
|
2630
|
+
console.warn(`[local-cli-pty] Failed fresh session PTY output before kill`
|
|
2631
|
+
+ ` | sessionId=${session.sessionId || 'unknown'}`
|
|
2632
|
+
+ ` | botId=${session.botId}`
|
|
2633
|
+
+ ` | pid=${ptyPid || 'unknown'}`
|
|
2634
|
+
+ `\n--- PTY buffer (last ${ptyBuffer.length} chars) ---\n${ptyBuffer}\n--- end PTY buffer ---`);
|
|
2635
|
+
}
|
|
2636
|
+
else {
|
|
2637
|
+
console.warn(`[local-cli-pty] Failed fresh session with no PTY output`
|
|
2638
|
+
+ ` | sessionId=${session.sessionId || 'unknown'}`
|
|
2639
|
+
+ ` | botId=${session.botId}`
|
|
2640
|
+
+ ` | pid=${ptyPid || 'unknown'}`);
|
|
2641
|
+
}
|
|
2642
|
+
const authFailure = getTerminalAuthFailure(session, activeTurn);
|
|
2643
|
+
if (authFailure) {
|
|
2644
|
+
this.closeSession(session.key);
|
|
2645
|
+
throw authFailure;
|
|
2646
|
+
}
|
|
2647
|
+
this.closeSession(session.key);
|
|
2648
|
+
throw buildClaudeFreshSessionStartupError(session.sessionId, now - startedAtMs);
|
|
2649
|
+
}
|
|
1360
2650
|
}
|
|
1361
2651
|
if (session._pendingTitle) {
|
|
1362
2652
|
session._pendingTitle = null;
|
|
@@ -1366,21 +2656,29 @@ class LocalCliPtySessionManager {
|
|
|
1366
2656
|
await this.consumeChildSubagentFiles(session, tracker, opts.onDetail);
|
|
1367
2657
|
}
|
|
1368
2658
|
if (session.closed) {
|
|
2659
|
+
if (!activeTurn.assistantOutputDetected && !tracker.lastAssistantText.trim()) {
|
|
2660
|
+
const authFailure = getTerminalAuthFailure(session, activeTurn);
|
|
2661
|
+
if (authFailure) {
|
|
2662
|
+
if (session.provider === 'claude-cli')
|
|
2663
|
+
this.closeSession(session.key);
|
|
2664
|
+
throw authFailure;
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
1369
2667
|
if (canFinalizeClaudeTurnOnSessionExit(session, tracker)) {
|
|
1370
2668
|
tracker.done = true;
|
|
1371
2669
|
tracker.finalContent = tracker.finalContent || tracker.lastAssistantText;
|
|
1372
2670
|
break;
|
|
1373
2671
|
}
|
|
1374
|
-
throw
|
|
2672
|
+
throw formatClosedSessionError(session, 'exited while waiting for a response');
|
|
1375
2673
|
}
|
|
1376
2674
|
if (!tracker.done) {
|
|
1377
|
-
await delayWithAbort(
|
|
2675
|
+
await delayWithAbort(200, abortSignal);
|
|
1378
2676
|
}
|
|
1379
2677
|
}
|
|
1380
2678
|
const settleStartedAt = Date.now();
|
|
1381
|
-
while (Date.now() - Math.max(activeTurn.lastDataAtMs, settleStartedAt) <
|
|
2679
|
+
while (Date.now() - Math.max(activeTurn.lastDataAtMs, settleStartedAt) < 100) {
|
|
1382
2680
|
throwIfAborted(abortSignal);
|
|
1383
|
-
if (Date.now() - settleStartedAt >
|
|
2681
|
+
if (Date.now() - settleStartedAt > 500)
|
|
1384
2682
|
break;
|
|
1385
2683
|
await delayWithAbort(50, abortSignal);
|
|
1386
2684
|
}
|
|
@@ -1389,7 +2687,7 @@ class LocalCliPtySessionManager {
|
|
|
1389
2687
|
}
|
|
1390
2688
|
await activeTurn.callbackChain;
|
|
1391
2689
|
session.lastUsedAtMs = Date.now();
|
|
1392
|
-
session.waitForNextSendMs =
|
|
2690
|
+
session.waitForNextSendMs = 150;
|
|
1393
2691
|
return {
|
|
1394
2692
|
content: (tracker.finalContent || tracker.lastAssistantText).trim(),
|
|
1395
2693
|
sessionId: session.sessionId,
|
|
@@ -1398,7 +2696,9 @@ class LocalCliPtySessionManager {
|
|
|
1398
2696
|
};
|
|
1399
2697
|
}
|
|
1400
2698
|
finally {
|
|
2699
|
+
syncClaudeSessionCredentialsBackToCanonical(session);
|
|
1401
2700
|
session.activeTurn = null;
|
|
2701
|
+
(0, managed_process_registry_1.markTurnFinished)(session.key, 'pty_turn_finished');
|
|
1402
2702
|
abortSignal?.removeEventListener('abort', abortHandler);
|
|
1403
2703
|
}
|
|
1404
2704
|
}
|
|
@@ -1414,6 +2714,7 @@ class LocalCliPtySessionManager {
|
|
|
1414
2714
|
}
|
|
1415
2715
|
if (stat.size <= session.sessionFileOffset)
|
|
1416
2716
|
return;
|
|
2717
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'transcript_data');
|
|
1417
2718
|
let fh = null;
|
|
1418
2719
|
try {
|
|
1419
2720
|
fh = await fs.promises.open(session.sessionFilePath, 'r');
|
|
@@ -1544,6 +2845,9 @@ class LocalCliPtySessionManager {
|
|
|
1544
2845
|
}
|
|
1545
2846
|
}
|
|
1546
2847
|
async applyRecord(session, tracker, record, onChunk, onDetail) {
|
|
2848
|
+
if (session.provider === 'claude-cli' && shouldIgnoreClaudeRecordForCurrentTurn(tracker, record)) {
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
1547
2851
|
const parsed = session.provider === 'claude-cli'
|
|
1548
2852
|
? parseClaudeSessionRecord(record)
|
|
1549
2853
|
: parseCodexSessionRecord(record);
|