funolio-agent 1.0.53 → 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/approval.d.ts +1 -6
- package/dist/approval.d.ts.map +1 -1
- package/dist/approval.js +2 -7
- package/dist/approval.js.map +1 -1
- 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 +5 -1
- package/dist/bot-manager.d.ts.map +1 -1
- package/dist/bot-manager.js +46 -27
- 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/cli-session-registry.d.ts +35 -0
- package/dist/cli-session-registry.d.ts.map +1 -0
- package/dist/cli-session-registry.js +177 -0
- package/dist/cli-session-registry.js.map +1 -0
- package/dist/cli.js +62 -0
- package/dist/cli.js.map +1 -1
- package/dist/codex-app-server-manager.d.ts +189 -0
- package/dist/codex-app-server-manager.d.ts.map +1 -0
- package/dist/codex-app-server-manager.js +1468 -0
- package/dist/codex-app-server-manager.js.map +1 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +8 -30
- package/dist/commands/init.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/setup.d.ts +4 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +9 -25
- package/dist/commands/setup.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 +559 -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/completion-marker.d.ts +7 -0
- package/dist/completion-marker.d.ts.map +1 -0
- package/dist/completion-marker.js +28 -0
- package/dist/completion-marker.js.map +1 -0
- package/dist/config.d.ts +7 -2
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +184 -60
- 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 +210 -17
- package/dist/context-window.js.map +1 -1
- package/dist/live-activity.d.ts +31 -0
- package/dist/live-activity.d.ts.map +1 -0
- package/dist/live-activity.js +36 -0
- package/dist/live-activity.js.map +1 -0
- 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 +186 -0
- package/dist/local-cli-pty-manager.d.ts.map +1 -1
- package/dist/local-cli-pty-manager.js +2581 -164
- 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 +276 -5
- package/dist/local-data.d.ts.map +1 -1
- package/dist/local-data.js +1201 -86
- 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 +428 -2
- 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 +55 -0
- package/dist/local-server.d.ts.map +1 -1
- package/dist/local-server.js +3281 -441
- 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 +6 -0
- package/dist/message-loop.d.ts.map +1 -1
- package/dist/message-loop.js +281 -89
- package/dist/message-loop.js.map +1 -1
- package/dist/mqtt-client.d.ts +44 -1
- package/dist/mqtt-client.d.ts.map +1 -1
- package/dist/mqtt-client.js +284 -46
- 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/oauth.d.ts.map +1 -1
- package/dist/oauth.js +69 -29
- package/dist/oauth.js.map +1 -1
- 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 +15 -0
- package/dist/orchestration/orchestrator-operating-prompt.d.ts.map +1 -1
- package/dist/orchestration/orchestrator-operating-prompt.js +206 -20
- 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/validation.d.ts +40 -0
- package/dist/orchestration/validation.d.ts.map +1 -0
- package/dist/orchestration/validation.js +203 -0
- package/dist/orchestration/validation.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 +214 -33
- package/dist/orchestrator.d.ts.map +1 -1
- package/dist/orchestrator.js +2200 -1100
- 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-prompt.d.ts.map +1 -1
- package/dist/providers/claude-cli-prompt.js +49 -5
- package/dist/providers/claude-cli-prompt.js.map +1 -1
- package/dist/providers/claude-cli.d.ts.map +1 -1
- package/dist/providers/claude-cli.js +81 -5
- 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 +204 -26
- 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/response-guard.js +1 -1
- package/dist/response-guard.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 +20 -5
- package/dist/tools/admin-tools.js.map +1 -1
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +2 -1
- package/dist/tools/index.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 +16 -0
- package/dist/tools/search-conversation-history.d.ts.map +1 -0
- package/dist/tools/search-conversation-history.js +334 -0
- package/dist/tools/search-conversation-history.js.map +1 -0
- 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 +20 -0
- package/dist/wizard-state.d.ts.map +1 -1
- package/dist/wizard-state.js +90 -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 +44 -2
- package/dist/workflow-engine.d.ts.map +1 -1
- package/dist/workflow-engine.js +932 -111
- package/dist/workflow-engine.js.map +1 -1
- package/package.json +2 -2
|
@@ -34,18 +34,344 @@ 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;
|
|
45
|
+
exports.stripAnsi = stripAnsi;
|
|
46
|
+
exports.resolveConptyOverwrites = resolveConptyOverwrites;
|
|
47
|
+
exports.getProviderSessionRootForTest = getProviderSessionRootForTest;
|
|
48
|
+
exports.emitAssistantChunkSequenceForTest = emitAssistantChunkSequenceForTest;
|
|
37
49
|
exports.parseClaudeSessionRecord = parseClaudeSessionRecord;
|
|
38
50
|
exports.parseCodexSessionRecord = parseCodexSessionRecord;
|
|
51
|
+
exports.trimPassthroughSlashChromeForTest = trimPassthroughSlashChromeForTest;
|
|
52
|
+
exports.finalizePassthroughContentForTest = finalizePassthroughContentForTest;
|
|
39
53
|
exports.getLocalCliPtySessionManager = getLocalCliPtySessionManager;
|
|
54
|
+
exports.runLocalCliPtyHealthCheck = runLocalCliPtyHealthCheck;
|
|
55
|
+
exports.runLocalCliPtyTurnHealthCheck = runLocalCliPtyTurnHealthCheck;
|
|
56
|
+
exports.runLocalCliPtyProbe = runLocalCliPtyProbe;
|
|
40
57
|
const fs = __importStar(require("fs"));
|
|
41
58
|
const os = __importStar(require("os"));
|
|
42
59
|
const path = __importStar(require("path"));
|
|
60
|
+
const module_1 = require("module");
|
|
43
61
|
const claude_cli_prompt_1 = require("./providers/claude-cli-prompt");
|
|
62
|
+
const live_activity_1 = require("./live-activity");
|
|
63
|
+
const completion_marker_1 = require("./completion-marker");
|
|
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;
|
|
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;
|
|
75
|
+
function getPtyInactivityFailTimeoutMs(provider) {
|
|
76
|
+
if (provider === 'claude-cli')
|
|
77
|
+
return CLAUDE_PTY_INACTIVITY_FAIL_TIMEOUT_MS;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
44
80
|
let _ptyModule = null;
|
|
45
81
|
let _manager = null;
|
|
46
82
|
function delay(ms) {
|
|
47
83
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
48
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
|
+
}
|
|
159
|
+
function buildAbortError() {
|
|
160
|
+
const abortErr = new Error('PTY turn aborted');
|
|
161
|
+
abortErr.name = 'AbortError';
|
|
162
|
+
return abortErr;
|
|
163
|
+
}
|
|
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`
|
|
226
|
+
+ (sessionId ? ` (${sessionId})` : ''));
|
|
227
|
+
err.name = 'ClaudeFreshSessionStartupTimeoutError';
|
|
228
|
+
err.code = 'CLAUDE_FRESH_SESSION_STARTUP_TIMEOUT';
|
|
229
|
+
return err;
|
|
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
|
+
}
|
|
354
|
+
function throwIfAborted(signal) {
|
|
355
|
+
if (!signal?.aborted)
|
|
356
|
+
return;
|
|
357
|
+
throw buildAbortError();
|
|
358
|
+
}
|
|
359
|
+
async function delayWithAbort(ms, signal) {
|
|
360
|
+
if (!signal) {
|
|
361
|
+
await delay(ms);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
await Promise.race([
|
|
365
|
+
delay(ms),
|
|
366
|
+
new Promise((_, reject) => {
|
|
367
|
+
const onAbort = () => {
|
|
368
|
+
signal.removeEventListener('abort', onAbort);
|
|
369
|
+
reject(buildAbortError());
|
|
370
|
+
};
|
|
371
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
372
|
+
}),
|
|
373
|
+
]);
|
|
374
|
+
}
|
|
49
375
|
function sessionKey(conversationId, botId) {
|
|
50
376
|
return `${conversationId}::${botId}`;
|
|
51
377
|
}
|
|
@@ -95,16 +421,99 @@ function loadNodePtyModule() {
|
|
|
95
421
|
for (const candidate of candidates) {
|
|
96
422
|
if (!fs.existsSync(candidate))
|
|
97
423
|
continue;
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
}
|
|
100
436
|
}
|
|
101
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)}`);
|
|
102
438
|
}
|
|
103
439
|
}
|
|
440
|
+
function parseBracketedSections(text) {
|
|
441
|
+
const lines = text.split('\n');
|
|
442
|
+
const sections = [];
|
|
443
|
+
let currentHeading = null;
|
|
444
|
+
let bodyLines = [];
|
|
445
|
+
const flush = () => {
|
|
446
|
+
if (!currentHeading)
|
|
447
|
+
return;
|
|
448
|
+
sections.push({
|
|
449
|
+
heading: currentHeading,
|
|
450
|
+
body: bodyLines.join('\n').trim(),
|
|
451
|
+
});
|
|
452
|
+
};
|
|
453
|
+
for (const line of lines) {
|
|
454
|
+
const trimmed = line.trim();
|
|
455
|
+
const match = /^\[(.+?)\]$/.exec(trimmed);
|
|
456
|
+
if (match) {
|
|
457
|
+
flush();
|
|
458
|
+
currentHeading = match[1];
|
|
459
|
+
bodyLines = [];
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
if (currentHeading) {
|
|
463
|
+
bodyLines.push(line);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
flush();
|
|
467
|
+
return sections;
|
|
468
|
+
}
|
|
469
|
+
function trimSectionBody(body, maxChars) {
|
|
470
|
+
const trimmed = body.trim();
|
|
471
|
+
if (trimmed.length <= maxChars)
|
|
472
|
+
return trimmed;
|
|
473
|
+
return `${trimmed.slice(0, Math.max(0, maxChars - 32)).trim()}\n[Context trimmed for direct Codex PTY]`;
|
|
474
|
+
}
|
|
475
|
+
function compactCodexDirectSystemPrompt(systemPrompt) {
|
|
476
|
+
const trimmed = systemPrompt.trim();
|
|
477
|
+
if (!trimmed)
|
|
478
|
+
return '';
|
|
479
|
+
const sections = parseBracketedSections(trimmed);
|
|
480
|
+
if (sections.length === 0) {
|
|
481
|
+
return trimmed.length <= 900
|
|
482
|
+
? trimmed
|
|
483
|
+
: `${trimmed.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
|
|
484
|
+
}
|
|
485
|
+
const priorities = [
|
|
486
|
+
{ heading: 'Bot Identity', maxChars: 360 },
|
|
487
|
+
{ heading: 'Project Overview', maxChars: 140 },
|
|
488
|
+
{ heading: 'Recent Messages (Last 2 Turns)', maxChars: 120 },
|
|
489
|
+
{ heading: 'Recent Messages (Last 3 Turns)', maxChars: 140 },
|
|
490
|
+
{ heading: 'Recent Messages (Last 4 Turns)', maxChars: 160 },
|
|
491
|
+
{ heading: 'Recent Messages (Last 5 Turns)', maxChars: 180 },
|
|
492
|
+
];
|
|
493
|
+
const selected = [];
|
|
494
|
+
for (const priority of priorities) {
|
|
495
|
+
const section = sections.find((candidate) => candidate.heading === priority.heading);
|
|
496
|
+
if (!section)
|
|
497
|
+
continue;
|
|
498
|
+
const body = trimSectionBody(section.body, priority.maxChars);
|
|
499
|
+
if (!body)
|
|
500
|
+
continue;
|
|
501
|
+
selected.push(`[${section.heading}]\n${body}`);
|
|
502
|
+
}
|
|
503
|
+
const compacted = selected.join('\n\n').trim();
|
|
504
|
+
if (!compacted) {
|
|
505
|
+
return trimmed.length <= 900
|
|
506
|
+
? trimmed
|
|
507
|
+
: `${trimmed.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
|
|
508
|
+
}
|
|
509
|
+
return compacted.length <= 900
|
|
510
|
+
? compacted
|
|
511
|
+
: `${compacted.slice(0, 820).trim()}\n\n[Context trimmed for direct Codex PTY]`;
|
|
512
|
+
}
|
|
104
513
|
function buildCodexSeedPrompt(systemPrompt, messages) {
|
|
105
514
|
let fullPrompt = '';
|
|
106
515
|
if (systemPrompt) {
|
|
107
|
-
fullPrompt += `[System Instructions]\n${systemPrompt}\n\n`;
|
|
516
|
+
fullPrompt += `[System Instructions]\n${compactCodexDirectSystemPrompt(systemPrompt)}\n\n`;
|
|
108
517
|
}
|
|
109
518
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
110
519
|
if (messages.length > 0) {
|
|
@@ -123,43 +532,511 @@ function buildCodexSeedPrompt(systemPrompt, messages) {
|
|
|
123
532
|
}
|
|
124
533
|
if (lastMessage?.role === 'user') {
|
|
125
534
|
const prompt = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
126
|
-
fullPrompt += `[User Request]\n${prompt}`;
|
|
535
|
+
fullPrompt += `[User Request]\n${prompt}\n\n${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
|
|
127
536
|
}
|
|
128
537
|
else if (lastMessage) {
|
|
129
538
|
const content = typeof lastMessage.content === 'string' ? lastMessage.content : JSON.stringify(lastMessage.content);
|
|
130
|
-
fullPrompt += `[Latest Message]\n${content}\n\nContinue from the transcript above and respond appropriately
|
|
539
|
+
fullPrompt += `[Latest Message]\n${content}\n\nContinue from the transcript above and respond appropriately. ${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
|
|
131
540
|
}
|
|
132
541
|
else {
|
|
133
|
-
fullPrompt +=
|
|
542
|
+
fullPrompt += `[User Request]\n\n${completion_marker_1.CLI_COMPLETION_INSTRUCTION}`;
|
|
134
543
|
}
|
|
135
544
|
return fullPrompt;
|
|
136
545
|
}
|
|
137
|
-
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 = '.') {
|
|
138
611
|
if (!freshSession) {
|
|
139
612
|
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
140
613
|
if (lastMessage?.role === 'user') {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
: JSON.stringify(lastMessage.content);
|
|
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}`;
|
|
144
616
|
}
|
|
145
617
|
}
|
|
618
|
+
// For fresh sessions, flatten images before passing to prompt builders
|
|
619
|
+
const { messages: flatMessages, imageNote } = flattenMessagesForImages(provider, messages);
|
|
146
620
|
if (provider === 'claude-cli') {
|
|
147
|
-
|
|
621
|
+
const prompt = (0, claude_cli_prompt_1.buildClaudeCliStylePrompt)({
|
|
148
622
|
system: systemPrompt,
|
|
149
|
-
messages,
|
|
623
|
+
messages: flatMessages,
|
|
150
624
|
runtimeMode: 'local_desktop',
|
|
151
625
|
});
|
|
626
|
+
return imageNote ? `${prompt}${imageNote}` : prompt;
|
|
627
|
+
}
|
|
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
|
+
};
|
|
721
|
+
}
|
|
722
|
+
function stripAnsi(text) {
|
|
723
|
+
if (!text)
|
|
724
|
+
return '';
|
|
725
|
+
return text
|
|
726
|
+
.replace(/\x00/g, '')
|
|
727
|
+
.replace(/\x1B\][^\x07]*(?:\x07|\x1B\\)/g, '')
|
|
728
|
+
.replace(/\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])/g, '');
|
|
729
|
+
}
|
|
730
|
+
function resolveConptyOverwrites(text) {
|
|
731
|
+
if (!text)
|
|
732
|
+
return '';
|
|
733
|
+
const normalized = text.replace(/\r\n/g, '\n');
|
|
734
|
+
const output = [];
|
|
735
|
+
let line = '';
|
|
736
|
+
for (const ch of normalized) {
|
|
737
|
+
if (ch === '\r') {
|
|
738
|
+
line = '';
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (ch === '\b') {
|
|
742
|
+
line = line.slice(0, -1);
|
|
743
|
+
continue;
|
|
744
|
+
}
|
|
745
|
+
if (ch === '\n') {
|
|
746
|
+
output.push(line);
|
|
747
|
+
line = '';
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
line += ch;
|
|
751
|
+
}
|
|
752
|
+
output.push(line);
|
|
753
|
+
return output.join('\n');
|
|
754
|
+
}
|
|
755
|
+
function normalizeTerminalChunk(text) {
|
|
756
|
+
return resolveConptyOverwrites(stripAnsi(text))
|
|
757
|
+
.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
|
758
|
+
.replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓▉▊▋▌▍▎▏▐█▓▒░■□▪▫◼◻◾◽]+/g, ' ')
|
|
759
|
+
.replace(/\u00a0/g, ' ')
|
|
760
|
+
.replace(/[^\S\n]+/g, ' ');
|
|
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
|
+
}
|
|
793
|
+
function isSyntheticPromptEchoLine(line) {
|
|
794
|
+
const trimmed = line.trim();
|
|
795
|
+
if (!trimmed)
|
|
796
|
+
return false;
|
|
797
|
+
return (/^\[(System Instructions|Recent Transcript|User Request|Latest Message|Bot Identity|Project Overview|Workflow State|Response Style|Input from Previous Step|Context trimmed.*)\]$/i.test(trimmed)
|
|
798
|
+
|| /^Current user request:$/i.test(trimmed)
|
|
799
|
+
|| /^Recent transcript \(for continuity\):$/i.test(trimmed)
|
|
800
|
+
|| /^(Bot Name|Role Label|Project|Workspace):/i.test(trimmed)
|
|
801
|
+
|| /^Responsibilities:?$/i.test(trimmed)
|
|
802
|
+
|| /^(USER|ASSISTANT|TOOL(?:\s+\(.+\))?):$/i.test(trimmed));
|
|
803
|
+
}
|
|
804
|
+
function isAutomationNoiseLine(provider, line) {
|
|
805
|
+
const trimmed = line.trim();
|
|
806
|
+
const normalizedLead = trimmed.replace(/^[>›â»âµâ–¸â–¶]+\s*/, '');
|
|
807
|
+
if (!trimmed)
|
|
808
|
+
return false;
|
|
809
|
+
if (isCliBootstrapChromeLine(provider, trimmed))
|
|
810
|
+
return true;
|
|
811
|
+
if (/^Pasting text/i.test(trimmed))
|
|
812
|
+
return true;
|
|
813
|
+
if (/^\[Pasted.*lines?\]/i.test(trimmed))
|
|
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;
|
|
819
|
+
if (/^ctrl\+g to edit in notepad$/i.test(trimmed))
|
|
820
|
+
return true;
|
|
821
|
+
if (/^\? for shortcuts$/i.test(trimmed))
|
|
822
|
+
return true;
|
|
823
|
+
if (/^Use \/skills to list available skills$/i.test(trimmed))
|
|
824
|
+
return true;
|
|
825
|
+
if (/^Improve documentation in @filename$/i.test(trimmed))
|
|
826
|
+
return true;
|
|
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))
|
|
836
|
+
return true;
|
|
837
|
+
if (/^[-_=]{4,}$/.test(trimmed))
|
|
838
|
+
return true;
|
|
839
|
+
if (/^[•◦·*✢✶✻✽●]+$/.test(trimmed))
|
|
840
|
+
return true;
|
|
841
|
+
if (/^(Wo|or|rk|ki|in|ng|Wng|Wog)$/.test(trimmed))
|
|
842
|
+
return true;
|
|
843
|
+
if (trimmed.length <= 3 && /^[A-Za-z0-9]+$/.test(trimmed))
|
|
844
|
+
return true;
|
|
845
|
+
if (provider === 'claude-cli' && /^>\s*$/.test(trimmed))
|
|
846
|
+
return true;
|
|
847
|
+
if (provider === 'codex-cli' && /^›\s*$/.test(trimmed))
|
|
848
|
+
return true;
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
function isRepeatedChromeLine(line) {
|
|
852
|
+
const trimmed = line.trim();
|
|
853
|
+
if (!trimmed)
|
|
854
|
+
return false;
|
|
855
|
+
return (/(?:gpt-\d|claude|% left|esc to interrupt|for shortcuts|Use \/skills|Quantumizing|Working)/i.test(trimmed)
|
|
856
|
+
|| /^› /.test(trimmed)
|
|
857
|
+
|| /^> /.test(trimmed));
|
|
858
|
+
}
|
|
859
|
+
function isLikelyAssistantAnswerLine(line) {
|
|
860
|
+
const trimmed = line.trim();
|
|
861
|
+
if (!trimmed)
|
|
862
|
+
return false;
|
|
863
|
+
if (/^(?:>|›|\$|\/)/.test(trimmed))
|
|
864
|
+
return false;
|
|
865
|
+
if (/^(Running|Working|Thinking|Brewing|Analyzing|Searching|Reading|Preparing|Loading|Using|Tool|Status|Task|Step)\b/i.test(trimmed))
|
|
866
|
+
return false;
|
|
867
|
+
if (/^[-*]\s+/.test(trimmed) && trimmed.length >= 48)
|
|
868
|
+
return true;
|
|
869
|
+
if (/^\d+\.\s+/.test(trimmed) && trimmed.length >= 48)
|
|
870
|
+
return true;
|
|
871
|
+
const words = trimmed.split(/\s+/).filter(Boolean);
|
|
872
|
+
if (words.length >= 8 && /[a-z]/.test(trimmed) && /[.:!?]$/.test(trimmed))
|
|
873
|
+
return true;
|
|
874
|
+
if (trimmed.length >= 72 && /[a-z]/.test(trimmed))
|
|
875
|
+
return true;
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
function extractTerminalFacingText(text, assistantOutputDetected) {
|
|
879
|
+
if (!text || assistantOutputDetected) {
|
|
880
|
+
return { terminalText: '', assistantOutputDetected };
|
|
881
|
+
}
|
|
882
|
+
const terminalLines = [];
|
|
883
|
+
let detected = assistantOutputDetected;
|
|
884
|
+
for (const rawLine of text.split('\n')) {
|
|
885
|
+
const trimmed = rawLine.trim();
|
|
886
|
+
if (!trimmed) {
|
|
887
|
+
if (terminalLines.length > 0 && terminalLines[terminalLines.length - 1] !== '') {
|
|
888
|
+
terminalLines.push('');
|
|
889
|
+
}
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (isLikelyAssistantAnswerLine(trimmed)) {
|
|
893
|
+
detected = true;
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
terminalLines.push(rawLine);
|
|
897
|
+
}
|
|
898
|
+
return {
|
|
899
|
+
terminalText: terminalLines.join('\n').replace(/\n{3,}/g, '\n\n').trim(),
|
|
900
|
+
assistantOutputDetected: detected,
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
function sanitizeVisibleChunk(provider, text, recentChromeLines) {
|
|
904
|
+
const sanitizedLines = [];
|
|
905
|
+
for (const rawLine of text.split('\n')) {
|
|
906
|
+
const trimmedRight = rawLine.replace(/\s+$/g, '');
|
|
907
|
+
const trimmed = trimmedRight.trim();
|
|
908
|
+
if (!trimmed) {
|
|
909
|
+
if (sanitizedLines.length > 0 && sanitizedLines[sanitizedLines.length - 1] !== '') {
|
|
910
|
+
sanitizedLines.push('');
|
|
911
|
+
}
|
|
912
|
+
continue;
|
|
913
|
+
}
|
|
914
|
+
if (isSyntheticPromptEchoLine(trimmedRight))
|
|
915
|
+
continue;
|
|
916
|
+
if (isAutomationNoiseLine(provider, trimmedRight))
|
|
917
|
+
continue;
|
|
918
|
+
if (isRepeatedChromeLine(trimmedRight) && recentChromeLines.includes(trimmed))
|
|
919
|
+
continue;
|
|
920
|
+
sanitizedLines.push(trimmedRight);
|
|
921
|
+
if (isRepeatedChromeLine(trimmedRight)) {
|
|
922
|
+
recentChromeLines.push(trimmed);
|
|
923
|
+
if (recentChromeLines.length > 12) {
|
|
924
|
+
recentChromeLines.splice(0, recentChromeLines.length - 12);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
return sanitizedLines.join('\n').replace(/\n{3,}/g, '\n\n');
|
|
929
|
+
}
|
|
930
|
+
function trimPromptEcho(text, promptEchoRemainder) {
|
|
931
|
+
if (!text || !promptEchoRemainder) {
|
|
932
|
+
return { text, promptEchoRemainder };
|
|
933
|
+
}
|
|
934
|
+
let output = '';
|
|
935
|
+
let remainingPrompt = promptEchoRemainder;
|
|
936
|
+
let index = 0;
|
|
937
|
+
const maxIterations = (text.length + promptEchoRemainder.length) * 2;
|
|
938
|
+
let iterations = 0;
|
|
939
|
+
while (index < text.length) {
|
|
940
|
+
if (++iterations > maxIterations) {
|
|
941
|
+
output += text.slice(index);
|
|
942
|
+
break;
|
|
943
|
+
}
|
|
944
|
+
if (!remainingPrompt) {
|
|
945
|
+
output += text.slice(index);
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
const current = text.slice(index);
|
|
949
|
+
if (remainingPrompt.startsWith(current)) {
|
|
950
|
+
return { text: output, promptEchoRemainder: remainingPrompt.slice(current.length) };
|
|
951
|
+
}
|
|
952
|
+
if (current.startsWith(remainingPrompt)) {
|
|
953
|
+
index += remainingPrompt.length;
|
|
954
|
+
remainingPrompt = '';
|
|
955
|
+
continue;
|
|
956
|
+
}
|
|
957
|
+
const maxPrefix = Math.min(current.length, remainingPrompt.length);
|
|
958
|
+
let matched = 0;
|
|
959
|
+
while (matched < maxPrefix && current[matched] === remainingPrompt[matched]) {
|
|
960
|
+
matched++;
|
|
961
|
+
}
|
|
962
|
+
if (matched > 0) {
|
|
963
|
+
index += matched;
|
|
964
|
+
remainingPrompt = remainingPrompt.slice(matched);
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
output += text[index];
|
|
968
|
+
index += 1;
|
|
969
|
+
}
|
|
970
|
+
return { text: output, promptEchoRemainder: remainingPrompt };
|
|
971
|
+
}
|
|
972
|
+
async function emitPtyChunk(session, chunk) {
|
|
973
|
+
const activeTurn = session.activeTurn;
|
|
974
|
+
if (!activeTurn)
|
|
975
|
+
return;
|
|
976
|
+
activeTurn.rawOutput += chunk;
|
|
977
|
+
activeTurn.lastDataAtMs = Date.now();
|
|
978
|
+
// Fix 13: Extract terminal title updates (OSC sequences) for live activity
|
|
979
|
+
// Format: \x1b]0;<title>\x07 or \x1b]0;<title>\x1b\\
|
|
980
|
+
const titleMatch = chunk.match(/\x1b\]0;([^\x07\x1b]*?)(?:\x07|\x1b\\)/);
|
|
981
|
+
if (titleMatch && titleMatch[1]) {
|
|
982
|
+
const title = titleMatch[1].replace(/[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏✳✺✹✸✷✶✵✴✻✽⏵⏸●◐◑◒◓]/g, '').trim();
|
|
983
|
+
if (title && title !== session._lastPtyTitle && title.length > 3) {
|
|
984
|
+
session._lastPtyTitle = title;
|
|
985
|
+
activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
|
|
986
|
+
// Store on session for the polling loop to emit via onDetail
|
|
987
|
+
session._pendingTitle = title;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
const cleaned = normalizeTerminalChunk(chunk);
|
|
991
|
+
if (!cleaned.trim())
|
|
992
|
+
return;
|
|
993
|
+
const echoTrimmed = trimPromptEcho(cleaned, activeTurn.promptEchoRemainder);
|
|
994
|
+
activeTurn.promptEchoRemainder = echoTrimmed.promptEchoRemainder;
|
|
995
|
+
const visibleText = sanitizeVisibleChunk(session.provider, echoTrimmed.text, activeTurn.recentChromeLines);
|
|
996
|
+
if (!visibleText.trim())
|
|
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
|
+
}
|
|
1010
|
+
const terminalView = extractTerminalFacingText(visibleText, activeTurn.assistantOutputDetected);
|
|
1011
|
+
activeTurn.assistantOutputDetected = terminalView.assistantOutputDetected;
|
|
1012
|
+
if (terminalView.terminalText) {
|
|
1013
|
+
activeTurn.lastMeaningfulPtyDataAtMs = Date.now();
|
|
1014
|
+
activeTurn.sawVisibleData = true;
|
|
1015
|
+
const terminalText = terminalView.terminalText.replace(/\n/g, '\r\n');
|
|
1016
|
+
activeTurn.callbackChain = activeTurn.callbackChain
|
|
1017
|
+
.catch(() => undefined)
|
|
1018
|
+
.then(() => activeTurn.onRawChunk?.(terminalText))
|
|
1019
|
+
.then(() => undefined);
|
|
1020
|
+
await activeTurn.callbackChain;
|
|
152
1021
|
}
|
|
153
|
-
return buildCodexSeedPrompt(systemPrompt, messages);
|
|
154
1022
|
}
|
|
155
|
-
function
|
|
1023
|
+
function getDefaultProviderHome(provider) {
|
|
156
1024
|
if (provider === 'claude-cli') {
|
|
157
|
-
return path.join(os.homedir(), '.claude'
|
|
1025
|
+
return path.join(os.homedir(), '.claude');
|
|
158
1026
|
}
|
|
159
|
-
return path.join(os.homedir(), '.codex'
|
|
1027
|
+
return path.join(os.homedir(), '.codex');
|
|
160
1028
|
}
|
|
161
|
-
function
|
|
162
|
-
const
|
|
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);
|
|
1037
|
+
}
|
|
1038
|
+
function listSessionFiles(provider, sessionRoot) {
|
|
1039
|
+
const root = sessionRoot || getProviderSessionRoot(provider);
|
|
163
1040
|
if (!fs.existsSync(root))
|
|
164
1041
|
return [];
|
|
165
1042
|
const results = [];
|
|
@@ -198,8 +1075,24 @@ function extractUserPromptFromRecord(provider, record) {
|
|
|
198
1075
|
}
|
|
199
1076
|
return '';
|
|
200
1077
|
}
|
|
201
|
-
function
|
|
202
|
-
|
|
1078
|
+
function normalizePromptMatchText(text) {
|
|
1079
|
+
return text.replace(/\r/g, '').trim();
|
|
1080
|
+
}
|
|
1081
|
+
function recordTimestampMs(record) {
|
|
1082
|
+
const raw = typeof record?.timestamp === 'string'
|
|
1083
|
+
? record.timestamp
|
|
1084
|
+
: typeof record?.ts === 'number'
|
|
1085
|
+
? record.ts * 1000
|
|
1086
|
+
: null;
|
|
1087
|
+
if (typeof raw === 'number')
|
|
1088
|
+
return Number.isFinite(raw) ? raw : null;
|
|
1089
|
+
if (!raw)
|
|
1090
|
+
return null;
|
|
1091
|
+
const parsed = Date.parse(raw);
|
|
1092
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
1093
|
+
}
|
|
1094
|
+
function recentFileContainsPrompt(provider, filePath, promptText, startedAtMs) {
|
|
1095
|
+
const expected = normalizePromptMatchText(promptText);
|
|
203
1096
|
if (!expected)
|
|
204
1097
|
return false;
|
|
205
1098
|
let stat;
|
|
@@ -224,7 +1117,13 @@ function recentFileContainsPrompt(provider, filePath, promptText) {
|
|
|
224
1117
|
continue;
|
|
225
1118
|
try {
|
|
226
1119
|
const record = JSON.parse(line);
|
|
227
|
-
if (
|
|
1120
|
+
if (provider === 'codex-cli' && startedAtMs) {
|
|
1121
|
+
const ts = recordTimestampMs(record);
|
|
1122
|
+
if (ts !== null && ts < (startedAtMs - 2000)) {
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (normalizePromptMatchText(extractUserPromptFromRecord(provider, record)) === expected) {
|
|
228
1127
|
return true;
|
|
229
1128
|
}
|
|
230
1129
|
}
|
|
@@ -238,8 +1137,8 @@ function recentFileContainsPrompt(provider, filePath, promptText) {
|
|
|
238
1137
|
}
|
|
239
1138
|
return false;
|
|
240
1139
|
}
|
|
241
|
-
function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocator) {
|
|
242
|
-
const candidates = listSessionFiles(provider)
|
|
1140
|
+
function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocator, sessionRoot) {
|
|
1141
|
+
const candidates = listSessionFiles(provider, sessionRoot)
|
|
243
1142
|
.map((candidate) => {
|
|
244
1143
|
let mtimeMs = 0;
|
|
245
1144
|
try {
|
|
@@ -251,44 +1150,149 @@ function discoverSessionFile(provider, launchSnapshot, startedAtMs, promptLocato
|
|
|
251
1150
|
return { candidate, mtimeMs, isNew: !launchSnapshot.has(candidate) };
|
|
252
1151
|
})
|
|
253
1152
|
.filter((item) => !!item)
|
|
254
|
-
.filter((item) => item.mtimeMs >= (startedAtMs - 5000))
|
|
255
|
-
.sort((a, b) =>
|
|
256
|
-
if (a.isNew !== b.isNew)
|
|
257
|
-
return a.isNew ? -1 : 1;
|
|
258
|
-
return b.mtimeMs - a.mtimeMs;
|
|
259
|
-
});
|
|
1153
|
+
.filter((item) => item.isNew && item.mtimeMs >= (startedAtMs - 5000))
|
|
1154
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
260
1155
|
if (promptLocator?.trim()) {
|
|
261
|
-
const matched = candidates.find((item) => recentFileContainsPrompt(provider, item.candidate, promptLocator));
|
|
1156
|
+
const matched = candidates.find((item) => recentFileContainsPrompt(provider, item.candidate, promptLocator, startedAtMs));
|
|
262
1157
|
if (matched)
|
|
263
1158
|
return matched.candidate;
|
|
1159
|
+
if (provider === 'codex-cli') {
|
|
1160
|
+
return null;
|
|
1161
|
+
}
|
|
264
1162
|
}
|
|
265
1163
|
return candidates[0]?.candidate || null;
|
|
266
1164
|
}
|
|
267
|
-
function
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
})
|
|
275
|
-
.join('');
|
|
276
|
-
}
|
|
277
|
-
function extractClaudeToolNames(record) {
|
|
278
|
-
const blocks = Array.isArray(record?.message?.content) ? record.message.content : [];
|
|
279
|
-
return blocks
|
|
280
|
-
.map((block) => (block?.type === 'tool_use' && typeof block?.name === 'string' ? block.name : ''))
|
|
281
|
-
.filter((name) => !!name);
|
|
282
|
-
}
|
|
283
|
-
function extractCodexAssistantText(record) {
|
|
284
|
-
if (!record || typeof record !== 'object')
|
|
285
|
-
return '';
|
|
286
|
-
if (record.type === 'event_msg') {
|
|
287
|
-
if (record.payload?.type === 'agent_message' && typeof record.payload?.message === 'string') {
|
|
288
|
-
return record.payload.message;
|
|
1165
|
+
function findCodexSessionFileBySessionId(sessionId) {
|
|
1166
|
+
if (!sessionId)
|
|
1167
|
+
return null;
|
|
1168
|
+
const candidates = listSessionFiles('codex-cli')
|
|
1169
|
+
.map((candidate) => {
|
|
1170
|
+
try {
|
|
1171
|
+
return { candidate, mtimeMs: fs.statSync(candidate).mtimeMs };
|
|
289
1172
|
}
|
|
290
|
-
|
|
291
|
-
return
|
|
1173
|
+
catch {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
})
|
|
1177
|
+
.filter((item) => !!item)
|
|
1178
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
1179
|
+
for (const item of candidates) {
|
|
1180
|
+
let stat;
|
|
1181
|
+
try {
|
|
1182
|
+
stat = fs.statSync(item.candidate);
|
|
1183
|
+
}
|
|
1184
|
+
catch {
|
|
1185
|
+
continue;
|
|
1186
|
+
}
|
|
1187
|
+
const readLength = Math.min(stat.size, 64 * 1024);
|
|
1188
|
+
if (readLength <= 0)
|
|
1189
|
+
continue;
|
|
1190
|
+
const fd = fs.openSync(item.candidate, 'r');
|
|
1191
|
+
try {
|
|
1192
|
+
const buffer = Buffer.alloc(readLength);
|
|
1193
|
+
fs.readSync(fd, buffer, 0, readLength, 0);
|
|
1194
|
+
const text = buffer.toString('utf8');
|
|
1195
|
+
for (const rawLine of text.split('\n')) {
|
|
1196
|
+
const line = rawLine.trim();
|
|
1197
|
+
if (!line)
|
|
1198
|
+
continue;
|
|
1199
|
+
try {
|
|
1200
|
+
const record = JSON.parse(line);
|
|
1201
|
+
if (record?.type === 'session_meta' && record?.payload?.id === sessionId) {
|
|
1202
|
+
return item.candidate;
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
catch {
|
|
1206
|
+
continue;
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
finally {
|
|
1211
|
+
fs.closeSync(fd);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return null;
|
|
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
|
+
}
|
|
1234
|
+
function formatToolUseDetail(toolName, input) {
|
|
1235
|
+
if (!input || typeof input !== 'object')
|
|
1236
|
+
return `🔧 ${toolName}`;
|
|
1237
|
+
// Format common tools nicely
|
|
1238
|
+
switch (toolName) {
|
|
1239
|
+
case 'Agent':
|
|
1240
|
+
return `🤖 Agent: ${input.description || input.subagent_type || 'working'}`;
|
|
1241
|
+
case 'Read':
|
|
1242
|
+
return `📄 Read: ${input.file_path || ''}`;
|
|
1243
|
+
case 'Write':
|
|
1244
|
+
return `✏️ Write: ${input.file_path || ''}`;
|
|
1245
|
+
case 'Edit':
|
|
1246
|
+
return `✏️ Edit: ${input.file_path || ''}`;
|
|
1247
|
+
case 'Bash':
|
|
1248
|
+
return `💻 Bash: ${String(input.command || '').slice(0, 200)}`;
|
|
1249
|
+
case 'Grep':
|
|
1250
|
+
return `🔍 Grep: "${input.pattern || ''}" ${input.path ? 'in ' + input.path : ''}`;
|
|
1251
|
+
case 'Glob':
|
|
1252
|
+
return `📁 Glob: ${input.pattern || ''}${input.path ? ' in ' + input.path : ''}`;
|
|
1253
|
+
case 'WebSearch':
|
|
1254
|
+
return `🌐 Search: ${input.query || ''}`;
|
|
1255
|
+
case 'WebFetch':
|
|
1256
|
+
return `🌐 Fetch: ${input.url || ''}`;
|
|
1257
|
+
default:
|
|
1258
|
+
return `🔧 ${toolName}: ${JSON.stringify(input).slice(0, 200)}`;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
function formatToolResultDetail(toolUseId, content) {
|
|
1262
|
+
const trimmedId = toolUseId.slice(-8);
|
|
1263
|
+
const lines = content.split('\n').length;
|
|
1264
|
+
const chars = content.length;
|
|
1265
|
+
if (chars <= 200) {
|
|
1266
|
+
return `✅ Result [${trimmedId}]: ${content}`;
|
|
1267
|
+
}
|
|
1268
|
+
const firstLine = content.split('\n')[0].slice(0, 120);
|
|
1269
|
+
return `✅ Result [${trimmedId}]: ${firstLine}... (${lines} lines, ${chars} chars)`;
|
|
1270
|
+
}
|
|
1271
|
+
function extractClaudeText(record) {
|
|
1272
|
+
const blocks = Array.isArray(record?.message?.content) ? record.message.content : [];
|
|
1273
|
+
return blocks
|
|
1274
|
+
.map((block) => {
|
|
1275
|
+
if (!block || typeof block !== 'object')
|
|
1276
|
+
return '';
|
|
1277
|
+
return block.type === 'text' && typeof block.text === 'string' ? block.text : '';
|
|
1278
|
+
})
|
|
1279
|
+
.join('');
|
|
1280
|
+
}
|
|
1281
|
+
function extractClaudeToolNames(record) {
|
|
1282
|
+
const blocks = Array.isArray(record?.message?.content) ? record.message.content : [];
|
|
1283
|
+
return blocks
|
|
1284
|
+
.map((block) => (block?.type === 'tool_use' && typeof block?.name === 'string' ? block.name : ''))
|
|
1285
|
+
.filter((name) => !!name);
|
|
1286
|
+
}
|
|
1287
|
+
function extractCodexAssistantText(record) {
|
|
1288
|
+
if (!record || typeof record !== 'object')
|
|
1289
|
+
return '';
|
|
1290
|
+
if (record.type === 'event_msg') {
|
|
1291
|
+
if (record.payload?.type === 'agent_message' && typeof record.payload?.message === 'string') {
|
|
1292
|
+
return record.payload.message;
|
|
1293
|
+
}
|
|
1294
|
+
if (record.payload?.type === 'task_complete' && typeof record.payload?.last_agent_message === 'string') {
|
|
1295
|
+
return record.payload.last_agent_message;
|
|
292
1296
|
}
|
|
293
1297
|
}
|
|
294
1298
|
if (record.type === 'response_item' && record.payload?.type === 'message' && record.payload?.role === 'assistant') {
|
|
@@ -310,63 +1314,233 @@ function extractCodexAssistantText(record) {
|
|
|
310
1314
|
function normalizeAssistantContent(text) {
|
|
311
1315
|
return text.replace(/^\s+/, '').replace(/\r/g, '');
|
|
312
1316
|
}
|
|
1317
|
+
function normalizeCompletionState(text) {
|
|
1318
|
+
const normalized = normalizeAssistantContent(text);
|
|
1319
|
+
if (!normalized)
|
|
1320
|
+
return { text: '', hasSentinel: false };
|
|
1321
|
+
const stripped = (0, completion_marker_1.stripCompletionSentinel)(normalized);
|
|
1322
|
+
return { text: stripped.text, hasSentinel: stripped.found };
|
|
1323
|
+
}
|
|
313
1324
|
async function emitDetail(tracker, detail, cb) {
|
|
314
1325
|
const trimmed = detail.trim();
|
|
315
1326
|
if (!trimmed)
|
|
316
1327
|
return;
|
|
317
|
-
|
|
1328
|
+
// Only deduplicate consecutive identical details, not across the whole turn
|
|
1329
|
+
if (tracker.lastDetail === trimmed)
|
|
318
1330
|
return;
|
|
319
|
-
tracker.
|
|
1331
|
+
tracker.lastDetail = trimmed;
|
|
320
1332
|
await cb?.(trimmed);
|
|
321
1333
|
}
|
|
1334
|
+
async function emitAssistantChunk(tracker, nextText, cb) {
|
|
1335
|
+
const normalized = normalizeCompletionState(nextText).text;
|
|
1336
|
+
if (!normalized)
|
|
1337
|
+
return;
|
|
1338
|
+
const previous = tracker.lastAssistantText;
|
|
1339
|
+
if (normalized === previous)
|
|
1340
|
+
return;
|
|
1341
|
+
tracker.lastAssistantText = normalized;
|
|
1342
|
+
if (!cb)
|
|
1343
|
+
return;
|
|
1344
|
+
if (!previous) {
|
|
1345
|
+
await cb(normalized);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (normalized.startsWith(previous)) {
|
|
1349
|
+
const delta = normalized.slice(previous.length);
|
|
1350
|
+
if (delta) {
|
|
1351
|
+
await cb(delta);
|
|
1352
|
+
}
|
|
1353
|
+
return;
|
|
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;
|
|
1379
|
+
}
|
|
322
1380
|
function parseClaudeSessionRecord(record) {
|
|
323
1381
|
const sessionId = typeof record?.sessionId === 'string' ? record.sessionId : undefined;
|
|
324
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;
|
|
325
1390
|
const usage = record.message.usage
|
|
326
1391
|
? {
|
|
327
|
-
inputTokens:
|
|
328
|
-
|
|
329
|
-
|
|
1392
|
+
inputTokens: fresh + cacheCreation + cacheRead,
|
|
1393
|
+
inputTokensFresh: fresh,
|
|
1394
|
+
inputTokensCacheCreation: cacheCreation,
|
|
1395
|
+
inputTokensCacheRead: cacheRead,
|
|
330
1396
|
outputTokens: record.message.usage.output_tokens || 0,
|
|
331
1397
|
}
|
|
332
1398
|
: undefined;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
1399
|
+
// Build detail lines and live activity events from ALL content blocks
|
|
1400
|
+
const blocks = Array.isArray(record.message.content) ? record.message.content : [];
|
|
1401
|
+
const details = [];
|
|
1402
|
+
const activities = [];
|
|
1403
|
+
const openedToolUseIds = [];
|
|
1404
|
+
for (const block of blocks) {
|
|
1405
|
+
if (!block || typeof block !== 'object')
|
|
1406
|
+
continue;
|
|
1407
|
+
if (block.type === 'thinking' && typeof block.thinking === 'string' && block.thinking.trim()) {
|
|
1408
|
+
details.push(`💭 Thinking`);
|
|
1409
|
+
}
|
|
1410
|
+
else if (block.type === 'tool_use') {
|
|
1411
|
+
const toolName = typeof block.name === 'string' ? block.name : 'unknown';
|
|
1412
|
+
if (typeof block.id === 'string' && block.id.trim()) {
|
|
1413
|
+
openedToolUseIds.push(block.id);
|
|
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
|
+
});
|
|
1423
|
+
// Emit structured live activity events
|
|
1424
|
+
if (toolName === 'Agent') {
|
|
1425
|
+
const activityLabel = (0, live_activity_1.agentToolUseToLabel)(block.input || {});
|
|
1426
|
+
if (activityLabel) {
|
|
1427
|
+
activities.push({
|
|
1428
|
+
type: 'subagent_started',
|
|
1429
|
+
label: activityLabel,
|
|
1430
|
+
key: block.id || undefined,
|
|
1431
|
+
timestamp: Date.now(),
|
|
1432
|
+
});
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
else {
|
|
1436
|
+
const activityLabel = (0, live_activity_1.toolUseToActivityLabel)(toolName);
|
|
1437
|
+
if (activityLabel) {
|
|
1438
|
+
activities.push({
|
|
1439
|
+
type: 'working',
|
|
1440
|
+
label: activityLabel,
|
|
1441
|
+
key: block.id || undefined,
|
|
1442
|
+
timestamp: Date.now(),
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
340
1447
|
}
|
|
341
|
-
const
|
|
1448
|
+
const completionState = normalizeCompletionState(extractClaudeText(record));
|
|
1449
|
+
const text = completionState.text;
|
|
1450
|
+
const detailStr = details.length > 0 ? details.join('\n') : undefined;
|
|
1451
|
+
// Completion detection:
|
|
1452
|
+
// 1. Explicit stop_reason (end_turn, max_tokens, stop_sequence) = done immediately
|
|
1453
|
+
// 2. stop_reason: null or tool_use = NOT done (wait for system/turn_duration fallback)
|
|
1454
|
+
// Why not "text + no tool_use = done"? Because CLI writes text and tool_use as
|
|
1455
|
+
// separate records — text arrives first with stop_reason: null, then tool_use follows.
|
|
1456
|
+
// We'd mark done prematurely before the tool_use record arrives.
|
|
1457
|
+
const stopReason = record.message?.stop_reason;
|
|
1458
|
+
const isDone = !!stopReason && stopReason !== 'tool_use';
|
|
1459
|
+
const activityResult = activities.length > 0 ? { activities } : {};
|
|
1460
|
+
const toolUseResult = openedToolUseIds.length > 0 ? { openedToolUseIds } : {};
|
|
342
1461
|
if (text) {
|
|
1462
|
+
const shouldFinalize = isDone || completionState.hasSentinel;
|
|
343
1463
|
return {
|
|
344
1464
|
sessionId,
|
|
345
|
-
|
|
1465
|
+
assistantText: text,
|
|
1466
|
+
...(detailStr ? { detail: detailStr } : {}),
|
|
1467
|
+
...(shouldFinalize
|
|
346
1468
|
? { finalContent: text, done: true, usage }
|
|
347
|
-
: {
|
|
1469
|
+
: { usage }),
|
|
1470
|
+
...activityResult,
|
|
1471
|
+
...toolUseResult,
|
|
348
1472
|
};
|
|
349
1473
|
}
|
|
350
|
-
return {
|
|
1474
|
+
return {
|
|
1475
|
+
sessionId,
|
|
1476
|
+
...(detailStr ? { detail: detailStr } : {}),
|
|
1477
|
+
...(completionState.hasSentinel ? { done: true } : {}),
|
|
1478
|
+
usage,
|
|
1479
|
+
...activityResult,
|
|
1480
|
+
...toolUseResult,
|
|
1481
|
+
};
|
|
351
1482
|
}
|
|
352
1483
|
if (record?.type === 'user' && Array.isArray(record?.message?.content)) {
|
|
353
|
-
const
|
|
354
|
-
.map((block) =>
|
|
1484
|
+
const resolvedToolUseIds = record.message.content
|
|
1485
|
+
.map((block) => (typeof block?.tool_use_id === 'string' ? block.tool_use_id : ''))
|
|
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) {
|
|
355
1490
|
if (!block || typeof block !== 'object')
|
|
356
|
-
|
|
357
|
-
if (typeof block.tool_use_id
|
|
358
|
-
|
|
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);
|
|
359
1499
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
+
});
|
|
366
1513
|
}
|
|
1514
|
+
if (resolvedToolUseIds.length > 0) {
|
|
1515
|
+
return {
|
|
1516
|
+
sessionId,
|
|
1517
|
+
resolvedToolUseIds,
|
|
1518
|
+
...(resultActivities.length > 0 ? { activities: resultActivities } : {}),
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
// system/turn_duration is a fallback completion signal from the CLI
|
|
1523
|
+
if (record?.type === 'system' && record?.subtype === 'turn_duration') {
|
|
1524
|
+
return { sessionId, done: true };
|
|
1525
|
+
}
|
|
1526
|
+
// Skip internal bookkeeping records, surface anything else
|
|
1527
|
+
const skipTypes = ['permission-mode', 'file-history-snapshot', 'attachment', 'system'];
|
|
1528
|
+
if (record?.type && !skipTypes.includes(record.type) && record.type !== 'assistant' && record.type !== 'user') {
|
|
1529
|
+
return { sessionId, detail: `[${record.type}]` };
|
|
367
1530
|
}
|
|
368
1531
|
return sessionId ? { sessionId } : {};
|
|
369
1532
|
}
|
|
1533
|
+
function canFinalizeClaudeTurnOnSessionExit(session, tracker) {
|
|
1534
|
+
if (session.provider !== 'claude-cli')
|
|
1535
|
+
return false;
|
|
1536
|
+
if (tracker.sawExplicitCompletion || tracker.done)
|
|
1537
|
+
return false;
|
|
1538
|
+
if (!tracker.lastAssistantText.trim())
|
|
1539
|
+
return false;
|
|
1540
|
+
if (tracker.pendingToolUseIds.size > 0)
|
|
1541
|
+
return false;
|
|
1542
|
+
return true;
|
|
1543
|
+
}
|
|
370
1544
|
function parseCodexSessionRecord(record) {
|
|
371
1545
|
if (!record || typeof record !== 'object')
|
|
372
1546
|
return {};
|
|
@@ -374,62 +1548,543 @@ function parseCodexSessionRecord(record) {
|
|
|
374
1548
|
return { sessionId: record.payload.id };
|
|
375
1549
|
}
|
|
376
1550
|
if (record.type === 'event_msg' && record.payload?.type === 'task_started') {
|
|
377
|
-
return {
|
|
1551
|
+
return {};
|
|
378
1552
|
}
|
|
379
1553
|
if (record.type === 'event_msg' && record.payload?.type === 'task_complete') {
|
|
380
|
-
const
|
|
1554
|
+
const completionState = normalizeCompletionState(typeof record.payload?.last_agent_message === 'string' ? record.payload.last_agent_message : '');
|
|
381
1555
|
return {
|
|
382
|
-
|
|
1556
|
+
assistantText: completionState.text,
|
|
1557
|
+
finalContent: completionState.text,
|
|
383
1558
|
done: true,
|
|
384
1559
|
};
|
|
385
1560
|
}
|
|
386
1561
|
if (record.type === 'event_msg' && record.payload?.type === 'token_count' && record.payload?.info?.total_token_usage) {
|
|
1562
|
+
const lastUsage = record.payload.info.last_token_usage;
|
|
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;
|
|
1571
|
+
return {
|
|
1572
|
+
usage: {
|
|
1573
|
+
inputTokens: fresh + cacheRead,
|
|
1574
|
+
inputTokensFresh: fresh,
|
|
1575
|
+
inputTokensCacheCreation: 0,
|
|
1576
|
+
inputTokensCacheRead: cacheRead,
|
|
1577
|
+
outputTokens: lastUsage.output_tokens || 0,
|
|
1578
|
+
},
|
|
1579
|
+
};
|
|
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;
|
|
387
1583
|
return {
|
|
388
1584
|
usage: {
|
|
389
|
-
inputTokens:
|
|
390
|
-
|
|
1585
|
+
inputTokens: fresh + cacheRead,
|
|
1586
|
+
inputTokensFresh: fresh,
|
|
1587
|
+
inputTokensCacheCreation: 0,
|
|
1588
|
+
inputTokensCacheRead: cacheRead,
|
|
391
1589
|
outputTokens: record.payload.info.total_token_usage.output_tokens || 0,
|
|
392
1590
|
},
|
|
393
1591
|
};
|
|
394
1592
|
}
|
|
395
|
-
const
|
|
1593
|
+
const completionState = normalizeCompletionState(extractCodexAssistantText(record));
|
|
1594
|
+
const assistantText = completionState.text;
|
|
396
1595
|
if (assistantText) {
|
|
397
|
-
return {
|
|
1596
|
+
return {
|
|
1597
|
+
assistantText,
|
|
1598
|
+
finalContent: assistantText,
|
|
1599
|
+
...(completionState.hasSentinel ? { done: true } : {}),
|
|
1600
|
+
};
|
|
398
1601
|
}
|
|
399
1602
|
if (record.type === 'event_msg' && typeof record.payload?.type === 'string') {
|
|
400
1603
|
const eventType = record.payload.type;
|
|
401
1604
|
if (!['user_message', 'agent_message', 'token_count', 'task_complete'].includes(eventType)) {
|
|
402
|
-
return {
|
|
1605
|
+
return {};
|
|
403
1606
|
}
|
|
404
1607
|
}
|
|
405
1608
|
return {};
|
|
406
1609
|
}
|
|
407
1610
|
async function waitForFirstTerminalData(session) {
|
|
408
1611
|
await Promise.race([session.readyPromise, delay(5000)]);
|
|
409
|
-
|
|
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';
|
|
1672
|
+
}
|
|
1673
|
+
async function writePromptInChunks(pty, promptText, chunkSize = 512, chunkDelayMs = 15) {
|
|
1674
|
+
for (let offset = 0; offset < promptText.length; offset += chunkSize) {
|
|
1675
|
+
pty.write(promptText.slice(offset, offset + chunkSize));
|
|
1676
|
+
if (offset + chunkSize < promptText.length) {
|
|
1677
|
+
await delay(chunkDelayMs);
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
async function writeBracketedPaste(pty, promptText, chunkSize, chunkDelayMs) {
|
|
1682
|
+
pty.write('\x1b[200~');
|
|
1683
|
+
await writePromptInChunks(pty, promptText, chunkSize, chunkDelayMs);
|
|
1684
|
+
pty.write('\x1b[201~');
|
|
410
1685
|
}
|
|
411
1686
|
async function writeInteractivePrompt(session, promptText) {
|
|
412
1687
|
if (session.provider === 'codex-cli') {
|
|
413
|
-
|
|
1688
|
+
const multiline = promptText.includes('\n');
|
|
1689
|
+
if (multiline) {
|
|
1690
|
+
await writeBracketedPaste(session.pty, promptText, 512, 15);
|
|
1691
|
+
}
|
|
1692
|
+
else {
|
|
1693
|
+
await writePromptInChunks(session.pty, promptText);
|
|
1694
|
+
}
|
|
414
1695
|
await delay(session.submitDelayMs);
|
|
415
1696
|
session.pty.write('\r');
|
|
416
1697
|
return;
|
|
417
1698
|
}
|
|
418
|
-
|
|
1699
|
+
// 512-byte chunks with 10ms delays — proven safe for Windows ConPTY pipe buffer.
|
|
1700
|
+
await writeBracketedPaste(session.pty, promptText, 512, 10);
|
|
1701
|
+
await delay(session.submitDelayMs);
|
|
1702
|
+
session.pty.write('\r');
|
|
1703
|
+
}
|
|
1704
|
+
async function writeNakedPrompt(session, text) {
|
|
1705
|
+
await writePromptInChunks(session.pty, text);
|
|
1706
|
+
await delay(session.submitDelayMs);
|
|
419
1707
|
session.pty.write('\r');
|
|
420
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
|
+
}
|
|
421
1894
|
class LocalCliPtySessionManager {
|
|
422
1895
|
sessions = new Map();
|
|
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();
|
|
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);
|
|
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
|
+
};
|
|
1985
|
+
}
|
|
423
1986
|
async runTurn(opts) {
|
|
424
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;
|
|
425
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;
|
|
426
2000
|
if (session
|
|
427
|
-
&& (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
|
+
}
|
|
428
2014
|
this.closeSession(key);
|
|
429
2015
|
session = undefined;
|
|
430
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
|
+
}
|
|
431
2063
|
if (!session) {
|
|
432
|
-
|
|
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
|
+
});
|
|
2072
|
+
try {
|
|
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);
|
|
2078
|
+
}
|
|
2079
|
+
catch (firstErr) {
|
|
2080
|
+
console.warn(`[pty] Session creation failed, retrying in 1s: ${firstErr instanceof Error ? firstErr.message : String(firstErr)}`);
|
|
2081
|
+
await delay(1000);
|
|
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);
|
|
2087
|
+
}
|
|
433
2088
|
this.sessions.set(key, session);
|
|
434
2089
|
}
|
|
435
2090
|
const run = async () => this.runTurnInternal(session, opts);
|
|
@@ -437,9 +2092,107 @@ class LocalCliPtySessionManager {
|
|
|
437
2092
|
session.chain = queued.then(() => undefined, () => undefined);
|
|
438
2093
|
return queued;
|
|
439
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
|
+
}
|
|
440
2138
|
closeSessionByConversation(conversationId, botId) {
|
|
441
2139
|
this.closeSession(sessionKey(conversationId, botId));
|
|
442
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
|
+
}
|
|
443
2196
|
closeAll() {
|
|
444
2197
|
for (const key of [...this.sessions.keys()]) {
|
|
445
2198
|
this.closeSession(key);
|
|
@@ -450,147 +2203,524 @@ class LocalCliPtySessionManager {
|
|
|
450
2203
|
if (!session)
|
|
451
2204
|
return;
|
|
452
2205
|
session.closed = true;
|
|
453
|
-
|
|
454
|
-
|
|
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;
|
|
455
2212
|
}
|
|
456
|
-
|
|
457
|
-
|
|
2213
|
+
else {
|
|
2214
|
+
try {
|
|
2215
|
+
session.pty.kill();
|
|
2216
|
+
}
|
|
2217
|
+
catch {
|
|
2218
|
+
closeFailed = true;
|
|
2219
|
+
}
|
|
458
2220
|
}
|
|
459
2221
|
this.sessions.delete(key);
|
|
2222
|
+
(0, managed_process_registry_1.unregisterProcess)(key, false, closeFailed);
|
|
2223
|
+
}
|
|
2224
|
+
closeSessionByKey(key) {
|
|
2225
|
+
this.closeSession(key);
|
|
460
2226
|
}
|
|
461
|
-
createSession(key, provider, cwd) {
|
|
2227
|
+
createSession(key, botId, conversationId, provider, botSettings, cwd, useConpty, toolEnv, resumeSessionId, newSessionId) {
|
|
462
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);
|
|
463
2233
|
const cleanEnv = { ...process.env };
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
2234
|
+
for (const envKey of Object.keys(cleanEnv)) {
|
|
2235
|
+
if (envKey === 'CLAUDECODE' || envKey.startsWith('CLAUDE_CODE_')) {
|
|
2236
|
+
delete cleanEnv[envKey];
|
|
2237
|
+
}
|
|
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
|
+
}
|
|
2264
|
+
// Snapshot existing session files BEFORE spawning the PTY.
|
|
2265
|
+
// Only needed for Pattern B providers (no --session-id support).
|
|
2266
|
+
// Pattern A providers know their file path from the generated ID.
|
|
2267
|
+
const preSpawnSnapshot = new Set(listSessionFiles(provider, sessionFilesRoot));
|
|
467
2268
|
let readyResolve = null;
|
|
468
2269
|
const readyPromise = new Promise((resolve) => {
|
|
469
2270
|
readyResolve = resolve;
|
|
470
2271
|
});
|
|
2272
|
+
// Build Claude CLI args with session control:
|
|
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;
|
|
2280
|
+
let knownSessionFilePath = null;
|
|
2281
|
+
if (knownSessionId && provider === 'claude-cli') {
|
|
2282
|
+
// Search all project directories for the session file.
|
|
2283
|
+
// Don't try to derive the directory name — Claude's naming convention
|
|
2284
|
+
// (e.g., C--Projects-Funolio) doesn't match simple path normalization.
|
|
2285
|
+
const projectRoot = sessionFilesRoot;
|
|
2286
|
+
if (fs.existsSync(projectRoot)) {
|
|
2287
|
+
try {
|
|
2288
|
+
const projectDirs = fs.readdirSync(projectRoot);
|
|
2289
|
+
for (const dir of projectDirs) {
|
|
2290
|
+
const candidate = path.join(projectRoot, dir, `${knownSessionId}.jsonl`);
|
|
2291
|
+
if (fs.existsSync(candidate)) {
|
|
2292
|
+
knownSessionFilePath = candidate;
|
|
2293
|
+
break;
|
|
2294
|
+
}
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
catch { }
|
|
2298
|
+
}
|
|
2299
|
+
// For new sessions, file doesn't exist yet. We'll find it after Claude creates it
|
|
2300
|
+
// by searching again in the polling loop (or it'll be discovered via discoverSessionFile).
|
|
2301
|
+
}
|
|
2302
|
+
else if (knownSessionId && provider === 'codex-cli') {
|
|
2303
|
+
knownSessionFilePath = findCodexSessionFileBySessionId(knownSessionId);
|
|
2304
|
+
}
|
|
2305
|
+
const codexArgs = buildCodexCliArgs(resumeSessionId, botSettings?.codex);
|
|
471
2306
|
const pty = provider === 'codex-cli'
|
|
472
|
-
? ptyModule.spawn(findExecutableOnPath('codex.cmd') || 'codex.cmd',
|
|
2307
|
+
? ptyModule.spawn(findExecutableOnPath('codex.cmd') || 'codex.cmd', codexArgs, {
|
|
473
2308
|
cwd,
|
|
474
2309
|
cols: 160,
|
|
475
2310
|
rows: 48,
|
|
476
2311
|
env: cleanEnv,
|
|
477
|
-
useConpty
|
|
2312
|
+
useConpty,
|
|
478
2313
|
name: 'xterm-color',
|
|
479
2314
|
})
|
|
480
|
-
: ptyModule.spawn('
|
|
2315
|
+
: ptyModule.spawn(findExecutableOnPath('claude.exe') || findExecutableOnPath('claude.cmd') || 'claude', claudeArgs, {
|
|
481
2316
|
cwd,
|
|
482
2317
|
cols: 160,
|
|
483
2318
|
rows: 48,
|
|
484
2319
|
env: cleanEnv,
|
|
485
|
-
useConpty
|
|
2320
|
+
useConpty,
|
|
486
2321
|
name: 'xterm-color',
|
|
487
2322
|
});
|
|
488
2323
|
const session = {
|
|
489
2324
|
key,
|
|
2325
|
+
conversationId,
|
|
2326
|
+
botId,
|
|
2327
|
+
topicId: null,
|
|
2328
|
+
warmRuntimeMode: null,
|
|
490
2329
|
provider,
|
|
491
2330
|
cwd,
|
|
2331
|
+
useConpty,
|
|
492
2332
|
pty,
|
|
493
2333
|
createdAtMs: Date.now(),
|
|
494
2334
|
lastUsedAtMs: Date.now(),
|
|
495
|
-
launchSnapshot:
|
|
496
|
-
|
|
497
|
-
|
|
2335
|
+
launchSnapshot: preSpawnSnapshot,
|
|
2336
|
+
sessionFilesRoot,
|
|
2337
|
+
sessionId: knownSessionId,
|
|
2338
|
+
sessionFilePath: knownSessionFilePath,
|
|
498
2339
|
sessionFileOffset: 0,
|
|
499
2340
|
sessionFileCarry: '',
|
|
500
2341
|
readyPromise,
|
|
501
2342
|
readyResolved: false,
|
|
502
|
-
waitForNextSendMs:
|
|
503
|
-
startupDelayMs:
|
|
504
|
-
|
|
2343
|
+
waitForNextSendMs: 100,
|
|
2344
|
+
startupDelayMs: 1200,
|
|
2345
|
+
startupDelayApplied: false,
|
|
2346
|
+
submitDelayMs: provider === 'codex-cli' ? 175 : 200,
|
|
505
2347
|
currentPromptLocator: null,
|
|
506
2348
|
currentPromptStartedAtMs: 0,
|
|
2349
|
+
activeTurn: null,
|
|
2350
|
+
warmPromise: null,
|
|
2351
|
+
warmRequestedAtMs: null,
|
|
2352
|
+
warmReadyAtMs: null,
|
|
2353
|
+
recentTerminalOutput: '',
|
|
507
2354
|
closed: false,
|
|
2355
|
+
exitReason: null,
|
|
508
2356
|
chain: Promise.resolve(),
|
|
2357
|
+
childFollowers: new Map(),
|
|
2358
|
+
childSnapshot: new Set(),
|
|
2359
|
+
claudeAuthFingerprint: provider === 'claude-cli' ? currentClaudeAuthFingerprint() : null,
|
|
2360
|
+
runtimeHomeDir: claudeSessionHome,
|
|
509
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
|
+
}
|
|
510
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
|
+
}
|
|
511
2380
|
if (!session.readyResolved && chunk && chunk.trim()) {
|
|
512
2381
|
session.readyResolved = true;
|
|
513
2382
|
readyResolve?.();
|
|
514
2383
|
}
|
|
2384
|
+
if (session.activeTurn && chunk) {
|
|
2385
|
+
void emitPtyChunk(session, chunk);
|
|
2386
|
+
}
|
|
515
2387
|
});
|
|
516
|
-
pty.on('exit', () => {
|
|
2388
|
+
pty.on('exit', (...exitArgs) => {
|
|
517
2389
|
session.closed = true;
|
|
2390
|
+
session.exitReason = describePtyExit(exitArgs);
|
|
2391
|
+
console.warn(`[local-cli-pty] ${provider} PTY exited (${session.exitReason}) cwd=${cwd}`);
|
|
518
2392
|
this.sessions.delete(key);
|
|
2393
|
+
(0, managed_process_registry_1.unregisterProcess)(key, false);
|
|
519
2394
|
});
|
|
520
|
-
if (provider === 'claude-cli') {
|
|
521
|
-
pty.write('claude\r');
|
|
522
|
-
}
|
|
523
2395
|
return session;
|
|
524
2396
|
}
|
|
525
|
-
async
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
throw new Error(`${session.provider} PTY session closed before prompt was sent`);
|
|
530
|
-
}
|
|
531
|
-
if (session.waitForNextSendMs > 0) {
|
|
532
|
-
await delay(session.waitForNextSendMs);
|
|
533
|
-
}
|
|
534
|
-
const tracker = {
|
|
535
|
-
done: false,
|
|
536
|
-
finalContent: '',
|
|
537
|
-
usage: undefined,
|
|
538
|
-
lastAssistantText: '',
|
|
539
|
-
detailFingerprints: new Set(),
|
|
2397
|
+
async runPassthroughInternal(session, opts) {
|
|
2398
|
+
const abortSignal = opts.abortSignal;
|
|
2399
|
+
const abortHandler = () => {
|
|
2400
|
+
this.closeSession(session.key);
|
|
540
2401
|
};
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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);
|
|
550
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
|
+
}
|
|
2470
|
+
async runTurnInternal(session, opts) {
|
|
2471
|
+
const abortSignal = opts.abortSignal;
|
|
2472
|
+
const abortHandler = () => {
|
|
2473
|
+
this.closeSession(session.key);
|
|
2474
|
+
};
|
|
2475
|
+
abortSignal?.addEventListener('abort', abortHandler, { once: true });
|
|
2476
|
+
try {
|
|
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');
|
|
2480
|
+
throwIfAborted(abortSignal);
|
|
2481
|
+
await waitForFirstTerminalData(session);
|
|
2482
|
+
throwIfAborted(abortSignal);
|
|
551
2483
|
if (session.closed) {
|
|
552
|
-
throw
|
|
2484
|
+
throw formatClosedSessionError(session, 'closed before prompt was sent');
|
|
553
2485
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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) {
|
|
2491
|
+
try {
|
|
2492
|
+
const currentSize = fs.statSync(session.sessionFilePath).size;
|
|
2493
|
+
session.sessionFileOffset = currentSize;
|
|
559
2494
|
session.sessionFileCarry = '';
|
|
560
2495
|
}
|
|
2496
|
+
catch {
|
|
2497
|
+
// File may not exist yet for new sessions — that's fine.
|
|
2498
|
+
}
|
|
561
2499
|
}
|
|
562
|
-
if (session.
|
|
563
|
-
await
|
|
2500
|
+
if (session.waitForNextSendMs > 0) {
|
|
2501
|
+
await delayWithAbort(session.waitForNextSendMs, abortSignal);
|
|
564
2502
|
}
|
|
565
|
-
|
|
566
|
-
|
|
2503
|
+
const timeoutMs = opts.timeoutMs ?? 10 * 60 * 1000;
|
|
2504
|
+
const startedAtMs = Date.now();
|
|
2505
|
+
const tracker = {
|
|
2506
|
+
done: false,
|
|
2507
|
+
sawExplicitCompletion: false,
|
|
2508
|
+
finalContent: '',
|
|
2509
|
+
usage: undefined,
|
|
2510
|
+
lastAssistantText: '',
|
|
2511
|
+
detailFingerprints: new Set(),
|
|
2512
|
+
pendingToolUseIds: new Set(),
|
|
2513
|
+
lastRecordAtMs: Date.now(),
|
|
2514
|
+
sawCompletionSentinel: false,
|
|
2515
|
+
turnStartedAtMs: startedAtMs,
|
|
2516
|
+
expectedUserPromptSnippet: extractCurrentUserPromptSnippet(opts.messages),
|
|
2517
|
+
sawCurrentTurnUserRecord: false,
|
|
2518
|
+
};
|
|
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);
|
|
2521
|
+
session.currentPromptLocator = promptText.trim();
|
|
2522
|
+
session.currentPromptStartedAtMs = startedAtMs;
|
|
2523
|
+
const activeTurn = {
|
|
2524
|
+
mode: 'default',
|
|
2525
|
+
promptEchoRemainder: normalizeTerminalChunk(promptText),
|
|
2526
|
+
rawOutput: '',
|
|
2527
|
+
visibleOutput: '',
|
|
2528
|
+
lastDataAtMs: startedAtMs,
|
|
2529
|
+
lastMeaningfulPtyDataAtMs: startedAtMs,
|
|
2530
|
+
callbackChain: Promise.resolve(),
|
|
2531
|
+
onChunk: opts.onChunk,
|
|
2532
|
+
onRawChunk: opts.onRawChunk,
|
|
2533
|
+
recentChromeLines: [],
|
|
2534
|
+
assistantOutputDetected: false,
|
|
2535
|
+
sawVisibleData: false,
|
|
2536
|
+
};
|
|
2537
|
+
session.activeTurn = activeTurn;
|
|
2538
|
+
// Snapshot existing child subagent files before this turn
|
|
2539
|
+
session.childFollowers.clear();
|
|
2540
|
+
if (session.sessionFilePath && session.provider === 'claude-cli') {
|
|
2541
|
+
const sessionDir = session.sessionFilePath.replace(/\.jsonl$/, '');
|
|
2542
|
+
const subagentsDir = path.join(sessionDir, 'subagents');
|
|
2543
|
+
if (fs.existsSync(subagentsDir)) {
|
|
2544
|
+
try {
|
|
2545
|
+
const existing = fs.readdirSync(subagentsDir).filter((f) => f.endsWith('.jsonl'));
|
|
2546
|
+
session.childSnapshot = new Set(existing.map((f) => path.join(subagentsDir, f)));
|
|
2547
|
+
}
|
|
2548
|
+
catch {
|
|
2549
|
+
session.childSnapshot = new Set();
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
else {
|
|
2553
|
+
session.childSnapshot = new Set();
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
throwIfAborted(abortSignal);
|
|
2557
|
+
await writeInteractivePrompt(session, promptText);
|
|
2558
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'prompt_sent');
|
|
2559
|
+
let lastHeartbeatAtMs = Date.now();
|
|
2560
|
+
const HEARTBEAT_INTERVAL_MS = 120_000; // 2 minutes
|
|
2561
|
+
while (!tracker.done) {
|
|
2562
|
+
throwIfAborted(abortSignal);
|
|
2563
|
+
if (Date.now() - startedAtMs > timeoutMs) {
|
|
2564
|
+
throw new Error(`${opts.provider} PTY session timed out waiting for a response`);
|
|
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
|
+
}
|
|
2574
|
+
const inactivityFailTimeoutMs = getPtyInactivityFailTimeoutMs(session.provider);
|
|
2575
|
+
if (inactivityFailTimeoutMs != null
|
|
2576
|
+
&& Date.now() - activeTurn.lastMeaningfulPtyDataAtMs > inactivityFailTimeoutMs) {
|
|
2577
|
+
if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
|
|
2578
|
+
await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
|
|
2579
|
+
}
|
|
2580
|
+
if (tracker.done) {
|
|
2581
|
+
break;
|
|
2582
|
+
}
|
|
2583
|
+
throw new Error(`${opts.provider} PTY session had no meaningful PTY activity for ${Math.floor(inactivityFailTimeoutMs / 1000)}s`);
|
|
2584
|
+
}
|
|
2585
|
+
// Heartbeat: if no detail emitted for 2+ minutes, send a status pulse
|
|
2586
|
+
if (opts.onDetail && Date.now() - lastHeartbeatAtMs > HEARTBEAT_INTERVAL_MS) {
|
|
2587
|
+
const elapsed = Math.floor((Date.now() - startedAtMs) / 1000);
|
|
2588
|
+
await opts.onDetail(`⏳ Still working... (${elapsed}s elapsed)`);
|
|
2589
|
+
lastHeartbeatAtMs = Date.now();
|
|
2590
|
+
}
|
|
2591
|
+
if (!session.sessionFilePath) {
|
|
2592
|
+
// Pattern A (known session ID): search for our specific file by ID
|
|
2593
|
+
if (session.sessionId && session.provider === 'claude-cli') {
|
|
2594
|
+
const projectRoot = session.sessionFilesRoot;
|
|
2595
|
+
if (fs.existsSync(projectRoot)) {
|
|
2596
|
+
try {
|
|
2597
|
+
for (const dir of fs.readdirSync(projectRoot)) {
|
|
2598
|
+
const candidate = path.join(projectRoot, dir, `${session.sessionId}.jsonl`);
|
|
2599
|
+
if (fs.existsSync(candidate)) {
|
|
2600
|
+
session.sessionFilePath = candidate;
|
|
2601
|
+
session.sessionFileOffset = 0;
|
|
2602
|
+
session.sessionFileCarry = '';
|
|
2603
|
+
break;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
}
|
|
2607
|
+
catch { }
|
|
2608
|
+
}
|
|
2609
|
+
}
|
|
2610
|
+
// Pattern B (unknown ID): discover by snapshot diff
|
|
2611
|
+
if (!session.sessionFilePath) {
|
|
2612
|
+
const discovered = discoverSessionFile(session.provider, session.launchSnapshot, session.currentPromptStartedAtMs || session.createdAtMs, session.currentPromptLocator, session.sessionFilesRoot);
|
|
2613
|
+
if (discovered) {
|
|
2614
|
+
session.sessionFilePath = discovered;
|
|
2615
|
+
session.sessionFileOffset = 0;
|
|
2616
|
+
session.sessionFileCarry = '';
|
|
2617
|
+
}
|
|
2618
|
+
}
|
|
2619
|
+
}
|
|
2620
|
+
if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
|
|
2621
|
+
await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
|
|
2622
|
+
}
|
|
2623
|
+
if (waitForFreshClaudeTranscript
|
|
2624
|
+
&& !(session.sessionFilePath && fs.existsSync(session.sessionFilePath))) {
|
|
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
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
if (session._pendingTitle) {
|
|
2652
|
+
session._pendingTitle = null;
|
|
2653
|
+
}
|
|
2654
|
+
// Fix 12: Follow child sub-agent JSON files for live progress
|
|
2655
|
+
if (session.sessionFilePath && session.provider === 'claude-cli' && opts.onDetail) {
|
|
2656
|
+
await this.consumeChildSubagentFiles(session, tracker, opts.onDetail);
|
|
2657
|
+
}
|
|
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
|
+
}
|
|
2667
|
+
if (canFinalizeClaudeTurnOnSessionExit(session, tracker)) {
|
|
2668
|
+
tracker.done = true;
|
|
2669
|
+
tracker.finalContent = tracker.finalContent || tracker.lastAssistantText;
|
|
2670
|
+
break;
|
|
2671
|
+
}
|
|
2672
|
+
throw formatClosedSessionError(session, 'exited while waiting for a response');
|
|
2673
|
+
}
|
|
2674
|
+
if (!tracker.done) {
|
|
2675
|
+
await delayWithAbort(200, abortSignal);
|
|
2676
|
+
}
|
|
2677
|
+
}
|
|
2678
|
+
const settleStartedAt = Date.now();
|
|
2679
|
+
while (Date.now() - Math.max(activeTurn.lastDataAtMs, settleStartedAt) < 100) {
|
|
2680
|
+
throwIfAborted(abortSignal);
|
|
2681
|
+
if (Date.now() - settleStartedAt > 500)
|
|
2682
|
+
break;
|
|
2683
|
+
await delayWithAbort(50, abortSignal);
|
|
567
2684
|
}
|
|
2685
|
+
if (session.sessionFilePath && fs.existsSync(session.sessionFilePath)) {
|
|
2686
|
+
await this.consumeSessionFile(session, tracker, opts.onChunk, opts.onDetail);
|
|
2687
|
+
}
|
|
2688
|
+
await activeTurn.callbackChain;
|
|
2689
|
+
session.lastUsedAtMs = Date.now();
|
|
2690
|
+
session.waitForNextSendMs = 150;
|
|
2691
|
+
return {
|
|
2692
|
+
content: (tracker.finalContent || tracker.lastAssistantText).trim(),
|
|
2693
|
+
sessionId: session.sessionId,
|
|
2694
|
+
usage: tracker.usage,
|
|
2695
|
+
rawOutput: activeTurn.rawOutput,
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
finally {
|
|
2699
|
+
syncClaudeSessionCredentialsBackToCanonical(session);
|
|
2700
|
+
session.activeTurn = null;
|
|
2701
|
+
(0, managed_process_registry_1.markTurnFinished)(session.key, 'pty_turn_finished');
|
|
2702
|
+
abortSignal?.removeEventListener('abort', abortHandler);
|
|
568
2703
|
}
|
|
569
|
-
session.lastUsedAtMs = Date.now();
|
|
570
|
-
session.waitForNextSendMs = 400;
|
|
571
|
-
return {
|
|
572
|
-
content: tracker.finalContent.trim(),
|
|
573
|
-
sessionId: session.sessionId,
|
|
574
|
-
usage: tracker.usage,
|
|
575
|
-
};
|
|
576
2704
|
}
|
|
577
|
-
async consumeSessionFile(session, tracker, onDetail) {
|
|
2705
|
+
async consumeSessionFile(session, tracker, onChunk, onDetail) {
|
|
578
2706
|
if (!session.sessionFilePath)
|
|
579
2707
|
return;
|
|
580
2708
|
let stat;
|
|
581
2709
|
try {
|
|
582
|
-
stat = fs.
|
|
2710
|
+
stat = await fs.promises.stat(session.sessionFilePath);
|
|
583
2711
|
}
|
|
584
2712
|
catch {
|
|
585
2713
|
return;
|
|
586
2714
|
}
|
|
587
2715
|
if (stat.size <= session.sessionFileOffset)
|
|
588
2716
|
return;
|
|
589
|
-
|
|
2717
|
+
(0, managed_process_registry_1.markTurnActivity)(session.key, 'transcript_data');
|
|
2718
|
+
let fh = null;
|
|
590
2719
|
try {
|
|
2720
|
+
fh = await fs.promises.open(session.sessionFilePath, 'r');
|
|
591
2721
|
const length = stat.size - session.sessionFileOffset;
|
|
592
2722
|
const buffer = Buffer.alloc(length);
|
|
593
|
-
|
|
2723
|
+
await fh.read(buffer, 0, length, session.sessionFileOffset);
|
|
594
2724
|
session.sessionFileOffset = stat.size;
|
|
595
2725
|
const text = session.sessionFileCarry + buffer.toString('utf8');
|
|
596
2726
|
const lines = text.split('\n');
|
|
@@ -606,14 +2736,118 @@ class LocalCliPtySessionManager {
|
|
|
606
2736
|
catch {
|
|
607
2737
|
continue;
|
|
608
2738
|
}
|
|
609
|
-
await this.applyRecord(session, tracker, record, onDetail);
|
|
2739
|
+
await this.applyRecord(session, tracker, record, onChunk, onDetail);
|
|
610
2740
|
}
|
|
611
2741
|
}
|
|
612
2742
|
finally {
|
|
613
|
-
|
|
2743
|
+
await fh?.close();
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
/**
|
|
2747
|
+
* Fix 12: Watch for child sub-agent .jsonl files and emit curated progress.
|
|
2748
|
+
* Claude sub-agents write to: <parent-session-folder>/subagents/agent-<id>.jsonl
|
|
2749
|
+
* We discover new files by watching the directory (not by deriving from agentId,
|
|
2750
|
+
* which isn't available until the sub-agent completes).
|
|
2751
|
+
*/
|
|
2752
|
+
async consumeChildSubagentFiles(session, tracker, onDetail) {
|
|
2753
|
+
if (!session.sessionFilePath)
|
|
2754
|
+
return;
|
|
2755
|
+
// Derive the subagents directory from the session file path
|
|
2756
|
+
const sessionDir = session.sessionFilePath.replace(/\.jsonl$/, '');
|
|
2757
|
+
const subagentsDir = path.join(sessionDir, 'subagents');
|
|
2758
|
+
if (!fs.existsSync(subagentsDir))
|
|
2759
|
+
return;
|
|
2760
|
+
// Scan for new child files
|
|
2761
|
+
try {
|
|
2762
|
+
const entries = fs.readdirSync(subagentsDir).filter((f) => f.endsWith('.jsonl'));
|
|
2763
|
+
for (const entry of entries) {
|
|
2764
|
+
const childPath = path.join(subagentsDir, entry);
|
|
2765
|
+
if (session.childSnapshot.has(childPath))
|
|
2766
|
+
continue; // Already known from before this turn
|
|
2767
|
+
// Start following this child file if not already
|
|
2768
|
+
if (!session.childFollowers.has(childPath)) {
|
|
2769
|
+
session.childFollowers.set(childPath, { offset: 0, carry: '' });
|
|
2770
|
+
}
|
|
2771
|
+
const follower = session.childFollowers.get(childPath);
|
|
2772
|
+
let stat;
|
|
2773
|
+
try {
|
|
2774
|
+
stat = fs.statSync(childPath);
|
|
2775
|
+
}
|
|
2776
|
+
catch {
|
|
2777
|
+
continue;
|
|
2778
|
+
}
|
|
2779
|
+
if (stat.size <= follower.offset)
|
|
2780
|
+
continue;
|
|
2781
|
+
// Read new content
|
|
2782
|
+
let fh = null;
|
|
2783
|
+
try {
|
|
2784
|
+
fh = await fs.promises.open(childPath, 'r');
|
|
2785
|
+
const length = stat.size - follower.offset;
|
|
2786
|
+
const buffer = Buffer.alloc(length);
|
|
2787
|
+
await fh.read(buffer, 0, length, follower.offset);
|
|
2788
|
+
follower.offset = stat.size;
|
|
2789
|
+
const text = follower.carry + buffer.toString('utf8');
|
|
2790
|
+
const lines = text.split('\n');
|
|
2791
|
+
follower.carry = lines.pop() || '';
|
|
2792
|
+
for (const line of lines) {
|
|
2793
|
+
const trimmed = line.trim();
|
|
2794
|
+
if (!trimmed)
|
|
2795
|
+
continue;
|
|
2796
|
+
let record;
|
|
2797
|
+
try {
|
|
2798
|
+
record = JSON.parse(trimmed);
|
|
2799
|
+
}
|
|
2800
|
+
catch {
|
|
2801
|
+
continue;
|
|
2802
|
+
}
|
|
2803
|
+
// Extract curated child progress
|
|
2804
|
+
if (record?.type === 'assistant' && record?.message?.content) {
|
|
2805
|
+
const blocks = Array.isArray(record.message.content) ? record.message.content : [];
|
|
2806
|
+
for (const block of blocks) {
|
|
2807
|
+
if (!block || typeof block !== 'object')
|
|
2808
|
+
continue;
|
|
2809
|
+
if (block.type === 'text' && typeof block.text === 'string') {
|
|
2810
|
+
const shortText = block.text.trim().slice(0, 150);
|
|
2811
|
+
if (shortText) {
|
|
2812
|
+
const activity = {
|
|
2813
|
+
type: 'subagent_working',
|
|
2814
|
+
label: shortText,
|
|
2815
|
+
key: childPath,
|
|
2816
|
+
timestamp: Date.now(),
|
|
2817
|
+
};
|
|
2818
|
+
await onDetail(`__ACTIVITY__${JSON.stringify(activity)}`);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
else if (block.type === 'tool_use') {
|
|
2822
|
+
const toolName = typeof block.name === 'string' ? block.name : 'tool';
|
|
2823
|
+
const activityLabel = (0, live_activity_1.toolUseToActivityLabel)(toolName);
|
|
2824
|
+
if (activityLabel) {
|
|
2825
|
+
const activity = {
|
|
2826
|
+
type: 'subagent_working',
|
|
2827
|
+
label: activityLabel,
|
|
2828
|
+
key: childPath,
|
|
2829
|
+
timestamp: Date.now(),
|
|
2830
|
+
};
|
|
2831
|
+
await onDetail(`__ACTIVITY__${JSON.stringify(activity)}`);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
finally {
|
|
2839
|
+
await fh?.close();
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
}
|
|
2843
|
+
catch {
|
|
2844
|
+
// subagents dir not readable — skip silently
|
|
614
2845
|
}
|
|
615
2846
|
}
|
|
616
|
-
async applyRecord(session, tracker, record, onDetail) {
|
|
2847
|
+
async applyRecord(session, tracker, record, onChunk, onDetail) {
|
|
2848
|
+
if (session.provider === 'claude-cli' && shouldIgnoreClaudeRecordForCurrentTurn(tracker, record)) {
|
|
2849
|
+
return;
|
|
2850
|
+
}
|
|
617
2851
|
const parsed = session.provider === 'claude-cli'
|
|
618
2852
|
? parseClaudeSessionRecord(record)
|
|
619
2853
|
: parseCodexSessionRecord(record);
|
|
@@ -623,14 +2857,40 @@ class LocalCliPtySessionManager {
|
|
|
623
2857
|
if (parsed.usage) {
|
|
624
2858
|
tracker.usage = parsed.usage;
|
|
625
2859
|
}
|
|
2860
|
+
if (parsed.openedToolUseIds?.length) {
|
|
2861
|
+
for (const toolUseId of parsed.openedToolUseIds) {
|
|
2862
|
+
tracker.pendingToolUseIds.add(toolUseId);
|
|
2863
|
+
}
|
|
2864
|
+
tracker.lastRecordAtMs = Date.now();
|
|
2865
|
+
}
|
|
2866
|
+
if (parsed.resolvedToolUseIds?.length) {
|
|
2867
|
+
for (const toolUseId of parsed.resolvedToolUseIds) {
|
|
2868
|
+
tracker.pendingToolUseIds.delete(toolUseId);
|
|
2869
|
+
}
|
|
2870
|
+
tracker.lastRecordAtMs = Date.now();
|
|
2871
|
+
}
|
|
626
2872
|
if (parsed.detail) {
|
|
2873
|
+
tracker.lastRecordAtMs = Date.now();
|
|
627
2874
|
await emitDetail(tracker, parsed.detail, onDetail);
|
|
628
2875
|
}
|
|
2876
|
+
// Emit structured live activity events as JSON-prefixed detail lines
|
|
2877
|
+
// Frontend can distinguish these from plain text details by the prefix
|
|
2878
|
+
if (parsed.activities && parsed.activities.length > 0 && onDetail) {
|
|
2879
|
+
for (const activity of parsed.activities) {
|
|
2880
|
+
const encoded = `__ACTIVITY__${JSON.stringify(activity)}`;
|
|
2881
|
+
await emitDetail(tracker, encoded, onDetail);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
if (parsed.assistantText) {
|
|
2885
|
+
tracker.lastRecordAtMs = Date.now();
|
|
2886
|
+
await emitAssistantChunk(tracker, parsed.assistantText, onChunk);
|
|
2887
|
+
}
|
|
629
2888
|
if (parsed.finalContent) {
|
|
630
2889
|
tracker.finalContent = parsed.finalContent;
|
|
631
2890
|
tracker.lastAssistantText = parsed.finalContent;
|
|
632
2891
|
}
|
|
633
2892
|
if (parsed.done) {
|
|
2893
|
+
tracker.sawExplicitCompletion = true;
|
|
634
2894
|
tracker.done = true;
|
|
635
2895
|
}
|
|
636
2896
|
}
|
|
@@ -642,4 +2902,161 @@ function getLocalCliPtySessionManager() {
|
|
|
642
2902
|
}
|
|
643
2903
|
return _manager;
|
|
644
2904
|
}
|
|
2905
|
+
async function runLocalCliPtyHealthCheck() {
|
|
2906
|
+
const execDir = path.dirname(process.execPath);
|
|
2907
|
+
const appDir = getAppDirFromExec(execDir);
|
|
2908
|
+
const packagedIndexPath = path.join(appDir, 'resources', 'node-pty-prebuilt', 'lib', 'index.js');
|
|
2909
|
+
const outputChunks = [];
|
|
2910
|
+
try {
|
|
2911
|
+
const ptyModule = loadNodePtyModule();
|
|
2912
|
+
const pty = ptyModule.spawn('cmd.exe', [], {
|
|
2913
|
+
cwd: process.cwd(),
|
|
2914
|
+
cols: 80,
|
|
2915
|
+
rows: 24,
|
|
2916
|
+
env: { ...process.env },
|
|
2917
|
+
useConpty: true,
|
|
2918
|
+
name: 'xterm-color',
|
|
2919
|
+
});
|
|
2920
|
+
const result = await new Promise((resolve) => {
|
|
2921
|
+
let finished = false;
|
|
2922
|
+
const finish = (value) => {
|
|
2923
|
+
if (finished)
|
|
2924
|
+
return;
|
|
2925
|
+
finished = true;
|
|
2926
|
+
try {
|
|
2927
|
+
pty.kill();
|
|
2928
|
+
}
|
|
2929
|
+
catch { }
|
|
2930
|
+
resolve(value);
|
|
2931
|
+
};
|
|
2932
|
+
const timeout = setTimeout(() => {
|
|
2933
|
+
finish({
|
|
2934
|
+
ok: false,
|
|
2935
|
+
output: outputChunks.join(''),
|
|
2936
|
+
error: 'Timed out waiting for PTY echo response',
|
|
2937
|
+
});
|
|
2938
|
+
}, 5000);
|
|
2939
|
+
pty.on('data', (chunk) => {
|
|
2940
|
+
outputChunks.push(chunk);
|
|
2941
|
+
if (outputChunks.join('').includes('PTY_OK')) {
|
|
2942
|
+
clearTimeout(timeout);
|
|
2943
|
+
finish({ ok: true, output: outputChunks.join('') });
|
|
2944
|
+
}
|
|
2945
|
+
});
|
|
2946
|
+
pty.on('exit', () => {
|
|
2947
|
+
clearTimeout(timeout);
|
|
2948
|
+
if (!finished) {
|
|
2949
|
+
finish({
|
|
2950
|
+
ok: false,
|
|
2951
|
+
output: outputChunks.join(''),
|
|
2952
|
+
error: 'PTY session exited before echo response',
|
|
2953
|
+
});
|
|
2954
|
+
}
|
|
2955
|
+
});
|
|
2956
|
+
pty.write('echo PTY_OK\r');
|
|
2957
|
+
pty.write('exit\r');
|
|
2958
|
+
});
|
|
2959
|
+
return {
|
|
2960
|
+
ok: result.ok,
|
|
2961
|
+
execPath: process.execPath,
|
|
2962
|
+
packagedIndexPath,
|
|
2963
|
+
output: result.output,
|
|
2964
|
+
error: result.error,
|
|
2965
|
+
};
|
|
2966
|
+
}
|
|
2967
|
+
catch (error) {
|
|
2968
|
+
return {
|
|
2969
|
+
ok: false,
|
|
2970
|
+
execPath: process.execPath,
|
|
2971
|
+
packagedIndexPath,
|
|
2972
|
+
output: outputChunks.join(''),
|
|
2973
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2974
|
+
};
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
async function runLocalCliPtyTurnHealthCheck(provider, cwd) {
|
|
2978
|
+
const manager = getLocalCliPtySessionManager();
|
|
2979
|
+
const conversationId = `pty-health-${Date.now()}`;
|
|
2980
|
+
const botId = `${provider}-health`;
|
|
2981
|
+
const expected = provider === 'claude-cli' ? 'CLAUDE_PTY_TURN_OK' : 'CODEX_PTY_TURN_OK';
|
|
2982
|
+
try {
|
|
2983
|
+
const result = await manager.runTurn({
|
|
2984
|
+
conversationId,
|
|
2985
|
+
botId,
|
|
2986
|
+
provider,
|
|
2987
|
+
cwd,
|
|
2988
|
+
systemPrompt: '',
|
|
2989
|
+
messages: [
|
|
2990
|
+
{
|
|
2991
|
+
role: 'user',
|
|
2992
|
+
content: `Reply with exactly ${expected} and nothing else.`,
|
|
2993
|
+
},
|
|
2994
|
+
],
|
|
2995
|
+
forceFreshSession: true,
|
|
2996
|
+
timeoutMs: 90_000,
|
|
2997
|
+
});
|
|
2998
|
+
manager.closeSessionByConversation(conversationId, botId);
|
|
2999
|
+
return {
|
|
3000
|
+
ok: result.content.trim() === expected,
|
|
3001
|
+
provider,
|
|
3002
|
+
cwd,
|
|
3003
|
+
content: result.content,
|
|
3004
|
+
error: result.content.trim() === expected
|
|
3005
|
+
? undefined
|
|
3006
|
+
: `Unexpected response content: ${JSON.stringify(result.content)}`,
|
|
3007
|
+
};
|
|
3008
|
+
}
|
|
3009
|
+
catch (error) {
|
|
3010
|
+
manager.closeSessionByConversation(conversationId, botId);
|
|
3011
|
+
return {
|
|
3012
|
+
ok: false,
|
|
3013
|
+
provider,
|
|
3014
|
+
cwd,
|
|
3015
|
+
content: '',
|
|
3016
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
}
|
|
3020
|
+
async function runLocalCliPtyProbe(provider, cwd, prompt, systemPrompt = '') {
|
|
3021
|
+
const manager = getLocalCliPtySessionManager();
|
|
3022
|
+
const conversationId = `pty-probe-${Date.now()}`;
|
|
3023
|
+
const botId = `${provider}-probe`;
|
|
3024
|
+
const startedAt = Date.now();
|
|
3025
|
+
try {
|
|
3026
|
+
const result = await manager.runTurn({
|
|
3027
|
+
conversationId,
|
|
3028
|
+
botId,
|
|
3029
|
+
provider,
|
|
3030
|
+
cwd,
|
|
3031
|
+
systemPrompt,
|
|
3032
|
+
messages: [
|
|
3033
|
+
{
|
|
3034
|
+
role: 'user',
|
|
3035
|
+
content: prompt,
|
|
3036
|
+
},
|
|
3037
|
+
],
|
|
3038
|
+
forceFreshSession: true,
|
|
3039
|
+
timeoutMs: 180_000,
|
|
3040
|
+
});
|
|
3041
|
+
manager.closeSessionByConversation(conversationId, botId);
|
|
3042
|
+
return {
|
|
3043
|
+
ok: result.content.trim().length > 0,
|
|
3044
|
+
provider,
|
|
3045
|
+
cwd,
|
|
3046
|
+
content: result.content,
|
|
3047
|
+
elapsedMs: Date.now() - startedAt,
|
|
3048
|
+
};
|
|
3049
|
+
}
|
|
3050
|
+
catch (error) {
|
|
3051
|
+
manager.closeSessionByConversation(conversationId, botId);
|
|
3052
|
+
return {
|
|
3053
|
+
ok: false,
|
|
3054
|
+
provider,
|
|
3055
|
+
cwd,
|
|
3056
|
+
content: '',
|
|
3057
|
+
elapsedMs: Date.now() - startedAt,
|
|
3058
|
+
error: error instanceof Error ? error.message : String(error),
|
|
3059
|
+
};
|
|
3060
|
+
}
|
|
3061
|
+
}
|
|
645
3062
|
//# sourceMappingURL=local-cli-pty-manager.js.map
|