tailii-host 0.9.0 → 0.10.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/dist/backend/herdr.js +8 -1
- package/dist/backend/herdr.js.map +1 -1
- package/dist/backend/tmux.js +90 -15
- package/dist/backend/tmux.js.map +1 -1
- package/dist/chat/subagentTranscript.js +4 -0
- package/dist/chat/subagentTranscript.js.map +1 -1
- package/dist/chat/transcriptTailer.js +8 -0
- package/dist/chat/transcriptTailer.js.map +1 -1
- package/dist/codex/codexAppServer.d.ts +74 -3
- package/dist/codex/codexAppServer.js +252 -12
- package/dist/codex/codexAppServer.js.map +1 -1
- package/dist/codex/codexNativeTurnController.d.ts +99 -2
- package/dist/codex/codexNativeTurnController.js +218 -8
- package/dist/codex/codexNativeTurnController.js.map +1 -1
- package/dist/codex/codexPlanItem.d.ts +14 -0
- package/dist/codex/codexPlanItem.js +50 -0
- package/dist/codex/codexPlanItem.js.map +1 -0
- package/dist/codex/codexRolloutTailer.d.ts +10 -0
- package/dist/codex/codexRolloutTailer.js +60 -2
- package/dist/codex/codexRolloutTailer.js.map +1 -1
- package/dist/codex/codexSystemNotice.d.ts +22 -1
- package/dist/codex/codexSystemNotice.js +46 -0
- package/dist/codex/codexSystemNotice.js.map +1 -1
- package/dist/engine/engine.js +1 -0
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/handlers/codex.js +36 -1
- package/dist/engine/handlers/codex.js.map +1 -1
- package/dist/hub/hubProtocol.d.ts +22 -1
- package/dist/hub/hubProtocol.js +53 -1
- package/dist/hub/hubProtocol.js.map +1 -1
- package/dist/hub/sessionHub.d.ts +12 -0
- package/dist/hub/sessionHub.js +96 -5
- package/dist/hub/sessionHub.js.map +1 -1
- package/dist/protocol/decode.js +52 -0
- package/dist/protocol/decode.js.map +1 -1
- package/dist/protocol/messages.d.ts +58 -0
- package/dist/sessions/claudeSessionStore.js +4 -0
- package/dist/sessions/claudeSessionStore.js.map +1 -1
- package/dist/sessions/sessionSearch.js +5 -0
- package/dist/sessions/sessionSearch.js.map +1 -1
- package/dist/shared/clientBuild.d.ts +1 -1
- package/dist/shared/clientBuild.js +1 -1
- package/dist/shared/clientBuild.js.map +1 -1
- package/dist/shared/compactSummary.d.ts +9 -0
- package/dist/shared/compactSummary.js +16 -0
- package/dist/shared/compactSummary.js.map +1 -0
- package/package.json +3 -3
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CodexAppServerApprovalPolicy, CodexAppServerThreadOptions, CodexThreadTitleGenerationResult, CodexThreadTitleSource } from "./codexAppServer.js";
|
|
2
|
-
import { type Decision, type QuestionAnswer, type QuestionPromptQuestion } from "../protocol.js";
|
|
2
|
+
import { type CodexCollaborationMode, type CodexGoalAction, type CodexGoalInfo, type Decision, type QuestionAnswer, type QuestionPromptQuestion } from "../protocol.js";
|
|
3
3
|
import type { ControlMessage } from "../protocol.js";
|
|
4
4
|
export interface CodexNativeApproval {
|
|
5
5
|
id: string;
|
|
@@ -26,6 +26,14 @@ export interface CodexNativeTurnControllerOptions {
|
|
|
26
26
|
itemId: string;
|
|
27
27
|
payload: ControlMessage;
|
|
28
28
|
}) => void;
|
|
29
|
+
/**
|
|
30
|
+
* 目標の現在値(codex-goal)。`thread/goal/updated` / `cleared`、goal RPC の結果、会話オープン時の
|
|
31
|
+
* 読み取りから配る。null は「目標なし」。Hub は会話 stream(codex_goal_state)へ流し、目標が
|
|
32
|
+
* active な間は購読者ゼロでも thread 購読を保持する(継続 turn は server 起点のため)。
|
|
33
|
+
*/
|
|
34
|
+
onGoal?: (session: string, goal: CodexGoalInfo | null) => void;
|
|
35
|
+
/** thread の collaboration mode(`thread/settings/updated`)。Hub は `pc:collab` マーカーで iOS へ配る。 */
|
|
36
|
+
onCollaborationMode?: (session: string, mode: CodexCollaborationMode) => void;
|
|
29
37
|
onDisconnect?: (session: string, error: Error) => void;
|
|
30
38
|
onThreadTitle?: (event: {
|
|
31
39
|
session: string;
|
|
@@ -53,8 +61,14 @@ export interface CodexTurnControllerRuntime {
|
|
|
53
61
|
effort?: string | null;
|
|
54
62
|
sandbox?: "read-only" | "workspace-write" | "danger-full-access" | null;
|
|
55
63
|
approvalPolicy?: CodexAppServerApprovalPolicy | null;
|
|
64
|
+
/** collaboration mode(プランモード, codex-plan-mode)。未指定は thread の現状のまま。 */
|
|
65
|
+
collaborationMode?: CodexCollaborationMode | null;
|
|
56
66
|
}): Promise<string>;
|
|
57
67
|
interruptTurn?(session: string): Promise<void>;
|
|
68
|
+
/** 目標の読み取り / 設定 / 解除(codex-goal)。thread を開いていなければ開いてから実行する。 */
|
|
69
|
+
goal?(options: CodexGoalOperation): Promise<CodexGoalOperationResult>;
|
|
70
|
+
/** 目標が active な会話か(Hub が購読者ゼロでも thread 購読を保持する判定)。 */
|
|
71
|
+
hasActiveGoal?(session: string): boolean;
|
|
58
72
|
/**
|
|
59
73
|
* rollout の terminal event(task_complete / turn_aborted)を、現在追跡中の
|
|
60
74
|
* 同一 turn に限って完了へ反映する。App Server の turn/completed 通知欠落を補う副経路。
|
|
@@ -85,11 +99,50 @@ export interface CodexThreadClient {
|
|
|
85
99
|
/** 子 thread の実行モデル(thread/read の Thread.model)。無ければ省略。 */
|
|
86
100
|
model?: string;
|
|
87
101
|
} | undefined>;
|
|
88
|
-
startTurn(text: string, clientUserMessageId?: string | null, effort?: string | null, sandbox?: "read-only" | "workspace-write" | "danger-full-access" | null, approvalPolicy?: CodexAppServerApprovalPolicy | null): Promise<string>;
|
|
102
|
+
startTurn(text: string, clientUserMessageId?: string | null, effort?: string | null, sandbox?: "read-only" | "workspace-write" | "danger-full-access" | null, approvalPolicy?: CodexAppServerApprovalPolicy | null, collaborationMode?: CodexCollaborationMode | null): Promise<string>;
|
|
89
103
|
steerTurn(turnId: string, text: string, clientUserMessageId?: string | null): Promise<void>;
|
|
90
104
|
interruptTurn(turnId: string): Promise<void>;
|
|
105
|
+
/** `thread/settings/updated` の取り込み(モデル / effort / collaboration mode の追従, codex-plan-mode)。 */
|
|
106
|
+
noteThreadSettings?(settings: Record<string, unknown>): void;
|
|
107
|
+
/** この接続で最後に観測 / 送信した collaboration mode(server の thread 設定に残る値)。不明は null。 */
|
|
108
|
+
readonly collaborationMode?: CodexCollaborationMode | null;
|
|
109
|
+
/** plan 直前の model / effort(default 復帰で戻す値)。plan 中でなければ null。未対応は undefined。 */
|
|
110
|
+
readonly planRestoreSnapshot?: CodexPlanRestoreSnapshot | null;
|
|
111
|
+
/** 接続を作り直したときに controller が預かっていた plan 直前の値を引き継ぐ(既にあれば上書きしない)。 */
|
|
112
|
+
seedPlanRestoreSnapshot?(snapshot: CodexPlanRestoreSnapshot): void;
|
|
113
|
+
/** `model/rerouted` 等のモデルだけの変更の取り込み。 */
|
|
114
|
+
noteModel?(model: string): void;
|
|
115
|
+
/** 目標 API(codex-goal)。旧 App Server / テスト fake では未提供。 */
|
|
116
|
+
goalGet?(): Promise<CodexGoalInfo | null>;
|
|
117
|
+
goalSet?(params: {
|
|
118
|
+
objective?: string;
|
|
119
|
+
status?: string;
|
|
120
|
+
tokenBudget?: number;
|
|
121
|
+
}): Promise<CodexGoalInfo>;
|
|
122
|
+
goalClear?(): Promise<boolean>;
|
|
91
123
|
close(): void;
|
|
92
124
|
}
|
|
125
|
+
/** plan に入る直前の thread 設定(default 復帰で戻す, codex-plan-mode)。 */
|
|
126
|
+
export interface CodexPlanRestoreSnapshot {
|
|
127
|
+
model: string | null;
|
|
128
|
+
effort: string | null;
|
|
129
|
+
}
|
|
130
|
+
/** 目標操作の入力(Hub の `codex_goal_submit` と同形)。 */
|
|
131
|
+
export interface CodexGoalOperation {
|
|
132
|
+
session: string;
|
|
133
|
+
threadId: string;
|
|
134
|
+
cwd: string;
|
|
135
|
+
action: CodexGoalAction;
|
|
136
|
+
objective?: string;
|
|
137
|
+
status?: string;
|
|
138
|
+
tokenBudget?: number;
|
|
139
|
+
}
|
|
140
|
+
export interface CodexGoalOperationResult {
|
|
141
|
+
/** 操作後の現在値(clear 後や未設定は null)。 */
|
|
142
|
+
goal: CodexGoalInfo | null;
|
|
143
|
+
/** clear の結果(server が何かを消したか)。 */
|
|
144
|
+
cleared?: boolean;
|
|
145
|
+
}
|
|
93
146
|
export interface CodexSubscriptionSnapshot {
|
|
94
147
|
itemIds: ReadonlySet<string>;
|
|
95
148
|
contentCounts: ReadonlyMap<string, number>;
|
|
@@ -103,6 +156,11 @@ export interface CodexSubscriptionSnapshot {
|
|
|
103
156
|
* 反映する。null は不明(未materialize で resume が成立しない・旧 App Server)。
|
|
104
157
|
*/
|
|
105
158
|
model?: string | null;
|
|
159
|
+
/**
|
|
160
|
+
* 購読時点で thread に残っている collaboration mode(controller が観測 / 送信した値。codex-plan-mode)。
|
|
161
|
+
* Hub は backfill 完了後に `pc:collab` として配り、iOS のトグルを実体へ揃える。null は不明。
|
|
162
|
+
*/
|
|
163
|
+
collaborationMode?: CodexCollaborationMode | null;
|
|
106
164
|
}
|
|
107
165
|
export interface CodexAppServerThreadRuntime {
|
|
108
166
|
openThread(options: CodexAppServerThreadOptions): Promise<CodexThreadClient>;
|
|
@@ -125,13 +183,25 @@ export declare class CodexNativeTurnController implements CodexTurnControllerRun
|
|
|
125
183
|
private readonly onQuestion;
|
|
126
184
|
private readonly onQuestionDismiss;
|
|
127
185
|
private readonly onChatItem;
|
|
186
|
+
private readonly onGoal;
|
|
187
|
+
private readonly onCollaborationMode;
|
|
128
188
|
private readonly onDisconnect;
|
|
129
189
|
private readonly onThreadTitle;
|
|
130
190
|
private readonly log;
|
|
131
191
|
/** 同文の deprecationNotice は process 内で 1 回だけ記録する(thread を開くたびに届く)。 */
|
|
132
192
|
private readonly loggedDeprecationNotices;
|
|
133
193
|
private readonly open;
|
|
194
|
+
/** session ごとの開始中 Promise(threadFor の同時呼び出しを 1 回の open に畳む)。 */
|
|
195
|
+
private readonly opening;
|
|
196
|
+
/**
|
|
197
|
+
* plan に入る直前の model / effort(codex-plan-mode)。thread 接続は購読者ゼロで畳まれるため、接続を
|
|
198
|
+
* 作り直しても default 復帰で元の値へ戻せるよう controller が session 単位で預かる(hub 再起動では失う)。
|
|
199
|
+
*/
|
|
200
|
+
private readonly planRestoreBySession;
|
|
201
|
+
/** 開始中(opening)に close を要求された session。開き終わった時点で畳む。 */
|
|
202
|
+
private readonly closeRequestedWhileOpening;
|
|
134
203
|
private readonly pendingUserInput;
|
|
204
|
+
private rememberPlanRestoreSnapshot;
|
|
135
205
|
constructor(options: CodexNativeTurnControllerOptions);
|
|
136
206
|
subscribeSession(options: {
|
|
137
207
|
session: string;
|
|
@@ -147,7 +217,28 @@ export declare class CodexNativeTurnController implements CodexTurnControllerRun
|
|
|
147
217
|
effort?: string | null;
|
|
148
218
|
sandbox?: "read-only" | "workspace-write" | "danger-full-access" | null;
|
|
149
219
|
approvalPolicy?: CodexAppServerApprovalPolicy | null;
|
|
220
|
+
collaborationMode?: CodexCollaborationMode | null;
|
|
150
221
|
}): Promise<string>;
|
|
222
|
+
/**
|
|
223
|
+
* 実行中 turn への steer は collaboration mode を変えられない(turn/steer に mode 引数が無い)。iOS の
|
|
224
|
+
* トグルと違う mode の turn が走っている間の送信は、そのまま steer しつつ注記で知らせる(次の新規 turn
|
|
225
|
+
* から反映される)。thread の現 mode が不明な接続(テスト fake / 旧 server)では判定できないので黙る。
|
|
226
|
+
*/
|
|
227
|
+
private noteCollaborationModeNotAppliedBySteer;
|
|
228
|
+
/** 目標の読み取り / 設定 / 解除(codex-goal)。結果は通知と同じ経路(onGoal / 注記)にも反映する。 */
|
|
229
|
+
goal(options: CodexGoalOperation): Promise<CodexGoalOperationResult>;
|
|
230
|
+
hasActiveGoal(session: string): boolean;
|
|
231
|
+
/**
|
|
232
|
+
* 目標の現在値を取り込む。状態 / 内容が変わったときだけ 🎯 注記を chat へ出し(進捗だけの更新では
|
|
233
|
+
* 出さない)、必要なら onGoal で Hub へ配る。open 直後の初回読み取りは注記を出さない(履歴側の
|
|
234
|
+
* rollout 注記と二重になるため)。
|
|
235
|
+
*/
|
|
236
|
+
private applyGoal;
|
|
237
|
+
/**
|
|
238
|
+
* open 直後に現在の目標を読む(失敗は無視: 旧 server / ephemeral thread は未対応)。threadFor が
|
|
239
|
+
* await するので、購読直後に購読者がいなくなっても hasActiveGoal が「未読」で false を返さない。
|
|
240
|
+
*/
|
|
241
|
+
private loadGoal;
|
|
151
242
|
private generateThreadTitle;
|
|
152
243
|
private generateThreadTitleWithRetry;
|
|
153
244
|
interruptTurn(session: string): Promise<void>;
|
|
@@ -156,7 +247,13 @@ export declare class CodexNativeTurnController implements CodexTurnControllerRun
|
|
|
156
247
|
close(): void;
|
|
157
248
|
/** iOS の既存 QuestionPromptSheet 回答を native requestUserInput response へ戻す。 */
|
|
158
249
|
answerQuestion(id: string, answers: QuestionAnswer[]): boolean;
|
|
250
|
+
/**
|
|
251
|
+
* `intent`: "operate"(turn 開始 / 目標操作)は、開いている途中で close 要求が入って畳まれた場合に開き直す
|
|
252
|
+
* (閉じた接続へ turn/start を投げない)。"subscribe" は畳まれた OpenThread をそのまま返す(購読者が
|
|
253
|
+
* 去った後の再 open は無駄で、hub 側が購読者ゼロなら結果を捨てる)。
|
|
254
|
+
*/
|
|
159
255
|
private threadFor;
|
|
256
|
+
private openThreadFor;
|
|
160
257
|
private handleNotification;
|
|
161
258
|
/** 子 thread(サブエージェント)のモデル変更を workflow ノードへ反映する。未知の thread は無視。 */
|
|
162
259
|
private applySubagentModel;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// Codex App Server の長寿命 thread 接続、native approval、turn/start を Tailii へ結線する。
|
|
2
2
|
import * as net from "node:net";
|
|
3
|
+
import { codexGoalFromWire } from "./codexAppServer.js";
|
|
3
4
|
import { decodeControlMessage, encodeControlMessage, PROTOCOL_V1, PROTOCOL_V2, } from "../protocol.js";
|
|
5
|
+
import { codexPlanChatOutput } from "./codexPlanItem.js";
|
|
4
6
|
import { resolveSocketPath } from "../shared/socketPath.js";
|
|
5
7
|
import { sleep } from "../shared/sleep.js";
|
|
6
8
|
import { codexItemToolActivities, codexPlanUpdateActivity, toolActivityContentKey, toolActivityMessage, } from "./codexToolActivity.js";
|
|
7
9
|
import { CodexSubagentTracker } from "./codexSubagentTracker.js";
|
|
8
|
-
import { codexAppServerSystemNotice, codexDeprecationNoticeLogLine, codexMcpItemErrorNotice, codexSystemNoticeContentKey, } from "./codexSystemNotice.js";
|
|
10
|
+
import { codexAppServerSystemNotice, codexCollaborationModeSteerNotice, codexDeprecationNoticeLogLine, codexGoalClearedNotice, codexGoalNotice, codexGoalNoticeKey, codexMcpItemErrorNotice, codexSystemNoticeContentKey, } from "./codexSystemNotice.js";
|
|
9
11
|
const TITLE_GENERATION_MAX_ATTEMPTS = 3;
|
|
10
12
|
const TITLE_GENERATION_RETRY_BASE_MS = 250;
|
|
11
13
|
/** 記録済み deprecationNotice 文言の保持上限。 */
|
|
@@ -52,13 +54,33 @@ export class CodexNativeTurnController {
|
|
|
52
54
|
onQuestion;
|
|
53
55
|
onQuestionDismiss;
|
|
54
56
|
onChatItem;
|
|
57
|
+
onGoal;
|
|
58
|
+
onCollaborationMode;
|
|
55
59
|
onDisconnect;
|
|
56
60
|
onThreadTitle;
|
|
57
61
|
log;
|
|
58
62
|
/** 同文の deprecationNotice は process 内で 1 回だけ記録する(thread を開くたびに届く)。 */
|
|
59
63
|
loggedDeprecationNotices = new Set();
|
|
60
64
|
open = new Map();
|
|
65
|
+
/** session ごとの開始中 Promise(threadFor の同時呼び出しを 1 回の open に畳む)。 */
|
|
66
|
+
opening = new Map();
|
|
67
|
+
/**
|
|
68
|
+
* plan に入る直前の model / effort(codex-plan-mode)。thread 接続は購読者ゼロで畳まれるため、接続を
|
|
69
|
+
* 作り直しても default 復帰で元の値へ戻せるよう controller が session 単位で預かる(hub 再起動では失う)。
|
|
70
|
+
*/
|
|
71
|
+
planRestoreBySession = new Map();
|
|
72
|
+
/** 開始中(opening)に close を要求された session。開き終わった時点で畳む。 */
|
|
73
|
+
closeRequestedWhileOpening = new Set();
|
|
61
74
|
pendingUserInput = new Map();
|
|
75
|
+
rememberPlanRestoreSnapshot(session, threadId, thread) {
|
|
76
|
+
const snapshot = thread.planRestoreSnapshot;
|
|
77
|
+
if (snapshot === undefined)
|
|
78
|
+
return;
|
|
79
|
+
if (snapshot === null)
|
|
80
|
+
this.planRestoreBySession.delete(session);
|
|
81
|
+
else
|
|
82
|
+
this.planRestoreBySession.set(session, { threadId, snapshot });
|
|
83
|
+
}
|
|
62
84
|
constructor(options) {
|
|
63
85
|
this.appServer = options.appServer;
|
|
64
86
|
this.approvalBroker = options.approvalBroker ?? requestCodexApprovalViaBroker;
|
|
@@ -68,12 +90,14 @@ export class CodexNativeTurnController {
|
|
|
68
90
|
this.onQuestion = options.onQuestion ?? (() => { });
|
|
69
91
|
this.onQuestionDismiss = options.onQuestionDismiss ?? (() => { });
|
|
70
92
|
this.onChatItem = options.onChatItem ?? (() => { });
|
|
93
|
+
this.onGoal = options.onGoal ?? (() => { });
|
|
94
|
+
this.onCollaborationMode = options.onCollaborationMode ?? (() => { });
|
|
71
95
|
this.onDisconnect = options.onDisconnect ?? (() => { });
|
|
72
96
|
this.onThreadTitle = options.onThreadTitle ?? (() => { });
|
|
73
97
|
this.log = options.log ?? (() => { });
|
|
74
98
|
}
|
|
75
99
|
async subscribeSession(options) {
|
|
76
|
-
const opened = await this.threadFor(options.session, options.threadId, options.cwd);
|
|
100
|
+
const opened = await this.threadFor(options.session, options.threadId, options.cwd, "subscribe");
|
|
77
101
|
const itemIds = new Set();
|
|
78
102
|
const contentCounts = new Map();
|
|
79
103
|
for (const item of opened.thread.initialItems ?? []) {
|
|
@@ -87,12 +111,16 @@ export class CodexNativeTurnController {
|
|
|
87
111
|
contentCounts.set(key, (contentCounts.get(key) ?? 0) + 1);
|
|
88
112
|
}
|
|
89
113
|
}
|
|
114
|
+
// 目標の現在値は購読のたびに配る(既存 thread の再利用 = 開き直し / 再接続 / 別端末でも届く。
|
|
115
|
+
// null も配って、開いていない間に解除された目標の古い表示を消す)。
|
|
116
|
+
this.onGoal(options.session, opened.goal);
|
|
90
117
|
return {
|
|
91
118
|
itemIds,
|
|
92
119
|
contentCounts,
|
|
93
120
|
liveSubscribed: opened.thread.liveSubscriptionReady !== false,
|
|
94
121
|
liveSubscriptionError: opened.thread.liveSubscriptionError ?? null,
|
|
95
122
|
model: opened.thread.model ?? null,
|
|
123
|
+
collaborationMode: opened.thread.collaborationMode ?? null,
|
|
96
124
|
};
|
|
97
125
|
}
|
|
98
126
|
async startTurn(options) {
|
|
@@ -111,6 +139,7 @@ export class CodexNativeTurnController {
|
|
|
111
139
|
if (activeTurnId !== null) {
|
|
112
140
|
try {
|
|
113
141
|
await opened.thread.steerTurn(activeTurnId, options.text, options.clientUserMessageId);
|
|
142
|
+
this.noteCollaborationModeNotAppliedBySteer(options.session, opened, activeTurnId, options.collaborationMode);
|
|
114
143
|
this.generateThreadTitle(opened, options.session, options.text);
|
|
115
144
|
return activeTurnId;
|
|
116
145
|
}
|
|
@@ -121,6 +150,7 @@ export class CodexNativeTurnController {
|
|
|
121
150
|
if (currentTurnId !== null && currentTurnId !== activeTurnId) {
|
|
122
151
|
opened.activeTurnId = currentTurnId;
|
|
123
152
|
await opened.thread.steerTurn(currentTurnId, options.text, options.clientUserMessageId);
|
|
153
|
+
this.noteCollaborationModeNotAppliedBySteer(options.session, opened, currentTurnId, options.collaborationMode);
|
|
124
154
|
this.generateThreadTitle(opened, options.session, options.text);
|
|
125
155
|
return currentTurnId;
|
|
126
156
|
}
|
|
@@ -131,7 +161,9 @@ export class CodexNativeTurnController {
|
|
|
131
161
|
throw error;
|
|
132
162
|
}
|
|
133
163
|
}
|
|
134
|
-
|
|
164
|
+
// 実行中 turn への steer は入力を足すだけで collaboration mode は変えられない(上の steer 経路)。
|
|
165
|
+
// 新規 turn だけが iOS のトグルどおりの mode で始まる。
|
|
166
|
+
const turnId = await opened.thread.startTurn(options.text, options.clientUserMessageId, options.effort, options.sandbox, options.approvalPolicy, options.collaborationMode ?? null);
|
|
135
167
|
opened.activeTurnId = turnId;
|
|
136
168
|
this.generateThreadTitle(opened, options.session, options.text);
|
|
137
169
|
return turnId;
|
|
@@ -141,6 +173,93 @@ export class CodexNativeTurnController {
|
|
|
141
173
|
throw error;
|
|
142
174
|
}
|
|
143
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* 実行中 turn への steer は collaboration mode を変えられない(turn/steer に mode 引数が無い)。iOS の
|
|
178
|
+
* トグルと違う mode の turn が走っている間の送信は、そのまま steer しつつ注記で知らせる(次の新規 turn
|
|
179
|
+
* から反映される)。thread の現 mode が不明な接続(テスト fake / 旧 server)では判定できないので黙る。
|
|
180
|
+
*/
|
|
181
|
+
noteCollaborationModeNotAppliedBySteer(session, opened, turnId, requested) {
|
|
182
|
+
if (!requested)
|
|
183
|
+
return;
|
|
184
|
+
const current = opened.thread.collaborationMode;
|
|
185
|
+
if (current === undefined || current === null || current === requested)
|
|
186
|
+
return;
|
|
187
|
+
const notice = codexCollaborationModeSteerNotice(requested, turnId);
|
|
188
|
+
this.onChatItem({ session, itemId: notice.itemId, payload: notice.payload });
|
|
189
|
+
}
|
|
190
|
+
/** 目標の読み取り / 設定 / 解除(codex-goal)。結果は通知と同じ経路(onGoal / 注記)にも反映する。 */
|
|
191
|
+
async goal(options) {
|
|
192
|
+
const opened = await this.threadFor(options.session, options.threadId, options.cwd);
|
|
193
|
+
const thread = opened.thread;
|
|
194
|
+
if (options.action === "get") {
|
|
195
|
+
if (thread.goalGet === undefined)
|
|
196
|
+
throw new Error("この Codex App Server は目標(goal)に対応していません");
|
|
197
|
+
const goal = await thread.goalGet();
|
|
198
|
+
this.applyGoal(options.session, opened, goal, { publishState: true });
|
|
199
|
+
return { goal };
|
|
200
|
+
}
|
|
201
|
+
if (options.action === "clear") {
|
|
202
|
+
if (thread.goalClear === undefined)
|
|
203
|
+
throw new Error("この Codex App Server は目標(goal)に対応していません");
|
|
204
|
+
const cleared = await thread.goalClear();
|
|
205
|
+
this.applyGoal(options.session, opened, null, { publishState: true });
|
|
206
|
+
return { goal: null, cleared };
|
|
207
|
+
}
|
|
208
|
+
if (thread.goalSet === undefined)
|
|
209
|
+
throw new Error("この Codex App Server は目標(goal)に対応していません");
|
|
210
|
+
const goal = await thread.goalSet({
|
|
211
|
+
...(options.objective !== undefined ? { objective: options.objective } : {}),
|
|
212
|
+
...(options.status !== undefined ? { status: options.status } : {}),
|
|
213
|
+
...(options.tokenBudget !== undefined ? { tokenBudget: options.tokenBudget } : {}),
|
|
214
|
+
});
|
|
215
|
+
this.applyGoal(options.session, opened, goal, { publishState: true });
|
|
216
|
+
return { goal };
|
|
217
|
+
}
|
|
218
|
+
hasActiveGoal(session) {
|
|
219
|
+
return this.open.get(session)?.goal?.status === "active";
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* 目標の現在値を取り込む。状態 / 内容が変わったときだけ 🎯 注記を chat へ出し(進捗だけの更新では
|
|
223
|
+
* 出さない)、必要なら onGoal で Hub へ配る。open 直後の初回読み取りは注記を出さない(履歴側の
|
|
224
|
+
* rollout 注記と二重になるため)。
|
|
225
|
+
*/
|
|
226
|
+
applyGoal(session, opened, goal, options) {
|
|
227
|
+
const key = codexGoalNoticeKey(goal);
|
|
228
|
+
const changed = opened.goalNoticeKey !== key;
|
|
229
|
+
const previousCreatedAt = opened.goal?.createdAt ?? null;
|
|
230
|
+
opened.goal = goal;
|
|
231
|
+
// 注記は「変化」だけ。初回読み取り(履歴の rollout 注記と二重になる)と、目標を一度も見ていない
|
|
232
|
+
// 状態での「解除」(意味が無い)は出さない。
|
|
233
|
+
const knownBefore = opened.goalNoticeKey !== null;
|
|
234
|
+
if (changed && options.initial !== true && (goal !== null || knownBefore)) {
|
|
235
|
+
const notice = goal === null ? codexGoalClearedNotice(previousCreatedAt) : codexGoalNotice(goal);
|
|
236
|
+
this.onChatItem({ session, itemId: notice.itemId, payload: notice.payload });
|
|
237
|
+
}
|
|
238
|
+
opened.goalNoticeKey = key;
|
|
239
|
+
if (options.publishState)
|
|
240
|
+
this.onGoal(session, goal);
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* open 直後に現在の目標を読む(失敗は無視: 旧 server / ephemeral thread は未対応)。threadFor が
|
|
244
|
+
* await するので、購読直後に購読者がいなくなっても hasActiveGoal が「未読」で false を返さない。
|
|
245
|
+
*/
|
|
246
|
+
async loadGoal(session, opened) {
|
|
247
|
+
if (opened.thread.goalGet === undefined)
|
|
248
|
+
return;
|
|
249
|
+
try {
|
|
250
|
+
const goal = await opened.thread.goalGet();
|
|
251
|
+
if (this.open.get(session) !== opened)
|
|
252
|
+
return;
|
|
253
|
+
// 通知(thread/goal/updated)が先に届いて確定済みなら上書きしない。
|
|
254
|
+
if (opened.goalNoticeKey !== null)
|
|
255
|
+
return;
|
|
256
|
+
// 配信は購読確立(subscribeSession)側で行う: 開き直しで既存 thread を再利用する場合も必ず 1 回届く。
|
|
257
|
+
this.applyGoal(session, opened, goal, { publishState: false, initial: true });
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
this.log(`Codex 目標の読み取りに失敗(無視) session=${session}: ${String(error)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
144
263
|
generateThreadTitle(opened, session, prompt) {
|
|
145
264
|
if (!opened.titleGenerationPending || this.appServer.generateThreadTitle === undefined)
|
|
146
265
|
return;
|
|
@@ -220,8 +339,13 @@ export class CodexNativeTurnController {
|
|
|
220
339
|
}
|
|
221
340
|
closeSession(session) {
|
|
222
341
|
const opened = this.open.get(session);
|
|
223
|
-
if (opened === undefined)
|
|
342
|
+
if (opened === undefined) {
|
|
343
|
+
// 開いている途中なら、開き終わった時点で畳む(openThread の完了を待たずに孤児接続を作らない)。
|
|
344
|
+
if (this.opening.has(session))
|
|
345
|
+
this.closeRequestedWhileOpening.add(session);
|
|
224
346
|
return;
|
|
347
|
+
}
|
|
348
|
+
this.rememberPlanRestoreSnapshot(session, opened.threadId, opened.thread);
|
|
225
349
|
this.open.delete(session);
|
|
226
350
|
opened.thread.close();
|
|
227
351
|
this.resolvePendingQuestionsForSession(session);
|
|
@@ -261,12 +385,54 @@ export class CodexNativeTurnController {
|
|
|
261
385
|
this.onQuestionDismiss(pending.session, id);
|
|
262
386
|
return true;
|
|
263
387
|
}
|
|
264
|
-
|
|
388
|
+
/**
|
|
389
|
+
* `intent`: "operate"(turn 開始 / 目標操作)は、開いている途中で close 要求が入って畳まれた場合に開き直す
|
|
390
|
+
* (閉じた接続へ turn/start を投げない)。"subscribe" は畳まれた OpenThread をそのまま返す(購読者が
|
|
391
|
+
* 去った後の再 open は無駄で、hub 側が購読者ゼロなら結果を捨てる)。
|
|
392
|
+
*/
|
|
393
|
+
async threadFor(session, threadId, cwd, intent = "operate") {
|
|
394
|
+
// 開始中(openThread → 目標読み取り → サブエージェント復元)の同じ session は完了を共有する。
|
|
395
|
+
// 会話オープンでは hub の購読と iOS の目標 get がほぼ同時に届くため、共有しないと openThread が
|
|
396
|
+
// 2 回走って片方の接続が閉じられずに残り、その切断が正しい方の OpenThread まで消す。
|
|
397
|
+
// 待った後は必ず opening を見直す: 別 threadId の待ち手が先に起きて新しい open を始めていることが
|
|
398
|
+
// あり、1 回だけの確認だと同じ threadId の open が 2 本走る。
|
|
399
|
+
for (;;) {
|
|
400
|
+
const pending = this.opening.get(session);
|
|
401
|
+
if (pending === undefined)
|
|
402
|
+
break;
|
|
403
|
+
let opened = null;
|
|
404
|
+
try {
|
|
405
|
+
opened = await pending.task;
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
// 同じ thread の open が失敗したなら、待ち手全員が順番に開き直して待ち時間を積み上げない
|
|
409
|
+
// (daemon 不調時に hub の RPC 予算を超える)。別 thread の失敗なら自分で開く。
|
|
410
|
+
if (pending.threadId === threadId)
|
|
411
|
+
throw error;
|
|
412
|
+
}
|
|
413
|
+
// 開き終わった直後に閉じられた / 別 thread へ切り替わった OpenThread は返さない。
|
|
414
|
+
if (opened !== null && opened.threadId === threadId && this.open.get(session) === opened)
|
|
415
|
+
return opened;
|
|
416
|
+
}
|
|
265
417
|
const existing = this.open.get(session);
|
|
266
418
|
if (existing?.threadId === threadId)
|
|
267
419
|
return existing;
|
|
268
420
|
if (existing !== undefined)
|
|
269
421
|
this.closeSession(session);
|
|
422
|
+
this.closeRequestedWhileOpening.delete(session);
|
|
423
|
+
const task = this.openThreadFor(session, threadId, cwd).finally(() => {
|
|
424
|
+
if (this.opening.get(session)?.task === task)
|
|
425
|
+
this.opening.delete(session);
|
|
426
|
+
});
|
|
427
|
+
this.opening.set(session, { threadId, task });
|
|
428
|
+
const opened = await task;
|
|
429
|
+
if (intent === "operate" && this.open.get(session) !== opened) {
|
|
430
|
+
// 開いている途中に close 要求(購読者が去った等)で畳まれた。操作の相手が要るので開き直す。
|
|
431
|
+
return this.threadFor(session, threadId, cwd, intent);
|
|
432
|
+
}
|
|
433
|
+
return opened;
|
|
434
|
+
}
|
|
435
|
+
async openThreadFor(session, threadId, cwd) {
|
|
270
436
|
const items = new Map();
|
|
271
437
|
const bufferedNotifications = [];
|
|
272
438
|
let notificationTargetReady = false;
|
|
@@ -285,12 +451,18 @@ export class CodexNativeTurnController {
|
|
|
285
451
|
const current = this.open.get(session);
|
|
286
452
|
if (current?.threadId !== threadId)
|
|
287
453
|
return;
|
|
454
|
+
this.rememberPlanRestoreSnapshot(session, threadId, current.thread);
|
|
288
455
|
this.open.delete(session);
|
|
289
456
|
this.resolvePendingQuestionsForSession(session);
|
|
290
457
|
this.onProcessing(session, "done");
|
|
291
458
|
this.onDisconnect(session, error);
|
|
292
459
|
},
|
|
293
460
|
});
|
|
461
|
+
// plan 直前の model / effort は接続をまたいで復元する(接続は購読者ゼロで畳まれるため)。
|
|
462
|
+
const planRestore = this.planRestoreBySession.get(session);
|
|
463
|
+
if (planRestore !== undefined && planRestore.threadId === threadId) {
|
|
464
|
+
thread.seedPlanRestoreSnapshot?.(planRestore.snapshot);
|
|
465
|
+
}
|
|
294
466
|
const opened = {
|
|
295
467
|
threadId,
|
|
296
468
|
cwd,
|
|
@@ -303,10 +475,22 @@ export class CodexNativeTurnController {
|
|
|
303
475
|
planSeq: 0,
|
|
304
476
|
subagents: new CodexSubagentTracker(threadId),
|
|
305
477
|
subagentSeq: 0,
|
|
478
|
+
goal: null,
|
|
479
|
+
goalNoticeKey: null,
|
|
306
480
|
};
|
|
307
481
|
this.open.set(session, opened);
|
|
482
|
+
// 開いている間に close が要求されていた(購読者が去った等)なら、開き終わった接続を残さず畳む。
|
|
483
|
+
if (this.closeRequestedWhileOpening.delete(session)) {
|
|
484
|
+
this.closeSession(session);
|
|
485
|
+
return opened;
|
|
486
|
+
}
|
|
308
487
|
if (opened.activeTurnId !== null)
|
|
309
488
|
this.onProcessing(session, "active");
|
|
489
|
+
// 目標は rollout に現在値が無い(SQLite 保存)ため、開くたびに App Server から読む(失敗無視)。
|
|
490
|
+
// Hub の close 判定(hasActiveGoal)が open 直後から正しい値を返すよう、ここで待つ。
|
|
491
|
+
await this.loadGoal(session, opened);
|
|
492
|
+
if (this.open.get(session) !== opened)
|
|
493
|
+
return opened;
|
|
310
494
|
const restoredSubagents = [];
|
|
311
495
|
for (const item of thread.initialItems ?? []) {
|
|
312
496
|
restoredSubagents.push(...opened.subagents.ingestItem(item, Date.now()));
|
|
@@ -464,25 +648,46 @@ export class CodexNativeTurnController {
|
|
|
464
648
|
const settingsThreadId = params?.["threadId"];
|
|
465
649
|
const settings = asRecord(params?.["threadSettings"]);
|
|
466
650
|
const model = settings?.["model"];
|
|
651
|
+
const ownThread = typeof settingsThreadId !== "string" || settingsThreadId === threadId;
|
|
467
652
|
if (typeof model === "string" && model.length > 0) {
|
|
468
|
-
if (
|
|
653
|
+
if (ownThread) {
|
|
469
654
|
this.onModel(session, model);
|
|
470
655
|
}
|
|
471
656
|
else {
|
|
472
657
|
this.applySubagentModel(session, threadId, settingsThreadId, model);
|
|
473
658
|
}
|
|
474
659
|
}
|
|
660
|
+
if (ownThread && settings !== null && current?.threadId === threadId) {
|
|
661
|
+
// collaboration mode / effort / モデルの実体を turn 開始側へ追従させる(codex-plan-mode)。
|
|
662
|
+
current.thread.noteThreadSettings?.(settings);
|
|
663
|
+
const mode = asRecord(settings["collaborationMode"])?.["mode"];
|
|
664
|
+
if (mode === "plan" || mode === "default")
|
|
665
|
+
this.onCollaborationMode(session, mode);
|
|
666
|
+
}
|
|
475
667
|
}
|
|
476
668
|
if (notification.method === "model/rerouted") {
|
|
477
669
|
const reroutedThreadId = params?.["threadId"];
|
|
478
670
|
const model = params?.["toModel"];
|
|
479
671
|
if (typeof reroutedThreadId === "string" && typeof model === "string" && model.length > 0) {
|
|
480
|
-
if (reroutedThreadId === threadId)
|
|
672
|
+
if (reroutedThreadId === threadId) {
|
|
673
|
+
// 表示だけ更新する。振り替え先を settings.model に採らない(default 毎回明示で恒久化しないため)。
|
|
481
674
|
this.onModel(session, model);
|
|
482
|
-
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
483
677
|
this.applySubagentModel(session, threadId, reroutedThreadId, model);
|
|
678
|
+
}
|
|
484
679
|
}
|
|
485
680
|
}
|
|
681
|
+
if (notification.method === "thread/goal/updated" && lifecycleMatchesThread) {
|
|
682
|
+
const goal = codexGoalFromWire(params?.["goal"]);
|
|
683
|
+
if (goal !== null && current?.threadId === threadId) {
|
|
684
|
+
this.applyGoal(session, current, goal, { publishState: true });
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (notification.method === "thread/goal/cleared" && lifecycleMatchesThread) {
|
|
688
|
+
if (current?.threadId === threadId)
|
|
689
|
+
this.applyGoal(session, current, null, { publishState: true });
|
|
690
|
+
}
|
|
486
691
|
if (notification.method === "thread/tokenUsage/updated") {
|
|
487
692
|
const tokenUsage = asRecord(params?.["tokenUsage"]);
|
|
488
693
|
const last = asRecord(tokenUsage?.["last"]);
|
|
@@ -616,6 +821,11 @@ export function codexItemToChatOutput(item) {
|
|
|
616
821
|
return { type: "chat_output", v: PROTOCOL_V1, streamId: `codex-item-${id}`,
|
|
617
822
|
role: "assistant", text, eof: true };
|
|
618
823
|
}
|
|
824
|
+
if (type === "plan") {
|
|
825
|
+
// プランモードの提案プラン(codex-plan-mode)。rollout の item_completed/Plan と同じ streamId / 本文。
|
|
826
|
+
const text = item["text"];
|
|
827
|
+
return typeof text === "string" ? codexPlanChatOutput(id, text) : null;
|
|
828
|
+
}
|
|
619
829
|
return null;
|
|
620
830
|
}
|
|
621
831
|
export function chatContentKey(payload) {
|