tailii-host 0.1.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/dist/{broker.d.ts → approval/broker.d.ts} +9 -5
- package/dist/{broker.js → approval/broker.js} +42 -6
- package/dist/approval/broker.js.map +1 -0
- package/dist/{hook.d.ts → approval/hook.d.ts} +48 -7
- package/dist/{hook.js → approval/hook.js} +359 -50
- package/dist/approval/hook.js.map +1 -0
- package/dist/backend/herdr.d.ts +260 -0
- package/dist/backend/herdr.js +838 -0
- package/dist/backend/herdr.js.map +1 -0
- package/dist/backend/sessionBackend.d.ts +81 -0
- package/dist/backend/sessionBackend.js +114 -0
- package/dist/backend/sessionBackend.js.map +1 -0
- package/dist/backend/tmux.d.ts +125 -0
- package/dist/backend/tmux.js +392 -0
- package/dist/backend/tmux.js.map +1 -0
- package/dist/{chatTailController.d.ts → chat/chatTailController.d.ts} +29 -6
- package/dist/{chatTailController.js → chat/chatTailController.js} +94 -18
- package/dist/chat/chatTailController.js.map +1 -0
- package/dist/{imageService.d.ts → chat/imageService.d.ts} +12 -1
- package/dist/{imageService.js → chat/imageService.js} +39 -11
- package/dist/chat/imageService.js.map +1 -0
- package/dist/chat/remoteQuestionMonitor.d.ts +27 -0
- package/dist/chat/remoteQuestionMonitor.js +128 -0
- package/dist/chat/remoteQuestionMonitor.js.map +1 -0
- package/dist/chat/subagentTailer.d.ts +30 -0
- package/dist/chat/subagentTailer.js +704 -0
- package/dist/chat/subagentTailer.js.map +1 -0
- package/dist/chat/subagentTranscript.d.ts +20 -0
- package/dist/chat/subagentTranscript.js +273 -0
- package/dist/chat/subagentTranscript.js.map +1 -0
- package/dist/{transcriptTailer.d.ts → chat/transcriptTailer.d.ts} +12 -1
- package/dist/{transcriptTailer.js → chat/transcriptTailer.js} +183 -10
- package/dist/chat/transcriptTailer.js.map +1 -0
- package/dist/cli.js +24 -10
- package/dist/cli.js.map +1 -1
- package/dist/codex/codexAccountUsage.d.ts +22 -0
- package/dist/codex/codexAccountUsage.js +128 -0
- package/dist/codex/codexAccountUsage.js.map +1 -0
- package/dist/codex/codexAppServer.d.ts +166 -0
- package/dist/codex/codexAppServer.js +1297 -0
- package/dist/codex/codexAppServer.js.map +1 -0
- package/dist/codex/codexNativeTurnController.d.ts +153 -0
- package/dist/codex/codexNativeTurnController.js +673 -0
- package/dist/codex/codexNativeTurnController.js.map +1 -0
- package/dist/codex/codexRolloutTailer.d.ts +100 -0
- package/dist/codex/codexRolloutTailer.js +568 -0
- package/dist/codex/codexRolloutTailer.js.map +1 -0
- package/dist/{codexSessionStore.d.ts → codex/codexSessionStore.d.ts} +10 -2
- package/dist/{codexSessionStore.js → codex/codexSessionStore.js} +184 -15
- package/dist/codex/codexSessionStore.js.map +1 -0
- package/dist/codex/codexSubagentTracker.d.ts +21 -0
- package/dist/codex/codexSubagentTracker.js +281 -0
- package/dist/codex/codexSubagentTracker.js.map +1 -0
- package/dist/codex/codexToolActivity.d.ts +69 -0
- package/dist/codex/codexToolActivity.js +425 -0
- package/dist/codex/codexToolActivity.js.map +1 -0
- package/dist/{codexUsage.d.ts → codex/codexUsage.d.ts} +1 -1
- package/dist/{codexUsage.js → codex/codexUsage.js} +19 -26
- package/dist/codex/codexUsage.js.map +1 -0
- package/dist/commands/doctor.d.ts +65 -0
- package/dist/commands/doctor.js +425 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/{hookSettings.d.ts → commands/hookSettings.d.ts} +14 -1
- package/dist/{hookSettings.js → commands/hookSettings.js} +96 -12
- package/dist/commands/hookSettings.js.map +1 -0
- package/dist/{kick.js → commands/kick.js} +16 -1
- package/dist/commands/kick.js.map +1 -0
- package/dist/{launch.d.ts → commands/launch.d.ts} +67 -5
- package/dist/commands/launch.js +716 -0
- package/dist/commands/launch.js.map +1 -0
- package/dist/commands/legacyHomeMigration.js.map +1 -0
- package/dist/{pairingCode.d.ts → commands/pairingCode.d.ts} +29 -2
- package/dist/{pairingCode.js → commands/pairingCode.js} +120 -11
- package/dist/commands/pairingCode.js.map +1 -0
- package/dist/{pushTokenCommand.d.ts → commands/pushTokenCommand.d.ts} +1 -1
- package/dist/{pushTokenCommand.js → commands/pushTokenCommand.js} +2 -2
- package/dist/commands/pushTokenCommand.js.map +1 -0
- package/dist/commands/selfUpdate.d.ts +71 -0
- package/dist/commands/selfUpdate.js +536 -0
- package/dist/commands/selfUpdate.js.map +1 -0
- package/dist/{setup.d.ts → commands/setup.d.ts} +17 -6
- package/dist/{setup.js → commands/setup.js} +133 -75
- package/dist/commands/setup.js.map +1 -0
- package/dist/engine/context.d.ts +145 -0
- package/dist/engine/context.js +184 -0
- package/dist/engine/context.js.map +1 -0
- package/dist/engine/engine.d.ts +121 -0
- package/dist/engine/engine.js +808 -0
- package/dist/engine/engine.js.map +1 -0
- package/dist/engine/handlers/accountUsage.d.ts +53 -0
- package/dist/engine/handlers/accountUsage.js +203 -0
- package/dist/engine/handlers/accountUsage.js.map +1 -0
- package/dist/engine/handlers/claudeModels.d.ts +2 -0
- package/dist/engine/handlers/claudeModels.js +23 -0
- package/dist/engine/handlers/claudeModels.js.map +1 -0
- package/dist/engine/handlers/codex.d.ts +2 -0
- package/dist/engine/handlers/codex.js +97 -0
- package/dist/engine/handlers/codex.js.map +1 -0
- package/dist/engine/handlers/conversation.d.ts +2 -0
- package/dist/engine/handlers/conversation.js +259 -0
- package/dist/engine/handlers/conversation.js.map +1 -0
- package/dist/engine/handlers/core.d.ts +2 -0
- package/dist/engine/handlers/core.js +66 -0
- package/dist/engine/handlers/core.js.map +1 -0
- package/dist/engine/handlers/git.d.ts +2 -0
- package/dist/engine/handlers/git.js +127 -0
- package/dist/engine/handlers/git.js.map +1 -0
- package/dist/engine/handlers/index.d.ts +2 -0
- package/dist/engine/handlers/index.js +34 -0
- package/dist/engine/handlers/index.js.map +1 -0
- package/dist/engine/handlers/mode.d.ts +2 -0
- package/dist/engine/handlers/mode.js +194 -0
- package/dist/engine/handlers/mode.js.map +1 -0
- package/dist/engine/handlers/officialApps.d.ts +2 -0
- package/dist/engine/handlers/officialApps.js +79 -0
- package/dist/engine/handlers/officialApps.js.map +1 -0
- package/dist/engine/handlers/preview.d.ts +2 -0
- package/dist/engine/handlers/preview.js +59 -0
- package/dist/engine/handlers/preview.js.map +1 -0
- package/dist/engine/handlers/selfUpdate.d.ts +2 -0
- package/dist/engine/handlers/selfUpdate.js +52 -0
- package/dist/engine/handlers/selfUpdate.js.map +1 -0
- package/dist/engine/handlers/session.d.ts +2 -0
- package/dist/engine/handlers/session.js +385 -0
- package/dist/engine/handlers/session.js.map +1 -0
- package/dist/engine/handlers/usage.d.ts +2 -0
- package/dist/engine/handlers/usage.js +96 -0
- package/dist/engine/handlers/usage.js.map +1 -0
- package/dist/engine/handlers/workspace.d.ts +2 -0
- package/dist/engine/handlers/workspace.js +110 -0
- package/dist/engine/handlers/workspace.js.map +1 -0
- package/dist/engine/slashCommands.d.ts +3 -0
- package/dist/engine/slashCommands.js +211 -0
- package/dist/engine/slashCommands.js.map +1 -0
- package/dist/hub/engineRelaySocket.d.ts +58 -0
- package/dist/hub/engineRelaySocket.js +161 -0
- package/dist/hub/engineRelaySocket.js.map +1 -0
- package/dist/hub/hubClient.d.ts +28 -0
- package/dist/hub/hubClient.js +187 -0
- package/dist/hub/hubClient.js.map +1 -0
- package/dist/hub/hubDaemon.d.ts +69 -0
- package/dist/hub/hubDaemon.js +466 -0
- package/dist/hub/hubDaemon.js.map +1 -0
- package/dist/hub/hubProtocol.d.ts +161 -0
- package/dist/hub/hubProtocol.js +379 -0
- package/dist/hub/hubProtocol.js.map +1 -0
- package/dist/hub/panePreviewPump.d.ts +65 -0
- package/dist/hub/panePreviewPump.js +187 -0
- package/dist/hub/panePreviewPump.js.map +1 -0
- package/dist/hub/questionInjection.d.ts +19 -0
- package/dist/hub/questionInjection.js +126 -0
- package/dist/hub/questionInjection.js.map +1 -0
- package/dist/hub/reaper.d.ts +74 -0
- package/dist/hub/reaper.js +333 -0
- package/dist/hub/reaper.js.map +1 -0
- package/dist/hub/sessionHub.d.ts +203 -0
- package/dist/hub/sessionHub.js +1862 -0
- package/dist/hub/sessionHub.js.map +1 -0
- package/dist/protocol/common.d.ts +18 -0
- package/dist/protocol/common.js +78 -0
- package/dist/protocol/common.js.map +1 -0
- package/dist/protocol/decode.d.ts +3 -0
- package/dist/protocol/decode.js +1058 -0
- package/dist/protocol/decode.js.map +1 -0
- package/dist/protocol/encode.d.ts +3 -0
- package/dist/protocol/encode.js +84 -0
- package/dist/protocol/encode.js.map +1 -0
- package/dist/protocol/messages.d.ts +1025 -0
- package/dist/protocol/messages.js +8 -0
- package/dist/protocol/messages.js.map +1 -0
- package/dist/protocol.d.ts +4 -329
- package/dist/protocol.js +13 -531
- package/dist/protocol.js.map +1 -1
- package/dist/push/apnsConfigStore.js.map +1 -0
- package/dist/push/apnsJwtProvider.js.map +1 -0
- package/dist/{apnsSender.js.map → push/apnsSender.js.map} +1 -1
- package/dist/{approvalPushNotifier.js → push/approvalPushNotifier.js} +1 -1
- package/dist/push/approvalPushNotifier.js.map +1 -0
- package/dist/push/deviceTokenStore.js.map +1 -0
- package/dist/{pushTypes.js → push/pushTypes.js} +1 -1
- package/dist/push/pushTypes.js.map +1 -0
- package/dist/push/sendLog.js.map +1 -0
- package/dist/services/accountIdentity.d.ts +44 -0
- package/dist/services/accountIdentity.js +184 -0
- package/dist/services/accountIdentity.js.map +1 -0
- package/dist/services/claudeCredentialRefresh.d.ts +30 -0
- package/dist/services/claudeCredentialRefresh.js +345 -0
- package/dist/services/claudeCredentialRefresh.js.map +1 -0
- package/dist/services/claudeModelCatalog.d.ts +21 -0
- package/dist/services/claudeModelCatalog.js +181 -0
- package/dist/services/claudeModelCatalog.js.map +1 -0
- package/dist/{dirLister.d.ts → services/dirLister.d.ts} +9 -4
- package/dist/services/dirLister.js +169 -0
- package/dist/services/dirLister.js.map +1 -0
- package/dist/services/fileService.d.ts +10 -0
- package/dist/services/fileService.js +123 -0
- package/dist/services/fileService.js.map +1 -0
- package/dist/services/gitService.d.ts +79 -0
- package/dist/services/gitService.js +648 -0
- package/dist/services/gitService.js.map +1 -0
- package/dist/services/hostVersions.d.ts +38 -0
- package/dist/services/hostVersions.js +120 -0
- package/dist/services/hostVersions.js.map +1 -0
- package/dist/services/officialApps.d.ts +148 -0
- package/dist/services/officialApps.js +1079 -0
- package/dist/services/officialApps.js.map +1 -0
- package/dist/services/planUsageFetcher.d.ts +119 -0
- package/dist/services/planUsageFetcher.js +357 -0
- package/dist/services/planUsageFetcher.js.map +1 -0
- package/dist/services/previewServer.d.ts +31 -0
- package/dist/services/previewServer.js +233 -0
- package/dist/services/previewServer.js.map +1 -0
- package/dist/services/quicGateway.d.ts +143 -0
- package/dist/services/quicGateway.js +292 -0
- package/dist/services/quicGateway.js.map +1 -0
- package/dist/services/serveService.d.ts +50 -0
- package/dist/services/serveService.js +302 -0
- package/dist/services/serveService.js.map +1 -0
- package/dist/services/usageAggregator.js.map +1 -0
- package/dist/sessions/claudeSessionStore.d.ts +80 -0
- package/dist/sessions/claudeSessionStore.js +477 -0
- package/dist/sessions/claudeSessionStore.js.map +1 -0
- package/dist/sessions/heartbeat.d.ts +24 -0
- package/dist/sessions/heartbeat.js +86 -0
- package/dist/sessions/heartbeat.js.map +1 -0
- package/dist/sessions/liveSessionJoin.d.ts +20 -0
- package/dist/sessions/liveSessionJoin.js +80 -0
- package/dist/sessions/liveSessionJoin.js.map +1 -0
- package/dist/sessions/sessionActivityProvider.d.ts +14 -0
- package/dist/sessions/sessionActivityProvider.js +57 -0
- package/dist/sessions/sessionActivityProvider.js.map +1 -0
- package/dist/{sessionListService.d.ts → sessions/sessionListService.d.ts} +3 -3
- package/dist/{sessionListService.js → sessions/sessionListService.js} +23 -6
- package/dist/sessions/sessionListService.js.map +1 -0
- package/dist/{sessionMetadataStore.d.ts → sessions/sessionMetadataStore.d.ts} +18 -0
- package/dist/{sessionMetadataStore.js → sessions/sessionMetadataStore.js} +36 -1
- package/dist/sessions/sessionMetadataStore.js.map +1 -0
- package/dist/{sessionSearch.d.ts → sessions/sessionSearch.d.ts} +1 -1
- package/dist/sessions/sessionSearch.js.map +1 -0
- package/dist/shared/hostDisplayName.d.ts +11 -0
- package/dist/shared/hostDisplayName.js +50 -0
- package/dist/shared/hostDisplayName.js.map +1 -0
- package/dist/{lineWriter.d.ts → shared/lineWriter.d.ts} +1 -1
- package/dist/{lineWriter.js → shared/lineWriter.js} +1 -1
- package/dist/shared/lineWriter.js.map +1 -0
- package/dist/{observationLog.js.map → shared/observationLog.js.map} +1 -1
- package/dist/{paths.d.ts → shared/paths.d.ts} +7 -0
- package/dist/{paths.js → shared/paths.js} +9 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/permissionMode.d.ts +10 -0
- package/dist/shared/permissionMode.js +51 -0
- package/dist/shared/permissionMode.js.map +1 -0
- package/dist/shared/skillInjection.d.ts +9 -0
- package/dist/shared/skillInjection.js +16 -0
- package/dist/shared/skillInjection.js.map +1 -0
- package/dist/shared/sleep.js.map +1 -0
- package/dist/{socketPath.d.ts → shared/socketPath.d.ts} +4 -0
- package/dist/{socketPath.js → shared/socketPath.js} +22 -0
- package/dist/shared/socketPath.js.map +1 -0
- package/dist/shared/version.js.map +1 -0
- package/package.json +13 -7
- package/dist/apnsConfigStore.js.map +0 -1
- package/dist/apnsJwtProvider.js.map +0 -1
- package/dist/approvalPushNotifier.js.map +0 -1
- package/dist/broker.js.map +0 -1
- package/dist/chatTailController.js.map +0 -1
- package/dist/claudeSessionStore.d.ts +0 -15
- package/dist/claudeSessionStore.js +0 -201
- package/dist/claudeSessionStore.js.map +0 -1
- package/dist/codexRolloutTailer.d.ts +0 -56
- package/dist/codexRolloutTailer.js +0 -303
- package/dist/codexRolloutTailer.js.map +0 -1
- package/dist/codexSessionStore.js.map +0 -1
- package/dist/codexUsage.js.map +0 -1
- package/dist/deviceTokenStore.js.map +0 -1
- package/dist/dirLister.js +0 -98
- package/dist/dirLister.js.map +0 -1
- package/dist/doctor.d.ts +0 -36
- package/dist/doctor.js +0 -162
- package/dist/doctor.js.map +0 -1
- package/dist/engine.d.ts +0 -85
- package/dist/engine.js +0 -962
- package/dist/engine.js.map +0 -1
- package/dist/hook.js.map +0 -1
- package/dist/hookSettings.js.map +0 -1
- package/dist/idleReaper.d.ts +0 -13
- package/dist/idleReaper.js +0 -30
- package/dist/idleReaper.js.map +0 -1
- package/dist/imageService.js.map +0 -1
- package/dist/kick.js.map +0 -1
- package/dist/launch.js +0 -396
- package/dist/launch.js.map +0 -1
- package/dist/legacyHomeMigration.js.map +0 -1
- package/dist/lineWriter.js.map +0 -1
- package/dist/mdnsAdvertiser.d.ts +0 -46
- package/dist/mdnsAdvertiser.js +0 -268
- package/dist/mdnsAdvertiser.js.map +0 -1
- package/dist/pairingCode.js.map +0 -1
- package/dist/paths.js.map +0 -1
- package/dist/permissionMode.d.ts +0 -8
- package/dist/permissionMode.js +0 -32
- package/dist/permissionMode.js.map +0 -1
- package/dist/planUsageFetcher.d.ts +0 -35
- package/dist/planUsageFetcher.js +0 -172
- package/dist/planUsageFetcher.js.map +0 -1
- package/dist/pushTokenCommand.js.map +0 -1
- package/dist/pushTypes.js.map +0 -1
- package/dist/sendLog.js.map +0 -1
- package/dist/sessionActivityProvider.d.ts +0 -7
- package/dist/sessionActivityProvider.js +0 -44
- package/dist/sessionActivityProvider.js.map +0 -1
- package/dist/sessionIdleTracker.d.ts +0 -19
- package/dist/sessionIdleTracker.js +0 -42
- package/dist/sessionIdleTracker.js.map +0 -1
- package/dist/sessionListService.js.map +0 -1
- package/dist/sessionMetadataStore.js.map +0 -1
- package/dist/sessionSearch.js.map +0 -1
- package/dist/setup.js.map +0 -1
- package/dist/sleep.js.map +0 -1
- package/dist/socketPath.js.map +0 -1
- package/dist/subagentTailer.d.ts +0 -15
- package/dist/subagentTailer.js +0 -399
- package/dist/subagentTailer.js.map +0 -1
- package/dist/tmux.d.ts +0 -62
- package/dist/tmux.js +0 -163
- package/dist/tmux.js.map +0 -1
- package/dist/transcriptTailer.js.map +0 -1
- package/dist/usageAggregator.js.map +0 -1
- package/dist/version.js.map +0 -1
- /package/dist/{kick.d.ts → commands/kick.d.ts} +0 -0
- /package/dist/{legacyHomeMigration.d.ts → commands/legacyHomeMigration.d.ts} +0 -0
- /package/dist/{legacyHomeMigration.js → commands/legacyHomeMigration.js} +0 -0
- /package/dist/{apnsConfigStore.d.ts → push/apnsConfigStore.d.ts} +0 -0
- /package/dist/{apnsConfigStore.js → push/apnsConfigStore.js} +0 -0
- /package/dist/{apnsJwtProvider.d.ts → push/apnsJwtProvider.d.ts} +0 -0
- /package/dist/{apnsJwtProvider.js → push/apnsJwtProvider.js} +0 -0
- /package/dist/{apnsSender.d.ts → push/apnsSender.d.ts} +0 -0
- /package/dist/{apnsSender.js → push/apnsSender.js} +0 -0
- /package/dist/{approvalPushNotifier.d.ts → push/approvalPushNotifier.d.ts} +0 -0
- /package/dist/{deviceTokenStore.d.ts → push/deviceTokenStore.d.ts} +0 -0
- /package/dist/{deviceTokenStore.js → push/deviceTokenStore.js} +0 -0
- /package/dist/{pushTypes.d.ts → push/pushTypes.d.ts} +0 -0
- /package/dist/{sendLog.d.ts → push/sendLog.d.ts} +0 -0
- /package/dist/{sendLog.js → push/sendLog.js} +0 -0
- /package/dist/{usageAggregator.d.ts → services/usageAggregator.d.ts} +0 -0
- /package/dist/{usageAggregator.js → services/usageAggregator.js} +0 -0
- /package/dist/{sessionSearch.js → sessions/sessionSearch.js} +0 -0
- /package/dist/{observationLog.d.ts → shared/observationLog.d.ts} +0 -0
- /package/dist/{observationLog.js → shared/observationLog.js} +0 -0
- /package/dist/{sleep.d.ts → shared/sleep.d.ts} +0 -0
- /package/dist/{sleep.js → shared/sleep.js} +0 -0
- /package/dist/{version.d.ts → shared/version.d.ts} +0 -0
- /package/dist/{version.js → shared/version.js} +0 -0
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
// codexAppServer.ts
|
|
2
|
+
// Tailii host が共有 Codex App Server を再利用・起動し、thread ID を先に確定する最小クライアント。
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import * as fs from "node:fs";
|
|
6
|
+
import * as net from "node:net";
|
|
7
|
+
import * as os from "node:os";
|
|
8
|
+
import * as path from "node:path";
|
|
9
|
+
import WebSocket from "ws";
|
|
10
|
+
import { ensureDirectory0700 } from "../shared/paths.js";
|
|
11
|
+
const THREAD_TITLE_MODEL = "gpt-5.6-luna";
|
|
12
|
+
const THREAD_TITLE_PROMPT_MAX_LENGTH = 2_000;
|
|
13
|
+
const THREAD_TITLE_MAX_LENGTH = 36;
|
|
14
|
+
const THREAD_TITLE_FALLBACK_MAX_LENGTH = 60;
|
|
15
|
+
const DEFAULT_TITLE_GENERATION_TIMEOUT_MS = 30_000;
|
|
16
|
+
const THREAD_TITLE_OUTPUT_SCHEMA = {
|
|
17
|
+
type: "object",
|
|
18
|
+
properties: {
|
|
19
|
+
title: { type: "string", minLength: 1, maxLength: THREAD_TITLE_MAX_LENGTH },
|
|
20
|
+
description: { type: "string", minLength: 1 },
|
|
21
|
+
},
|
|
22
|
+
required: ["title", "description"],
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
};
|
|
25
|
+
const THREAD_TITLE_CONFIG = {
|
|
26
|
+
model_reasoning_effort: "low",
|
|
27
|
+
"features.enable_fanout": false,
|
|
28
|
+
"features.hooks": false,
|
|
29
|
+
"features.multi_agent": false,
|
|
30
|
+
"features.multi_agent_v2": false,
|
|
31
|
+
"features.plugins": false,
|
|
32
|
+
"features.tool_suggest": false,
|
|
33
|
+
"features.apps": false,
|
|
34
|
+
apps: {
|
|
35
|
+
_default: {
|
|
36
|
+
enabled: false,
|
|
37
|
+
destructive_enabled: false,
|
|
38
|
+
open_world_enabled: false,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
web_search: "disabled",
|
|
42
|
+
};
|
|
43
|
+
/** Unix socket 上のWebSocketを使うJSON-RPC接続。 */
|
|
44
|
+
class WebSocketCodexAppServerConnection {
|
|
45
|
+
socket;
|
|
46
|
+
requestTimeoutMs;
|
|
47
|
+
nextId = 1;
|
|
48
|
+
pending = new Map();
|
|
49
|
+
notificationHandlers = new Set();
|
|
50
|
+
serverRequestHandlers = new Set();
|
|
51
|
+
disconnectHandlers = new Set();
|
|
52
|
+
disconnected = false;
|
|
53
|
+
constructor(socket, requestTimeoutMs) {
|
|
54
|
+
this.socket = socket;
|
|
55
|
+
this.requestTimeoutMs = requestTimeoutMs;
|
|
56
|
+
socket.on("message", (data) => this.onMessage(data));
|
|
57
|
+
socket.on("close", () => this.disconnect(new Error("Codex App Server connection closed")));
|
|
58
|
+
socket.on("error", (error) => this.disconnect(error));
|
|
59
|
+
}
|
|
60
|
+
static connect(socketPath, requestTimeoutMs = 5_000) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const socket = new WebSocket("ws://localhost/", {
|
|
63
|
+
createConnection: () => net.createConnection(socketPath),
|
|
64
|
+
// CodexのUnix control socketはWebSocket拡張を交渉しない。ws既定の
|
|
65
|
+
// permessage-deflateを送るとRust側がhandshakeを拒否する。
|
|
66
|
+
perMessageDeflate: false,
|
|
67
|
+
});
|
|
68
|
+
const onError = (error) => {
|
|
69
|
+
socket.removeListener("open", onOpen);
|
|
70
|
+
reject(error);
|
|
71
|
+
};
|
|
72
|
+
const onOpen = () => {
|
|
73
|
+
socket.removeListener("error", onError);
|
|
74
|
+
resolve(new WebSocketCodexAppServerConnection(socket, requestTimeoutMs));
|
|
75
|
+
};
|
|
76
|
+
socket.once("error", onError);
|
|
77
|
+
socket.once("open", onOpen);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async initialize() {
|
|
81
|
+
await this.request("initialize", {
|
|
82
|
+
clientInfo: { name: "tailii_host", title: "Tailii Host", version: "0.1.1" },
|
|
83
|
+
capabilities: { experimentalApi: true },
|
|
84
|
+
});
|
|
85
|
+
this.notify("initialized", undefined);
|
|
86
|
+
}
|
|
87
|
+
request(method, params, timeoutMs) {
|
|
88
|
+
const id = this.nextId;
|
|
89
|
+
this.nextId += 1;
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
this.pending.delete(id);
|
|
93
|
+
reject(new Error(`Codex App Server request timed out: ${method}`));
|
|
94
|
+
}, timeoutMs ?? this.requestTimeoutMs);
|
|
95
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
96
|
+
this.socket.send(JSON.stringify({ id, method, params }));
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
close() {
|
|
100
|
+
this.socket.close();
|
|
101
|
+
}
|
|
102
|
+
onNotification(handler) {
|
|
103
|
+
this.notificationHandlers.add(handler);
|
|
104
|
+
return () => this.notificationHandlers.delete(handler);
|
|
105
|
+
}
|
|
106
|
+
onServerRequest(handler) {
|
|
107
|
+
this.serverRequestHandlers.add(handler);
|
|
108
|
+
return () => this.serverRequestHandlers.delete(handler);
|
|
109
|
+
}
|
|
110
|
+
onDisconnect(handler) {
|
|
111
|
+
this.disconnectHandlers.add(handler);
|
|
112
|
+
return () => this.disconnectHandlers.delete(handler);
|
|
113
|
+
}
|
|
114
|
+
respond(id, result) {
|
|
115
|
+
this.socket.send(JSON.stringify({ id, result }));
|
|
116
|
+
}
|
|
117
|
+
respondError(id, code, message) {
|
|
118
|
+
this.socket.send(JSON.stringify({ id, error: { code, message } }));
|
|
119
|
+
}
|
|
120
|
+
notify(method, params) {
|
|
121
|
+
const message = { method };
|
|
122
|
+
if (params !== undefined)
|
|
123
|
+
message["params"] = params;
|
|
124
|
+
this.socket.send(JSON.stringify(message));
|
|
125
|
+
}
|
|
126
|
+
onMessage(data) {
|
|
127
|
+
let parsed;
|
|
128
|
+
try {
|
|
129
|
+
parsed = JSON.parse(data.toString());
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
135
|
+
return;
|
|
136
|
+
const message = parsed;
|
|
137
|
+
const id = message["id"];
|
|
138
|
+
const method = message["method"];
|
|
139
|
+
// 双方向 JSON-RPC では client request と server request の id 名前空間が別なので、
|
|
140
|
+
// `method` の有無を先に見る。数値 id だけで response と判定すると衝突し得る。
|
|
141
|
+
if (typeof method === "string") {
|
|
142
|
+
const notification = {
|
|
143
|
+
method,
|
|
144
|
+
params: message["params"],
|
|
145
|
+
};
|
|
146
|
+
if (typeof id === "number" || typeof id === "string") {
|
|
147
|
+
for (const handler of this.serverRequestHandlers)
|
|
148
|
+
handler({ ...notification, id });
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
for (const handler of this.notificationHandlers)
|
|
152
|
+
handler(notification);
|
|
153
|
+
}
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (typeof id !== "number")
|
|
157
|
+
return;
|
|
158
|
+
const pending = this.pending.get(id);
|
|
159
|
+
if (!pending)
|
|
160
|
+
return;
|
|
161
|
+
this.pending.delete(id);
|
|
162
|
+
clearTimeout(pending.timer);
|
|
163
|
+
const error = message["error"];
|
|
164
|
+
if (typeof error === "object" && error !== null) {
|
|
165
|
+
const text = error["message"];
|
|
166
|
+
pending.reject(new Error(typeof text === "string" ? text : "Codex App Server request failed"));
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
pending.resolve(message["result"]);
|
|
170
|
+
}
|
|
171
|
+
rejectPending(error) {
|
|
172
|
+
for (const pending of this.pending.values()) {
|
|
173
|
+
clearTimeout(pending.timer);
|
|
174
|
+
pending.reject(error);
|
|
175
|
+
}
|
|
176
|
+
this.pending.clear();
|
|
177
|
+
}
|
|
178
|
+
disconnect(error) {
|
|
179
|
+
if (this.disconnected)
|
|
180
|
+
return;
|
|
181
|
+
this.disconnected = true;
|
|
182
|
+
this.rejectPending(error);
|
|
183
|
+
for (const handler of this.disconnectHandlers)
|
|
184
|
+
handler(error);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** 1 thread を購読し、Tailii から turn を開始する長寿命 App Server 接続。 */
|
|
188
|
+
export class CodexAppServerThread {
|
|
189
|
+
threadId;
|
|
190
|
+
initialItems;
|
|
191
|
+
initialActiveTurnId;
|
|
192
|
+
liveSubscriptionReady;
|
|
193
|
+
connection;
|
|
194
|
+
cwd;
|
|
195
|
+
/** 読み取り専用の readiness 確認だけは、共有 App Server の一時的な混雑を自動回復する。 */
|
|
196
|
+
// 初回5秒 + 再試行2秒に抑え、上位の15秒 Hub RPC に turn/start の予算を残す。
|
|
197
|
+
static activeTurnReadMaxAttempts = 2;
|
|
198
|
+
static activeTurnReadRetryTimeoutMs = 2_000;
|
|
199
|
+
/** null=未判定、true=stable ID対応、false=旧App ServerなのでIDなしsteerへ固定。 */
|
|
200
|
+
supportsSteerClientUserMessageId = null;
|
|
201
|
+
constructor(threadId, initialItems, initialActiveTurnId,
|
|
202
|
+
/**
|
|
203
|
+
* この接続で thread/resume(または作成元 bootstrap)が成功し、live 通知を受け取れるか。
|
|
204
|
+
* rollout 未生成の新規 thread は resume に失敗するため false。呼び出し側は初回 turn の
|
|
205
|
+
* 表示を rollout tail へフォールバックし、通知が来ない接続を live 権威にしない。
|
|
206
|
+
*/
|
|
207
|
+
liveSubscriptionReady, connection, cwd) {
|
|
208
|
+
this.threadId = threadId;
|
|
209
|
+
this.initialItems = initialItems;
|
|
210
|
+
this.initialActiveTurnId = initialActiveTurnId;
|
|
211
|
+
this.liveSubscriptionReady = liveSubscriptionReady;
|
|
212
|
+
this.connection = connection;
|
|
213
|
+
this.cwd = cwd;
|
|
214
|
+
}
|
|
215
|
+
async startTurn(text, clientUserMessageId, effort, sandbox, approvalPolicy) {
|
|
216
|
+
if (text.length === 0)
|
|
217
|
+
throw new Error("Codex turn text must not be empty");
|
|
218
|
+
const inherited = sandbox === null || approvalPolicy === null
|
|
219
|
+
? await readCodexSecurityDefaults(this.connection, this.cwd)
|
|
220
|
+
: null;
|
|
221
|
+
const effectiveSandbox = sandbox === null ? inherited?.sandbox ?? null : sandbox;
|
|
222
|
+
const effectiveApprovalPolicy = approvalPolicy === null ? inherited?.approvalPolicy ?? null : approvalPolicy;
|
|
223
|
+
const effectiveApprovalsReviewer = approvalPolicy === null ? inherited?.approvalsReviewer ?? null : null;
|
|
224
|
+
const response = await this.connection.request("turn/start", {
|
|
225
|
+
threadId: this.threadId,
|
|
226
|
+
input: [{ type: "text", text }],
|
|
227
|
+
...(clientUserMessageId ? { clientUserMessageId } : {}),
|
|
228
|
+
...(effort ? { effort } : {}),
|
|
229
|
+
...(effectiveSandbox ? { sandboxPolicy: codexSandboxPolicy(effectiveSandbox) } : {}),
|
|
230
|
+
...(effectiveApprovalPolicy ? { approvalPolicy: effectiveApprovalPolicy } : {}),
|
|
231
|
+
...(effectiveApprovalsReviewer ? { approvalsReviewer: effectiveApprovalsReviewer } : {}),
|
|
232
|
+
});
|
|
233
|
+
const turnId = extractTurnId(response);
|
|
234
|
+
if (turnId === null)
|
|
235
|
+
throw new Error("Codex App Server turn/start response omitted turn.id");
|
|
236
|
+
return turnId;
|
|
237
|
+
}
|
|
238
|
+
async readActiveTurnId() {
|
|
239
|
+
for (let attempt = 1;; attempt += 1) {
|
|
240
|
+
try {
|
|
241
|
+
const response = await this.connection.request("thread/read", {
|
|
242
|
+
threadId: this.threadId,
|
|
243
|
+
includeTurns: true,
|
|
244
|
+
}, attempt === 1 ? undefined : CodexAppServerThread.activeTurnReadRetryTimeoutMs);
|
|
245
|
+
return extractActiveTurnId(response);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
// 新規 thread の最初の rollout 行がまだ無い間は、idle とは断定できない。
|
|
249
|
+
// controller は手元の ID を維持するか、初回 turn/start へ進む。
|
|
250
|
+
if (isUnmaterializedThreadError(error, this.threadId))
|
|
251
|
+
return undefined;
|
|
252
|
+
// thread/read は副作用が無い。未 materialize thread の初回送信と
|
|
253
|
+
// 別セッションの処理が共有 App Server 上で重なった場合のみ、
|
|
254
|
+
// 5秒 timeout を有界リトライする。turn/start / turn/steer は受理済みか
|
|
255
|
+
// 不明なため、この自動リトライの対象にしない。
|
|
256
|
+
if (!isRequestTimeout(error, "thread/read") ||
|
|
257
|
+
attempt >= CodexAppServerThread.activeTurnReadMaxAttempts) {
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
/** 親履歴で欠落する sub-agent の終端状態を、子 thread 自身から読み直す。 */
|
|
264
|
+
async readThreadStatus(threadId) {
|
|
265
|
+
for (let attempt = 1; attempt <= CodexAppServerThread.activeTurnReadMaxAttempts; attempt += 1) {
|
|
266
|
+
try {
|
|
267
|
+
const response = objectRecord(await this.connection.request("thread/read", { threadId, includeTurns: false }, CodexAppServerThread.activeTurnReadRetryTimeoutMs));
|
|
268
|
+
const thread = objectRecord(response?.["thread"]);
|
|
269
|
+
if (thread === null || thread["status"] === undefined)
|
|
270
|
+
return undefined;
|
|
271
|
+
const timestampMs = codexTimestampMs(thread["createdAt"] ?? thread["updatedAt"] ?? thread["recencyAt"]);
|
|
272
|
+
return {
|
|
273
|
+
status: thread["status"],
|
|
274
|
+
...(timestampMs === undefined ? {} : { timestampMs }),
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (isRequestTimeout(error, "thread/read") &&
|
|
279
|
+
attempt < CodexAppServerThread.activeTurnReadMaxAttempts) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
// workflow snapshot の補完経路なので、子 rollout の一時的不在で親会話を開けなくしない。
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
async steerTurn(turnId, text, clientUserMessageId) {
|
|
289
|
+
if (text.length === 0)
|
|
290
|
+
throw new Error("Codex turn text must not be empty");
|
|
291
|
+
const params = {
|
|
292
|
+
threadId: this.threadId,
|
|
293
|
+
input: [{ type: "text", text }],
|
|
294
|
+
expectedTurnId: turnId,
|
|
295
|
+
};
|
|
296
|
+
if (!clientUserMessageId || this.supportsSteerClientUserMessageId === false) {
|
|
297
|
+
await this.connection.request("turn/steer", params);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
try {
|
|
301
|
+
await this.connection.request("turn/steer", { ...params, clientUserMessageId });
|
|
302
|
+
this.supportsSteerClientUserMessageId = true;
|
|
303
|
+
}
|
|
304
|
+
catch (error) {
|
|
305
|
+
// codex-cli 0.144系にはこの additive field が無い。未知フィールド拒否だけは
|
|
306
|
+
// 同じactive turnへIDなしで再試行し、別turnを開始するcontroller fallbackへ落とさない。
|
|
307
|
+
// timeout/切断など「受理済みか不明」な失敗は再試行せず、そのまま上位へ返す。
|
|
308
|
+
if (!isUnsupportedSteerClientUserMessageIdError(error))
|
|
309
|
+
throw error;
|
|
310
|
+
await this.connection.request("turn/steer", params);
|
|
311
|
+
this.supportsSteerClientUserMessageId = false;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
async interruptTurn(turnId) {
|
|
315
|
+
await this.connection.request("turn/interrupt", {
|
|
316
|
+
threadId: this.threadId,
|
|
317
|
+
turnId,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
close() {
|
|
321
|
+
this.connection.close();
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
function isUnsupportedSteerClientUserMessageIdError(error) {
|
|
325
|
+
if (!(error instanceof Error))
|
|
326
|
+
return false;
|
|
327
|
+
const message = error.message.toLowerCase();
|
|
328
|
+
if (!message.includes("clientusermessageid"))
|
|
329
|
+
return false;
|
|
330
|
+
return [
|
|
331
|
+
"unknown field",
|
|
332
|
+
"unknown parameter",
|
|
333
|
+
"unexpected field",
|
|
334
|
+
"invalid params",
|
|
335
|
+
"invalid parameter",
|
|
336
|
+
"not supported",
|
|
337
|
+
"unsupported",
|
|
338
|
+
].some((marker) => message.includes(marker));
|
|
339
|
+
}
|
|
340
|
+
function isRequestTimeout(error, method) {
|
|
341
|
+
return error instanceof Error &&
|
|
342
|
+
error.message === `Codex App Server request timed out: ${method}`;
|
|
343
|
+
}
|
|
344
|
+
async function readCodexSecurityDefaults(connection, cwd) {
|
|
345
|
+
let rawResponse;
|
|
346
|
+
try {
|
|
347
|
+
rawResponse = await connection.request("config/read", {
|
|
348
|
+
includeLayers: false,
|
|
349
|
+
cwd,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
// 旧 App Server や制限付きで起動された常駐プロセスでは、現在の thread 設定へ委ねる。
|
|
354
|
+
return { approvalPolicy: null, approvalsReviewer: null, sandbox: null };
|
|
355
|
+
}
|
|
356
|
+
const response = objectRecord(rawResponse);
|
|
357
|
+
const config = objectRecord(response?.["config"]);
|
|
358
|
+
const rawApprovalPolicy = config?.["approval_policy"];
|
|
359
|
+
const approvalPolicy = rawApprovalPolicy === "untrusted" ||
|
|
360
|
+
rawApprovalPolicy === "on-request" ||
|
|
361
|
+
rawApprovalPolicy === "never"
|
|
362
|
+
? rawApprovalPolicy
|
|
363
|
+
: null;
|
|
364
|
+
const reviewer = config?.["approvals_reviewer"];
|
|
365
|
+
const approvalsReviewer = reviewer === "user" || reviewer === "auto_review" || reviewer === "guardian_subagent"
|
|
366
|
+
? reviewer
|
|
367
|
+
: null;
|
|
368
|
+
const rawSandbox = config?.["sandbox_mode"];
|
|
369
|
+
const sandbox = rawSandbox === "read-only" ||
|
|
370
|
+
rawSandbox === "workspace-write" ||
|
|
371
|
+
rawSandbox === "danger-full-access"
|
|
372
|
+
? rawSandbox
|
|
373
|
+
: null;
|
|
374
|
+
return { approvalPolicy, approvalsReviewer, sandbox };
|
|
375
|
+
}
|
|
376
|
+
function objectRecord(value) {
|
|
377
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
378
|
+
? value
|
|
379
|
+
: null;
|
|
380
|
+
}
|
|
381
|
+
function codexSandboxPolicy(sandbox) {
|
|
382
|
+
switch (sandbox) {
|
|
383
|
+
case "read-only": return { type: "readOnly", networkAccess: false };
|
|
384
|
+
case "workspace-write": return {
|
|
385
|
+
type: "workspaceWrite", writableRoots: [], networkAccess: false,
|
|
386
|
+
excludeTmpdirEnvVar: false, excludeSlashTmp: false,
|
|
387
|
+
};
|
|
388
|
+
case "danger-full-access": return { type: "dangerFullAccess" };
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
/** 1 CODEX_HOMEにつき1つのApp Serverを共有し、論理threadを作成する。 */
|
|
392
|
+
export class CodexAppServerManager {
|
|
393
|
+
socketPath;
|
|
394
|
+
remoteEndpoint;
|
|
395
|
+
codexHome;
|
|
396
|
+
codexPath;
|
|
397
|
+
connect;
|
|
398
|
+
launch;
|
|
399
|
+
pollIntervalMs;
|
|
400
|
+
startupTimeoutMs;
|
|
401
|
+
titleGenerationTimeoutMs;
|
|
402
|
+
startupLockPath;
|
|
403
|
+
/** 最初の turn 前(rollout 未作成)の thread を生存させる作成元購読。openThread が引き継ぐ。 */
|
|
404
|
+
bootstrapConnections = new Map();
|
|
405
|
+
constructor(options = {}) {
|
|
406
|
+
this.codexHome = options.codexHome ?? path.join(os.homedir(), ".codex");
|
|
407
|
+
this.codexPath = options.codexPath ?? "codex";
|
|
408
|
+
this.socketPath =
|
|
409
|
+
options.socketPath ?? path.join(this.codexHome, "app-server-control", "app-server-control.sock");
|
|
410
|
+
this.remoteEndpoint = options.remoteEndpoint ?? "unix://";
|
|
411
|
+
this.connect =
|
|
412
|
+
options.connect ?? ((socketPath, requestTimeoutMs) => WebSocketCodexAppServerConnection.connect(socketPath, requestTimeoutMs));
|
|
413
|
+
this.launch = options.launch ?? defaultLaunch;
|
|
414
|
+
this.pollIntervalMs = options.pollIntervalMs ?? 100;
|
|
415
|
+
this.startupTimeoutMs = options.startupTimeoutMs ?? 10_000;
|
|
416
|
+
this.titleGenerationTimeoutMs =
|
|
417
|
+
options.titleGenerationTimeoutMs ?? DEFAULT_TITLE_GENERATION_TIMEOUT_MS;
|
|
418
|
+
this.startupLockPath = path.join(path.dirname(this.socketPath), "tailii-start.lock");
|
|
419
|
+
}
|
|
420
|
+
/** serverを再利用または排他的に起動し、利用可能になるまで待つ。 */
|
|
421
|
+
async ensureRunning() {
|
|
422
|
+
if (await this.probe())
|
|
423
|
+
return;
|
|
424
|
+
ensureDirectory0700(path.dirname(this.startupLockPath));
|
|
425
|
+
let lockFd = null;
|
|
426
|
+
try {
|
|
427
|
+
lockFd = this.acquireStartupLock();
|
|
428
|
+
if (lockFd === null) {
|
|
429
|
+
if (await this.waitUntilReady())
|
|
430
|
+
return;
|
|
431
|
+
throw new Error("Codex App Server startup is already in progress but did not become ready");
|
|
432
|
+
}
|
|
433
|
+
// lock取得待ちの間に別processが起動済みかもしれないので再確認する。
|
|
434
|
+
if (await this.probe())
|
|
435
|
+
return;
|
|
436
|
+
await this.launch(this.codexPath, ["app-server", "--listen", this.remoteEndpoint], { ...process.env, CODEX_HOME: this.codexHome });
|
|
437
|
+
if (!(await this.waitUntilReady())) {
|
|
438
|
+
throw new Error("Codex App Server failed to become ready");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
if (lockFd !== null) {
|
|
443
|
+
try {
|
|
444
|
+
fs.closeSync(lockFd);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// 無視。
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
fs.unlinkSync(this.startupLockPath);
|
|
451
|
+
}
|
|
452
|
+
catch {
|
|
453
|
+
// 無視。
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
/** 稼働中の server にだけ接続する。停止中・不達時は起動せず null を返す。 */
|
|
459
|
+
async connectIfRunning(requestTimeoutMs = 2_000) {
|
|
460
|
+
let connection = null;
|
|
461
|
+
try {
|
|
462
|
+
connection = await this.connect(this.socketPath, requestTimeoutMs);
|
|
463
|
+
await connection.initialize();
|
|
464
|
+
return connection;
|
|
465
|
+
}
|
|
466
|
+
catch {
|
|
467
|
+
connection?.close();
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
/** 稼働中の server から thread/list を最大200件取得する。停止中なら null。 */
|
|
472
|
+
async listThreads(maxThreads = 200) {
|
|
473
|
+
const connection = await this.connectIfRunning();
|
|
474
|
+
if (connection === null)
|
|
475
|
+
return null;
|
|
476
|
+
try {
|
|
477
|
+
const threads = [];
|
|
478
|
+
const seenCursors = new Set();
|
|
479
|
+
let cursor = null;
|
|
480
|
+
do {
|
|
481
|
+
const limit = Math.min(100, maxThreads - threads.length);
|
|
482
|
+
if (limit <= 0)
|
|
483
|
+
break;
|
|
484
|
+
const response = await connection.request("thread/list", {
|
|
485
|
+
limit,
|
|
486
|
+
sortKey: "updated_at",
|
|
487
|
+
sortDirection: "desc",
|
|
488
|
+
...(cursor !== null ? { cursor } : {}),
|
|
489
|
+
});
|
|
490
|
+
const page = parseThreadListPage(response);
|
|
491
|
+
threads.push(...page.data.slice(0, limit));
|
|
492
|
+
cursor = page.nextCursor;
|
|
493
|
+
if (cursor !== null) {
|
|
494
|
+
if (seenCursors.has(cursor))
|
|
495
|
+
throw new Error("Codex App Server thread/list cursor loop");
|
|
496
|
+
seenCursors.add(cursor);
|
|
497
|
+
}
|
|
498
|
+
} while (cursor !== null && threads.length < maxThreads);
|
|
499
|
+
return threads;
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
connection.close();
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Codex App Server の正式な会話名を設定する。
|
|
507
|
+
*
|
|
508
|
+
* loaded thread と persisted rollout の両方を同じ API で更新できる。
|
|
509
|
+
* schema上は空文字もstringだが、実App Serverは `thread name must not be empty` で拒否する。
|
|
510
|
+
*/
|
|
511
|
+
async setThreadName(threadId, name) {
|
|
512
|
+
const normalizedThreadId = threadId.trim();
|
|
513
|
+
if (normalizedThreadId.length === 0) {
|
|
514
|
+
throw new Error("Codex thread id must not be empty");
|
|
515
|
+
}
|
|
516
|
+
if (name.trim().length === 0) {
|
|
517
|
+
throw new Error("Codex thread name must not be empty");
|
|
518
|
+
}
|
|
519
|
+
await this.ensureRunning();
|
|
520
|
+
const connection = await this.connect(this.socketPath);
|
|
521
|
+
try {
|
|
522
|
+
await connection.initialize();
|
|
523
|
+
await connection.request("thread/name/set", {
|
|
524
|
+
threadId: normalizedThreadId,
|
|
525
|
+
name,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
connection.close();
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
/** 現在共有中の App Server から Remote Control 状態を読む。 */
|
|
533
|
+
async remoteControlStatus() {
|
|
534
|
+
return this.remoteControlRequest("remoteControl/status/read", {}, parseRemoteControlStatus);
|
|
535
|
+
}
|
|
536
|
+
/** 現在共有中の App Server 自体で Remote Control を有効化する。 */
|
|
537
|
+
async enableRemoteControl() {
|
|
538
|
+
return this.remoteControlRequest("remoteControl/enable", {}, parseRemoteControlStatus);
|
|
539
|
+
}
|
|
540
|
+
/** 現在共有中の App Server 自体で Remote Control を無効化する。 */
|
|
541
|
+
async disableRemoteControl() {
|
|
542
|
+
return this.remoteControlRequest("remoteControl/disable", {}, parseRemoteControlStatus);
|
|
543
|
+
}
|
|
544
|
+
/** 短命なペアリングコードを発行する。呼び出し側は URL 化後も永続化しない。 */
|
|
545
|
+
async startRemoteControlPairing() {
|
|
546
|
+
return this.remoteControlRequest("remoteControl/pairing/start", { manualCode: true }, parseRemoteControlPairing);
|
|
547
|
+
}
|
|
548
|
+
/** 新規threadをApp Serverで作り、TUI起動前に安定IDを返す。 */
|
|
549
|
+
async startThread(options) {
|
|
550
|
+
if (!path.isAbsolute(options.cwd))
|
|
551
|
+
throw new Error("Codex thread cwd must be absolute");
|
|
552
|
+
await this.ensureRunning();
|
|
553
|
+
const connection = await this.connect(this.socketPath);
|
|
554
|
+
let succeeded = false;
|
|
555
|
+
try {
|
|
556
|
+
await connection.initialize();
|
|
557
|
+
const params = {
|
|
558
|
+
cwd: options.cwd,
|
|
559
|
+
};
|
|
560
|
+
if (options.model)
|
|
561
|
+
params["model"] = options.model;
|
|
562
|
+
if (options.sandbox)
|
|
563
|
+
params["sandbox"] = options.sandbox;
|
|
564
|
+
const response = await connection.request("thread/start", params);
|
|
565
|
+
const threadId = extractThreadId(response);
|
|
566
|
+
if (threadId === null)
|
|
567
|
+
throw new Error("Codex App Server thread/start response omitted thread.id");
|
|
568
|
+
// turn が1件も無い thread は rollout がまだ存在せず、作成元購読を閉じると直後の
|
|
569
|
+
// remote TUI/thread/resume が "no rollout found" になる。openThread まで保持して引き継ぐ。
|
|
570
|
+
this.bootstrapConnections.get(threadId)?.close();
|
|
571
|
+
this.bootstrapConnections.set(threadId, connection);
|
|
572
|
+
succeeded = true;
|
|
573
|
+
return threadId;
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
if (!succeeded)
|
|
577
|
+
connection.close();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* Codex Desktop と同じ短命 thread で初回 prompt の短いタイトルを生成し、対象 thread へ保存する。
|
|
582
|
+
* `gpt-5.6-luna` を優先し、App Server が利用不能と判断した場合は既定モデルへ委ねる。
|
|
583
|
+
*/
|
|
584
|
+
async generateThreadTitle(options) {
|
|
585
|
+
if (!path.isAbsolute(options.cwd))
|
|
586
|
+
throw new Error("Codex thread cwd must be absolute");
|
|
587
|
+
await this.ensureRunning();
|
|
588
|
+
const connection = await this.connect(this.socketPath);
|
|
589
|
+
let ephemeralThreadId = null;
|
|
590
|
+
try {
|
|
591
|
+
await connection.initialize();
|
|
592
|
+
const target = await readThreadTitleTarget(connection, options.threadId, options.prompt);
|
|
593
|
+
if (target.name !== null)
|
|
594
|
+
return { title: null, source: null };
|
|
595
|
+
const prompt = target.prompt.trim().slice(0, THREAD_TITLE_PROMPT_MAX_LENGTH);
|
|
596
|
+
if (prompt.length === 0)
|
|
597
|
+
return { title: null, source: null };
|
|
598
|
+
const fallbackTitle = normalizeFallbackThreadTitle(prompt);
|
|
599
|
+
let generatedTitle = null;
|
|
600
|
+
try {
|
|
601
|
+
const response = await connection.request("thread/start", {
|
|
602
|
+
model: THREAD_TITLE_MODEL,
|
|
603
|
+
modelProvider: null,
|
|
604
|
+
allowProviderModelFallback: true,
|
|
605
|
+
cwd: options.cwd,
|
|
606
|
+
approvalPolicy: "never",
|
|
607
|
+
permissions: ":read-only",
|
|
608
|
+
runtimeWorkspaceRoots: [],
|
|
609
|
+
config: THREAD_TITLE_CONFIG,
|
|
610
|
+
personality: null,
|
|
611
|
+
ephemeral: true,
|
|
612
|
+
threadSource: "system",
|
|
613
|
+
experimentalRawEvents: false,
|
|
614
|
+
dynamicTools: null,
|
|
615
|
+
serviceTier: null,
|
|
616
|
+
});
|
|
617
|
+
ephemeralThreadId = extractThreadId(response);
|
|
618
|
+
if (ephemeralThreadId === null) {
|
|
619
|
+
throw new Error("Codex title thread/start response omitted thread.id");
|
|
620
|
+
}
|
|
621
|
+
generatedTitle = await runThreadTitleTurn(connection, ephemeralThreadId, buildThreadTitlePrompt(prompt), this.titleGenerationTimeoutMs);
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
// Desktop と同様、生成経路の失敗は初回 prompt の先頭60文字へフォールバックする。
|
|
625
|
+
}
|
|
626
|
+
const title = generatedTitle ?? fallbackTitle;
|
|
627
|
+
const source = generatedTitle === null ? "promptFallback" : "model";
|
|
628
|
+
// 生成中の手動 rename や別 client の命名を上書きしない。
|
|
629
|
+
if (await readThreadName(connection, options.threadId) !== null) {
|
|
630
|
+
return { title: null, source: null };
|
|
631
|
+
}
|
|
632
|
+
await connection.request("thread/name/set", {
|
|
633
|
+
threadId: options.threadId,
|
|
634
|
+
name: title,
|
|
635
|
+
});
|
|
636
|
+
return { title, source };
|
|
637
|
+
}
|
|
638
|
+
finally {
|
|
639
|
+
if (ephemeralThreadId !== null) {
|
|
640
|
+
try {
|
|
641
|
+
await connection.request("thread/unsubscribe", { threadId: ephemeralThreadId });
|
|
642
|
+
}
|
|
643
|
+
catch {
|
|
644
|
+
// 短命接続を閉じれば通常会話には影響しない。
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
connection.close();
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/** ログイン中アカウントで利用可能なCodexモデルをApp Server APIから取得する。 */
|
|
651
|
+
async listModels() {
|
|
652
|
+
await this.ensureRunning();
|
|
653
|
+
const connection = await this.connect(this.socketPath);
|
|
654
|
+
try {
|
|
655
|
+
await connection.initialize();
|
|
656
|
+
const rawModels = [];
|
|
657
|
+
const seenCursors = new Set();
|
|
658
|
+
let cursor = null;
|
|
659
|
+
do {
|
|
660
|
+
const response = await connection.request("model/list", {
|
|
661
|
+
limit: 100,
|
|
662
|
+
includeHidden: false,
|
|
663
|
+
...(cursor !== null ? { cursor } : {}),
|
|
664
|
+
});
|
|
665
|
+
const page = parseModelListPage(response);
|
|
666
|
+
rawModels.push(...page.data);
|
|
667
|
+
cursor = page.nextCursor;
|
|
668
|
+
if (cursor !== null) {
|
|
669
|
+
if (seenCursors.has(cursor))
|
|
670
|
+
throw new Error("Codex App Server model/list cursor loop");
|
|
671
|
+
seenCursors.add(cursor);
|
|
672
|
+
}
|
|
673
|
+
} while (cursor !== null);
|
|
674
|
+
// model/list がAPIキャッシュを更新した後に読む。App Serverの公開Model型には
|
|
675
|
+
// context windowが無いため、同じCodex API応答の永続キャッシュから実効値を結合する。
|
|
676
|
+
const cachedWindows = readModelContextWindows(path.join(this.codexHome, "models_cache.json"));
|
|
677
|
+
const seenModels = new Set();
|
|
678
|
+
return rawModels.flatMap((raw) => {
|
|
679
|
+
if (raw["hidden"] === true)
|
|
680
|
+
return [];
|
|
681
|
+
const model = stringValue(raw["model"]) ?? stringValue(raw["id"]);
|
|
682
|
+
if (model === null || seenModels.has(model))
|
|
683
|
+
return [];
|
|
684
|
+
seenModels.add(model);
|
|
685
|
+
const directWindow = positiveInteger(raw["contextWindow"]);
|
|
686
|
+
const contextWindow = directWindow ?? cachedWindows.get(model);
|
|
687
|
+
const supportedReasoningEfforts = Array.isArray(raw["supportedReasoningEfforts"])
|
|
688
|
+
? raw["supportedReasoningEfforts"].flatMap((value) => {
|
|
689
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
690
|
+
return [];
|
|
691
|
+
const effort = stringValue(value["reasoningEffort"]);
|
|
692
|
+
return effort === null ? [] : [effort];
|
|
693
|
+
})
|
|
694
|
+
: [];
|
|
695
|
+
const defaultReasoningEffort = stringValue(raw["defaultReasoningEffort"]);
|
|
696
|
+
return [{
|
|
697
|
+
id: model,
|
|
698
|
+
displayName: stringValue(raw["displayName"]) ?? model,
|
|
699
|
+
description: stringValue(raw["description"]) ?? "",
|
|
700
|
+
...(contextWindow !== undefined ? { contextWindow } : {}),
|
|
701
|
+
...(defaultReasoningEffort !== null ? { defaultReasoningEffort } : {}),
|
|
702
|
+
...(supportedReasoningEfforts.length > 0 ? { supportedReasoningEfforts } : {}),
|
|
703
|
+
isDefault: raw["isDefault"] === true,
|
|
704
|
+
}];
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
finally {
|
|
708
|
+
connection.close();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/** 既存 thread を購読する長寿命接続を開く。turn と native approval はこの接続を流れる。 */
|
|
712
|
+
async openThread(options) {
|
|
713
|
+
await this.ensureRunning();
|
|
714
|
+
const bootstrap = this.bootstrapConnections.get(options.threadId) ?? null;
|
|
715
|
+
if (bootstrap !== null)
|
|
716
|
+
this.bootstrapConnections.delete(options.threadId);
|
|
717
|
+
const connection = bootstrap ?? (await this.connect(this.socketPath));
|
|
718
|
+
const removeNotification = connection.onNotification((notification) => {
|
|
719
|
+
options.onNotification?.(notification);
|
|
720
|
+
});
|
|
721
|
+
const removeServerRequest = connection.onServerRequest((request) => {
|
|
722
|
+
const handler = options.onServerRequest;
|
|
723
|
+
if (handler === undefined) {
|
|
724
|
+
connection.respondError(request.id, -32601, `unsupported server request: ${request.method}`);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
void handler(request).then((result) => connection.respond(request.id, result), (error) => connection.respondError(request.id, -32000, String(error)));
|
|
728
|
+
});
|
|
729
|
+
const removeDisconnect = connection.onDisconnect?.((error) => options.onDisconnect?.(error)) ?? (() => { });
|
|
730
|
+
try {
|
|
731
|
+
let initialItems = [];
|
|
732
|
+
let initialActiveTurnId = null;
|
|
733
|
+
let liveSubscriptionReady = bootstrap !== null;
|
|
734
|
+
if (bootstrap === null) {
|
|
735
|
+
await connection.initialize();
|
|
736
|
+
try {
|
|
737
|
+
const response = await connection.request("thread/resume", {
|
|
738
|
+
threadId: options.threadId,
|
|
739
|
+
// Hub の rollout backfill と live 通知の厳密な境界に履歴 item ID を使う。
|
|
740
|
+
excludeTurns: false,
|
|
741
|
+
});
|
|
742
|
+
initialItems = extractThreadItems(response);
|
|
743
|
+
initialActiveTurnId = extractActiveTurnId(response);
|
|
744
|
+
liveSubscriptionReady = true;
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
// thread/start から最初の user turn まで rollout は未作成で、別接続からの
|
|
748
|
+
// thread/resume は "no rollout found" になる。ただし共有 App Server 内の
|
|
749
|
+
// live thread 自体は存在し、この接続から turn/start を直接送れば materialize
|
|
750
|
+
// できる。engine と Session Hub は別プロセスなので、この場合だけ履歴ゼロとして
|
|
751
|
+
// 接続を維持し、最初の turn/start へ進ませる。
|
|
752
|
+
if (!isUnmaterializedThreadError(error, options.threadId))
|
|
753
|
+
throw error;
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
return new CodexAppServerThread(options.threadId, initialItems, initialActiveTurnId, liveSubscriptionReady, connection, options.cwd ?? null);
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
removeNotification();
|
|
760
|
+
removeServerRequest();
|
|
761
|
+
removeDisconnect();
|
|
762
|
+
connection.close();
|
|
763
|
+
throw error;
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
async probe() {
|
|
767
|
+
const connection = await this.connectIfRunning();
|
|
768
|
+
if (connection === null)
|
|
769
|
+
return false;
|
|
770
|
+
connection.close();
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
async remoteControlRequest(method, params, parse) {
|
|
774
|
+
try {
|
|
775
|
+
await this.ensureRunning();
|
|
776
|
+
const connection = await this.connectIfRunning(22_000);
|
|
777
|
+
if (connection === null)
|
|
778
|
+
return null;
|
|
779
|
+
try {
|
|
780
|
+
return parse(await connection.request(method, params));
|
|
781
|
+
}
|
|
782
|
+
finally {
|
|
783
|
+
connection.close();
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
catch {
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
async waitUntilReady() {
|
|
791
|
+
const deadline = Date.now() + this.startupTimeoutMs;
|
|
792
|
+
do {
|
|
793
|
+
if (await this.probe())
|
|
794
|
+
return true;
|
|
795
|
+
await new Promise((resolve) => setTimeout(resolve, this.pollIntervalMs));
|
|
796
|
+
} while (Date.now() <= deadline);
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
/** lockが他の生存processに所有されていればnull。死んだownerのlockは1回だけ回収する。 */
|
|
800
|
+
acquireStartupLock() {
|
|
801
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
802
|
+
try {
|
|
803
|
+
const fd = fs.openSync(this.startupLockPath, "wx", 0o600);
|
|
804
|
+
fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, createdAt: Date.now() }));
|
|
805
|
+
return fd;
|
|
806
|
+
}
|
|
807
|
+
catch (error) {
|
|
808
|
+
if (error.code !== "EEXIST")
|
|
809
|
+
throw error;
|
|
810
|
+
const ownerPid = readLockPid(this.startupLockPath);
|
|
811
|
+
if (ownerPid !== null && processIsAlive(ownerPid))
|
|
812
|
+
return null;
|
|
813
|
+
try {
|
|
814
|
+
fs.unlinkSync(this.startupLockPath);
|
|
815
|
+
}
|
|
816
|
+
catch {
|
|
817
|
+
return null;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
return null;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
async function readThreadName(connection, threadId) {
|
|
825
|
+
let response;
|
|
826
|
+
try {
|
|
827
|
+
response = objectRecord(await connection.request("thread/read", {
|
|
828
|
+
threadId,
|
|
829
|
+
includeTurns: false,
|
|
830
|
+
}));
|
|
831
|
+
}
|
|
832
|
+
catch (error) {
|
|
833
|
+
// turn/start が成功していても rollout の最初の行が flush されるまでは
|
|
834
|
+
// thread/read が空ファイルとして失敗する。thread/list のメタデータはこの間も
|
|
835
|
+
// name を返せるため、手動 rename の上書き防止を維持したまま命名を続行する。
|
|
836
|
+
if (!isUnmaterializedThreadError(error, threadId))
|
|
837
|
+
throw error;
|
|
838
|
+
return readThreadNameFromList(connection, threadId);
|
|
839
|
+
}
|
|
840
|
+
const thread = objectRecord(response?.["thread"]);
|
|
841
|
+
if (thread === null)
|
|
842
|
+
throw new Error("Codex App Server thread/read response omitted thread");
|
|
843
|
+
const name = thread["name"];
|
|
844
|
+
if (name === null || name === undefined)
|
|
845
|
+
return null;
|
|
846
|
+
if (typeof name !== "string") {
|
|
847
|
+
throw new Error("Codex App Server thread/read returned an invalid thread name");
|
|
848
|
+
}
|
|
849
|
+
return name.trim().length > 0 ? name.trim() : null;
|
|
850
|
+
}
|
|
851
|
+
async function readThreadTitleTarget(connection, threadId, fallbackPrompt) {
|
|
852
|
+
let response;
|
|
853
|
+
try {
|
|
854
|
+
response = await connection.request("thread/read", {
|
|
855
|
+
threadId,
|
|
856
|
+
includeTurns: true,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
// 作成直後は初回 turn/start 応答後もしばらく rollout が空の場合がある。
|
|
861
|
+
// controller から渡された最初の入力を使い、既存 name だけ thread/list で保護する。
|
|
862
|
+
if (!isUnmaterializedThreadError(error, threadId))
|
|
863
|
+
throw error;
|
|
864
|
+
return {
|
|
865
|
+
name: await readThreadNameFromList(connection, threadId),
|
|
866
|
+
prompt: fallbackPrompt,
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
const record = objectRecord(response);
|
|
870
|
+
const thread = objectRecord(record?.["thread"]);
|
|
871
|
+
if (thread === null)
|
|
872
|
+
throw new Error("Codex App Server thread/read response omitted thread");
|
|
873
|
+
const rawName = thread["name"];
|
|
874
|
+
if (rawName !== null && rawName !== undefined && typeof rawName !== "string") {
|
|
875
|
+
throw new Error("Codex App Server thread/read returned an invalid thread name");
|
|
876
|
+
}
|
|
877
|
+
const name = typeof rawName === "string" && rawName.trim().length > 0 ? rawName.trim() : null;
|
|
878
|
+
const firstUserPrompt = extractThreadItems(response)
|
|
879
|
+
.map(extractUserMessageText)
|
|
880
|
+
.find((value) => value !== null && value.trim().length > 0);
|
|
881
|
+
return {
|
|
882
|
+
name,
|
|
883
|
+
prompt: firstUserPrompt ?? fallbackPrompt,
|
|
884
|
+
};
|
|
885
|
+
}
|
|
886
|
+
async function readThreadNameFromList(connection, threadId) {
|
|
887
|
+
const page = parseThreadListPage(await connection.request("thread/list", {
|
|
888
|
+
limit: 100,
|
|
889
|
+
sortKey: "updated_at",
|
|
890
|
+
sortDirection: "desc",
|
|
891
|
+
}));
|
|
892
|
+
const thread = page.data.find((candidate) => candidate.id === threadId);
|
|
893
|
+
if (thread === undefined) {
|
|
894
|
+
throw new Error(`Codex App Server thread/list omitted target thread ${threadId}`);
|
|
895
|
+
}
|
|
896
|
+
return thread.name;
|
|
897
|
+
}
|
|
898
|
+
function extractUserMessageText(item) {
|
|
899
|
+
if (item["type"] !== "userMessage")
|
|
900
|
+
return null;
|
|
901
|
+
const content = item["content"];
|
|
902
|
+
if (!Array.isArray(content))
|
|
903
|
+
return null;
|
|
904
|
+
const text = content.flatMap((part) => {
|
|
905
|
+
const record = objectRecord(part);
|
|
906
|
+
if (record === null || record["type"] !== "text")
|
|
907
|
+
return [];
|
|
908
|
+
return typeof record["text"] === "string" ? [record["text"]] : [];
|
|
909
|
+
}).join("\n").trim();
|
|
910
|
+
return text.length > 0 ? text : null;
|
|
911
|
+
}
|
|
912
|
+
async function runThreadTitleTurn(connection, threadId, prompt, timeoutMs) {
|
|
913
|
+
let turnId = null;
|
|
914
|
+
let responseText = "";
|
|
915
|
+
let settled = false;
|
|
916
|
+
let resolveCompletion = () => { };
|
|
917
|
+
const completion = new Promise((resolve) => {
|
|
918
|
+
resolveCompletion = resolve;
|
|
919
|
+
});
|
|
920
|
+
const finish = (title) => {
|
|
921
|
+
if (settled)
|
|
922
|
+
return;
|
|
923
|
+
settled = true;
|
|
924
|
+
resolveCompletion(title);
|
|
925
|
+
};
|
|
926
|
+
const removeNotification = connection.onNotification((notification) => {
|
|
927
|
+
const params = objectRecord(notification.params);
|
|
928
|
+
if (params?.["threadId"] !== threadId)
|
|
929
|
+
return;
|
|
930
|
+
const notificationTurnId = stringValue(params["turnId"]) ?? stringValue(objectRecord(params["turn"])?.["id"]);
|
|
931
|
+
if (turnId !== null && notificationTurnId !== null && notificationTurnId !== turnId)
|
|
932
|
+
return;
|
|
933
|
+
if (notification.method === "turn/started") {
|
|
934
|
+
if (notificationTurnId !== null)
|
|
935
|
+
turnId = notificationTurnId;
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
if (notification.method === "item/agentMessage/delta") {
|
|
939
|
+
const delta = params["delta"];
|
|
940
|
+
if (typeof delta === "string")
|
|
941
|
+
responseText += delta;
|
|
942
|
+
return;
|
|
943
|
+
}
|
|
944
|
+
if (notification.method === "item/completed") {
|
|
945
|
+
const item = objectRecord(params["item"]);
|
|
946
|
+
if (item?.["type"] === "agentMessage" && typeof item["text"] === "string") {
|
|
947
|
+
responseText = item["text"];
|
|
948
|
+
}
|
|
949
|
+
return;
|
|
950
|
+
}
|
|
951
|
+
if (notification.method === "error") {
|
|
952
|
+
finish(null);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
if (notification.method === "turn/completed") {
|
|
956
|
+
const turn = objectRecord(params["turn"]);
|
|
957
|
+
finish(turn?.["status"] === "completed" ? parseThreadTitleResult(responseText) : null);
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
const timer = setTimeout(() => {
|
|
961
|
+
if (turnId !== null) {
|
|
962
|
+
void connection.request("turn/interrupt", { threadId, turnId }).catch(() => { });
|
|
963
|
+
}
|
|
964
|
+
finish(null);
|
|
965
|
+
}, Math.max(1, timeoutMs));
|
|
966
|
+
try {
|
|
967
|
+
const response = await connection.request("turn/start", {
|
|
968
|
+
threadId,
|
|
969
|
+
clientUserMessageId: randomUUID(),
|
|
970
|
+
input: [{ type: "text", text: prompt, text_elements: [] }],
|
|
971
|
+
cwd: null,
|
|
972
|
+
approvalPolicy: null,
|
|
973
|
+
permissions: ":read-only",
|
|
974
|
+
runtimeWorkspaceRoots: [],
|
|
975
|
+
model: null,
|
|
976
|
+
effort: null,
|
|
977
|
+
serviceTier: null,
|
|
978
|
+
summary: "none",
|
|
979
|
+
personality: null,
|
|
980
|
+
outputSchema: THREAD_TITLE_OUTPUT_SCHEMA,
|
|
981
|
+
collaborationMode: null,
|
|
982
|
+
});
|
|
983
|
+
turnId = extractTurnId(response);
|
|
984
|
+
if (turnId === null)
|
|
985
|
+
throw new Error("Codex title turn/start response omitted turn.id");
|
|
986
|
+
return await completion;
|
|
987
|
+
}
|
|
988
|
+
finally {
|
|
989
|
+
clearTimeout(timer);
|
|
990
|
+
removeNotification();
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function buildThreadTitlePrompt(prompt) {
|
|
994
|
+
return [
|
|
995
|
+
"You are a helpful assistant that creates a short UI title for a coding task.",
|
|
996
|
+
`Return a concise title of at most ${THREAD_TITLE_MAX_LENGTH} characters and a compact, search-oriented description of at most 100 characters.`,
|
|
997
|
+
"Write both the title and description in the same primary language as the user prompt.",
|
|
998
|
+
"Do not translate them into another language. Preserve product names and identifiers in their original form.",
|
|
999
|
+
"Start change requests with a precise action word appropriate to the user's language.",
|
|
1000
|
+
"Keep the title under five words when possible.",
|
|
1001
|
+
"Make questions clearly express their intent in the user's language.",
|
|
1002
|
+
"Reuse an explicit short title from the user when one is provided.",
|
|
1003
|
+
"Preserve ticket references such as ABC-123 verbatim.",
|
|
1004
|
+
"Do not use quotes, Markdown, formatting characters, or trailing punctuation.",
|
|
1005
|
+
"Do not answer the prompt or solve the task. Only produce the structured title and description.",
|
|
1006
|
+
"",
|
|
1007
|
+
"User prompt:",
|
|
1008
|
+
prompt,
|
|
1009
|
+
].join("\n");
|
|
1010
|
+
}
|
|
1011
|
+
function parseThreadTitleResult(raw) {
|
|
1012
|
+
let parsed;
|
|
1013
|
+
try {
|
|
1014
|
+
parsed = JSON.parse(raw.trim());
|
|
1015
|
+
}
|
|
1016
|
+
catch {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
const result = objectRecord(parsed);
|
|
1020
|
+
const title = result?.["title"];
|
|
1021
|
+
const description = result?.["description"];
|
|
1022
|
+
if (typeof title !== "string" ||
|
|
1023
|
+
title.length === 0 ||
|
|
1024
|
+
title.length > THREAD_TITLE_MAX_LENGTH ||
|
|
1025
|
+
typeof description !== "string" ||
|
|
1026
|
+
description.trim().length === 0) {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
return sanitizeGeneratedThreadTitle(title);
|
|
1030
|
+
}
|
|
1031
|
+
function sanitizeGeneratedThreadTitle(raw) {
|
|
1032
|
+
let title = raw
|
|
1033
|
+
.replaceAll("\r\n", "\n")
|
|
1034
|
+
.split("\n")
|
|
1035
|
+
.find((line) => line.trim().length > 0)
|
|
1036
|
+
?.trim() ?? "";
|
|
1037
|
+
title = title.replace(/^title[:\s]+/i, "");
|
|
1038
|
+
title = title.replace(/^[`"'“”‘’]+|[`"'“”‘’]+$/g, "");
|
|
1039
|
+
title = title.replace(/\s+/g, " ").trim();
|
|
1040
|
+
title = title.replace(/[.?!]+$/, "").trim();
|
|
1041
|
+
if (title.length === 0)
|
|
1042
|
+
return null;
|
|
1043
|
+
return title.length > THREAD_TITLE_MAX_LENGTH
|
|
1044
|
+
? `${title.slice(0, THREAD_TITLE_MAX_LENGTH - 1).trimEnd()}…`
|
|
1045
|
+
: title;
|
|
1046
|
+
}
|
|
1047
|
+
function normalizeFallbackThreadTitle(raw) {
|
|
1048
|
+
const title = raw.replaceAll("\n", " ").replaceAll("\r", " ").trim();
|
|
1049
|
+
return title.slice(0, THREAD_TITLE_FALLBACK_MAX_LENGTH);
|
|
1050
|
+
}
|
|
1051
|
+
function isUnmaterializedThreadError(error, threadId) {
|
|
1052
|
+
if (!(error instanceof Error))
|
|
1053
|
+
return false;
|
|
1054
|
+
if (error.message === `no rollout found for thread id ${threadId}`)
|
|
1055
|
+
return true;
|
|
1056
|
+
const notMaterializedYet = `thread ${threadId} is not materialized yet; ` +
|
|
1057
|
+
"includeTurns is unavailable before first user message";
|
|
1058
|
+
if (error.message === notMaterializedYet)
|
|
1059
|
+
return true;
|
|
1060
|
+
return error.message.includes(threadId) &&
|
|
1061
|
+
error.message.includes("failed to read session metadata") &&
|
|
1062
|
+
error.message.endsWith("is empty");
|
|
1063
|
+
}
|
|
1064
|
+
function parseRemoteControlStatus(value) {
|
|
1065
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1066
|
+
return null;
|
|
1067
|
+
const record = value;
|
|
1068
|
+
const status = record["status"];
|
|
1069
|
+
if (status !== "disabled" &&
|
|
1070
|
+
status !== "connecting" &&
|
|
1071
|
+
status !== "connected" &&
|
|
1072
|
+
status !== "errored") {
|
|
1073
|
+
return null;
|
|
1074
|
+
}
|
|
1075
|
+
const environmentId = record["environmentId"];
|
|
1076
|
+
if (environmentId !== null && environmentId !== undefined && typeof environmentId !== "string") {
|
|
1077
|
+
return null;
|
|
1078
|
+
}
|
|
1079
|
+
return {
|
|
1080
|
+
status,
|
|
1081
|
+
hasEnvironment: typeof environmentId === "string" && environmentId.length > 0,
|
|
1082
|
+
};
|
|
1083
|
+
}
|
|
1084
|
+
function parseRemoteControlPairing(value) {
|
|
1085
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1086
|
+
return null;
|
|
1087
|
+
const record = value;
|
|
1088
|
+
const pairingCode = record["pairingCode"];
|
|
1089
|
+
const manualPairingCode = record["manualPairingCode"];
|
|
1090
|
+
const environmentId = record["environmentId"];
|
|
1091
|
+
const expiresAt = record["expiresAt"];
|
|
1092
|
+
if (typeof pairingCode !== "string" ||
|
|
1093
|
+
pairingCode.length === 0 ||
|
|
1094
|
+
Buffer.byteLength(pairingCode, "utf8") > 2_048 ||
|
|
1095
|
+
(manualPairingCode !== null && typeof manualPairingCode !== "string") ||
|
|
1096
|
+
typeof environmentId !== "string" ||
|
|
1097
|
+
environmentId.length === 0 ||
|
|
1098
|
+
!Number.isSafeInteger(expiresAt) ||
|
|
1099
|
+
expiresAt <= 0) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
return {
|
|
1103
|
+
pairingCode,
|
|
1104
|
+
manualPairingCode,
|
|
1105
|
+
expiresAt: expiresAt,
|
|
1106
|
+
};
|
|
1107
|
+
}
|
|
1108
|
+
function extractThreadItems(response) {
|
|
1109
|
+
const result = [];
|
|
1110
|
+
if (typeof response !== "object" || response === null)
|
|
1111
|
+
return result;
|
|
1112
|
+
const thread = response["thread"];
|
|
1113
|
+
if (typeof thread !== "object" || thread === null)
|
|
1114
|
+
return result;
|
|
1115
|
+
const turns = thread["turns"];
|
|
1116
|
+
if (!Array.isArray(turns))
|
|
1117
|
+
return result;
|
|
1118
|
+
for (const turn of turns) {
|
|
1119
|
+
if (typeof turn !== "object" || turn === null)
|
|
1120
|
+
continue;
|
|
1121
|
+
const items = turn["items"];
|
|
1122
|
+
if (!Array.isArray(items))
|
|
1123
|
+
continue;
|
|
1124
|
+
for (const item of items) {
|
|
1125
|
+
if (typeof item !== "object" || item === null || Array.isArray(item))
|
|
1126
|
+
continue;
|
|
1127
|
+
result.push(item);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
return result;
|
|
1131
|
+
}
|
|
1132
|
+
/** thread/resume が返す turn 一覧から、別 client が開始した実行中 turn を復元する。 */
|
|
1133
|
+
function extractActiveTurnId(response) {
|
|
1134
|
+
if (typeof response !== "object" || response === null)
|
|
1135
|
+
return null;
|
|
1136
|
+
const thread = response["thread"];
|
|
1137
|
+
if (typeof thread !== "object" || thread === null)
|
|
1138
|
+
return null;
|
|
1139
|
+
const turns = thread["turns"];
|
|
1140
|
+
if (!Array.isArray(turns))
|
|
1141
|
+
return null;
|
|
1142
|
+
for (let index = turns.length - 1; index >= 0; index -= 1) {
|
|
1143
|
+
const turn = turns[index];
|
|
1144
|
+
if (typeof turn !== "object" || turn === null || Array.isArray(turn))
|
|
1145
|
+
continue;
|
|
1146
|
+
const record = turn;
|
|
1147
|
+
if (record["status"] !== "inProgress")
|
|
1148
|
+
continue;
|
|
1149
|
+
const id = record["id"];
|
|
1150
|
+
if (typeof id === "string" && id.length > 0)
|
|
1151
|
+
return id;
|
|
1152
|
+
}
|
|
1153
|
+
return null;
|
|
1154
|
+
}
|
|
1155
|
+
function defaultLaunch(executable, args, env) {
|
|
1156
|
+
const child = spawn(executable, args, { detached: true, env, stdio: "ignore" });
|
|
1157
|
+
child.once("error", () => {
|
|
1158
|
+
// readiness probeが最終的な構造化エラーを返すため、EventEmitterの未処理errorだけ防ぐ。
|
|
1159
|
+
});
|
|
1160
|
+
child.unref();
|
|
1161
|
+
}
|
|
1162
|
+
function extractThreadId(response) {
|
|
1163
|
+
if (typeof response !== "object" || response === null)
|
|
1164
|
+
return null;
|
|
1165
|
+
const thread = response["thread"];
|
|
1166
|
+
if (typeof thread !== "object" || thread === null)
|
|
1167
|
+
return null;
|
|
1168
|
+
const id = thread["id"];
|
|
1169
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
1170
|
+
}
|
|
1171
|
+
function extractTurnId(response) {
|
|
1172
|
+
if (typeof response !== "object" || response === null)
|
|
1173
|
+
return null;
|
|
1174
|
+
const turn = response["turn"];
|
|
1175
|
+
if (typeof turn !== "object" || turn === null)
|
|
1176
|
+
return null;
|
|
1177
|
+
const id = turn["id"];
|
|
1178
|
+
return typeof id === "string" && id.length > 0 ? id : null;
|
|
1179
|
+
}
|
|
1180
|
+
function parseModelListPage(response) {
|
|
1181
|
+
if (typeof response !== "object" || response === null || Array.isArray(response)) {
|
|
1182
|
+
throw new Error("Codex App Server model/list response is not an object");
|
|
1183
|
+
}
|
|
1184
|
+
const record = response;
|
|
1185
|
+
const data = record["data"];
|
|
1186
|
+
if (!Array.isArray(data))
|
|
1187
|
+
throw new Error("Codex App Server model/list response omitted data");
|
|
1188
|
+
const models = data.filter((value) => typeof value === "object" && value !== null && !Array.isArray(value));
|
|
1189
|
+
if (models.length !== data.length) {
|
|
1190
|
+
throw new Error("Codex App Server model/list returned an invalid model");
|
|
1191
|
+
}
|
|
1192
|
+
const rawCursor = record["nextCursor"];
|
|
1193
|
+
if (rawCursor !== null && rawCursor !== undefined && typeof rawCursor !== "string") {
|
|
1194
|
+
throw new Error("Codex App Server model/list returned an invalid cursor");
|
|
1195
|
+
}
|
|
1196
|
+
return { data: models, nextCursor: typeof rawCursor === "string" && rawCursor.length > 0 ? rawCursor : null };
|
|
1197
|
+
}
|
|
1198
|
+
function parseThreadListPage(response) {
|
|
1199
|
+
if (typeof response !== "object" || response === null || Array.isArray(response)) {
|
|
1200
|
+
throw new Error("Codex App Server thread/list response is not an object");
|
|
1201
|
+
}
|
|
1202
|
+
const record = response;
|
|
1203
|
+
const rawData = record["data"];
|
|
1204
|
+
if (!Array.isArray(rawData))
|
|
1205
|
+
throw new Error("Codex App Server thread/list response omitted data");
|
|
1206
|
+
const data = rawData.map((value) => {
|
|
1207
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
1208
|
+
throw new Error("Codex App Server thread/list returned an invalid thread");
|
|
1209
|
+
}
|
|
1210
|
+
const thread = value;
|
|
1211
|
+
const id = stringValue(thread["id"]);
|
|
1212
|
+
const updatedAt = thread["updatedAt"];
|
|
1213
|
+
if (id === null || typeof updatedAt !== "number" || !Number.isFinite(updatedAt)) {
|
|
1214
|
+
throw new Error("Codex App Server thread/list returned an invalid thread");
|
|
1215
|
+
}
|
|
1216
|
+
return {
|
|
1217
|
+
id,
|
|
1218
|
+
name: stringValue(thread["name"]),
|
|
1219
|
+
preview: stringValue(thread["preview"]),
|
|
1220
|
+
updatedAt: Math.floor(updatedAt),
|
|
1221
|
+
cwd: stringValue(thread["cwd"]),
|
|
1222
|
+
source: thread["source"],
|
|
1223
|
+
parentThreadId: stringValue(thread["parentThreadId"]),
|
|
1224
|
+
};
|
|
1225
|
+
});
|
|
1226
|
+
const rawCursor = record["nextCursor"];
|
|
1227
|
+
if (rawCursor !== null && rawCursor !== undefined && typeof rawCursor !== "string") {
|
|
1228
|
+
throw new Error("Codex App Server thread/list returned an invalid cursor");
|
|
1229
|
+
}
|
|
1230
|
+
return { data, nextCursor: typeof rawCursor === "string" && rawCursor.length > 0 ? rawCursor : null };
|
|
1231
|
+
}
|
|
1232
|
+
/** models_cacheのraw上限へ有効率を掛け、実行時token_countと同じ分母へ揃える。 */
|
|
1233
|
+
function readModelContextWindows(cachePath) {
|
|
1234
|
+
const result = new Map();
|
|
1235
|
+
let parsed;
|
|
1236
|
+
try {
|
|
1237
|
+
parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
1238
|
+
}
|
|
1239
|
+
catch {
|
|
1240
|
+
return result;
|
|
1241
|
+
}
|
|
1242
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed))
|
|
1243
|
+
return result;
|
|
1244
|
+
const models = parsed["models"];
|
|
1245
|
+
if (!Array.isArray(models))
|
|
1246
|
+
return result;
|
|
1247
|
+
for (const value of models) {
|
|
1248
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
1249
|
+
continue;
|
|
1250
|
+
const model = value;
|
|
1251
|
+
const slug = stringValue(model["slug"]);
|
|
1252
|
+
const rawWindow = positiveInteger(model["context_window"]);
|
|
1253
|
+
if (slug === null || rawWindow === undefined)
|
|
1254
|
+
continue;
|
|
1255
|
+
const percent = model["effective_context_window_percent"];
|
|
1256
|
+
const effective = typeof percent === "number" && Number.isFinite(percent) && percent > 0 && percent <= 100
|
|
1257
|
+
? Math.floor(rawWindow * percent / 100)
|
|
1258
|
+
: rawWindow;
|
|
1259
|
+
if (effective > 0)
|
|
1260
|
+
result.set(slug, effective);
|
|
1261
|
+
}
|
|
1262
|
+
return result;
|
|
1263
|
+
}
|
|
1264
|
+
function stringValue(value) {
|
|
1265
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
1266
|
+
}
|
|
1267
|
+
function positiveInteger(value) {
|
|
1268
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
|
|
1269
|
+
}
|
|
1270
|
+
/** App Server のUnix秒をprotocolのミリ秒へ揃える。将来ミリ秒値になっても二重変換しない。 */
|
|
1271
|
+
function codexTimestampMs(value) {
|
|
1272
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
1273
|
+
return undefined;
|
|
1274
|
+
return Math.floor(value < 10_000_000_000 ? value * 1_000 : value);
|
|
1275
|
+
}
|
|
1276
|
+
function readLockPid(lockPath) {
|
|
1277
|
+
try {
|
|
1278
|
+
const parsed = JSON.parse(fs.readFileSync(lockPath, "utf8"));
|
|
1279
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
1280
|
+
return null;
|
|
1281
|
+
const pid = parsed["pid"];
|
|
1282
|
+
return typeof pid === "number" && Number.isSafeInteger(pid) && pid > 0 ? pid : null;
|
|
1283
|
+
}
|
|
1284
|
+
catch {
|
|
1285
|
+
return null;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
function processIsAlive(pid) {
|
|
1289
|
+
try {
|
|
1290
|
+
process.kill(pid, 0);
|
|
1291
|
+
return true;
|
|
1292
|
+
}
|
|
1293
|
+
catch (error) {
|
|
1294
|
+
return error.code === "EPERM";
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
//# sourceMappingURL=codexAppServer.js.map
|