tailii-host 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval/hook.js +11 -1
- package/dist/approval/hook.js.map +1 -1
- package/dist/backend/herdr.d.ts +41 -6
- package/dist/backend/herdr.js +111 -15
- package/dist/backend/herdr.js.map +1 -1
- package/dist/backend/sessionBackend.d.ts +10 -3
- package/dist/backend/sessionBackend.js +5 -2
- package/dist/backend/sessionBackend.js.map +1 -1
- package/dist/backend/tmux.d.ts +45 -1
- package/dist/backend/tmux.js +95 -11
- package/dist/backend/tmux.js.map +1 -1
- package/dist/chat/chatTailController.d.ts +1 -1
- package/dist/chat/chatTailController.js +16 -33
- package/dist/chat/chatTailController.js.map +1 -1
- package/dist/chat/imageService.d.ts +9 -1
- package/dist/chat/imageService.js +39 -0
- package/dist/chat/imageService.js.map +1 -1
- package/dist/chat/subagentTailer.js +23 -4
- package/dist/chat/subagentTailer.js.map +1 -1
- package/dist/chat/transcriptTailer.d.ts +38 -13
- package/dist/chat/transcriptTailer.js +240 -17
- package/dist/chat/transcriptTailer.js.map +1 -1
- package/dist/codex/codexAppServer.d.ts +54 -9
- package/dist/codex/codexAppServer.js +495 -76
- package/dist/codex/codexAppServer.js.map +1 -1
- package/dist/codex/codexNativeTurnController.d.ts +22 -1
- package/dist/codex/codexNativeTurnController.js +57 -8
- package/dist/codex/codexNativeTurnController.js.map +1 -1
- package/dist/codex/codexRolloutTailer.d.ts +2 -0
- package/dist/codex/codexRolloutTailer.js +78 -13
- package/dist/codex/codexRolloutTailer.js.map +1 -1
- package/dist/codex/codexSessionStore.js +54 -16
- package/dist/codex/codexSessionStore.js.map +1 -1
- package/dist/codex/codexSubagentTracker.d.ts +13 -0
- package/dist/codex/codexSubagentTracker.js +55 -1
- package/dist/codex/codexSubagentTracker.js.map +1 -1
- package/dist/codex/codexSystemNotice.d.ts +2 -0
- package/dist/codex/codexSystemNotice.js +20 -3
- package/dist/codex/codexSystemNotice.js.map +1 -1
- package/dist/commands/launch.js +50 -13
- package/dist/commands/launch.js.map +1 -1
- package/dist/engine/context.d.ts +32 -1
- package/dist/engine/context.js +39 -3
- package/dist/engine/context.js.map +1 -1
- package/dist/engine/engine.d.ts +5 -1
- package/dist/engine/engine.js +89 -4
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/handlers/codex.js +37 -0
- package/dist/engine/handlers/codex.js.map +1 -1
- package/dist/engine/handlers/core.js +14 -1
- package/dist/engine/handlers/core.js.map +1 -1
- package/dist/engine/handlers/mode.js +64 -1
- package/dist/engine/handlers/mode.js.map +1 -1
- package/dist/engine/handlers/session.js +17 -33
- package/dist/engine/handlers/session.js.map +1 -1
- package/dist/engine/handlers/workspace.js +22 -1
- package/dist/engine/handlers/workspace.js.map +1 -1
- package/dist/hub/effortDialog.d.ts +15 -0
- package/dist/hub/effortDialog.js +60 -16
- package/dist/hub/effortDialog.js.map +1 -1
- package/dist/hub/engineRelaySocket.d.ts +4 -0
- package/dist/hub/engineRelaySocket.js +6 -1
- package/dist/hub/engineRelaySocket.js.map +1 -1
- package/dist/hub/hubDaemon.js +22 -3
- package/dist/hub/hubDaemon.js.map +1 -1
- package/dist/hub/hubProtocol.d.ts +10 -0
- package/dist/hub/hubProtocol.js +12 -1
- package/dist/hub/hubProtocol.js.map +1 -1
- package/dist/hub/questionInjection.d.ts +12 -0
- package/dist/hub/questionInjection.js +70 -3
- package/dist/hub/questionInjection.js.map +1 -1
- package/dist/hub/sessionHub.d.ts +78 -4
- package/dist/hub/sessionHub.js +281 -67
- package/dist/hub/sessionHub.js.map +1 -1
- package/dist/protocol/decode.js +74 -17
- package/dist/protocol/decode.js.map +1 -1
- package/dist/protocol/encode.js +2 -0
- package/dist/protocol/encode.js.map +1 -1
- package/dist/protocol/messages.d.ts +65 -1
- package/dist/services/fileService.d.ts +15 -0
- package/dist/services/fileService.js +176 -14
- package/dist/services/fileService.js.map +1 -1
- package/dist/services/officialApps.d.ts +12 -1
- package/dist/services/officialApps.js +145 -30
- package/dist/services/officialApps.js.map +1 -1
- package/dist/services/previewServer.js +15 -6
- package/dist/services/previewServer.js.map +1 -1
- package/dist/sessions/transcriptPath.d.ts +3 -0
- package/dist/sessions/transcriptPath.js +15 -0
- package/dist/sessions/transcriptPath.js.map +1 -0
- package/dist/shared/clientBuild.d.ts +44 -0
- package/dist/shared/clientBuild.js +116 -0
- package/dist/shared/clientBuild.js.map +1 -0
- package/package.json +3 -3
package/dist/hub/sessionHub.js
CHANGED
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
// tailii (TS host) — connection 非依存 Session Hub コア。
|
|
3
3
|
import { reaperTick } from "./reaper.js";
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
|
-
import * as os from "node:os";
|
|
6
5
|
import * as path from "node:path";
|
|
7
6
|
import { bumpHeartbeat, listHeartbeatSessions, readHeartbeat, writeHeartbeat } from "../sessions/heartbeat.js";
|
|
8
|
-
import {
|
|
7
|
+
import { ensureDirectory0700 } from "../shared/paths.js";
|
|
9
8
|
import { decodeHubClientLine, encodeHubMessage, } from "./hubProtocol.js";
|
|
10
|
-
import { HISTORY_DONE_STREAM_ID,
|
|
9
|
+
import { HISTORY_DONE_STREAM_ID, findTrailingTurnEndMarkerMs, findTrailingUserPromptText, } from "../chat/transcriptTailer.js";
|
|
11
10
|
import { PROTOCOL_V1, PROTOCOL_V2 } from "../protocol.js";
|
|
12
11
|
import { LoginCodeError } from "../backend/tmux.js";
|
|
13
12
|
import { CodexAppServerManager } from "../codex/codexAppServer.js";
|
|
14
13
|
import { CodexNativeTurnController, chatContentKey, } from "../codex/codexNativeTurnController.js";
|
|
15
14
|
import { CONTEXT_STREAM_ID as CODEX_CONTEXT_STREAM_ID, CONTEXT_WINDOW_STREAM_ID as CODEX_CONTEXT_WINDOW_STREAM_ID, MODEL_STREAM_ID as CODEX_MODEL_STREAM_ID, } from "../codex/codexRolloutTailer.js";
|
|
15
|
+
import { claudeTranscriptPathFor } from "../sessions/transcriptPath.js";
|
|
16
16
|
/**
|
|
17
17
|
* backfilling 中の購読者は publishConversationEvent が丸ごと飛ばす。旗が立ちっぱなしになると
|
|
18
18
|
* 「接続は健全なのに会話だけ永久に更新されない」実障害になるため、履歴が一定時間 1 行も進まな
|
|
@@ -24,17 +24,20 @@ const DELIVERED_RECEIPT_TTL_MS = 30 * 24 * 60 * 60 * 1_000;
|
|
|
24
24
|
const MAX_DELIVERED_RECEIPTS_PER_SESSION = 10_000;
|
|
25
25
|
/** 中断マーカーで done にした後、継続 hook(Pre/PostToolUse)の遅着 active を残響として無視する窓(ms)。 */
|
|
26
26
|
const LATE_HOOK_AFTER_INTERRUPT_MS = 3_000;
|
|
27
|
+
/**
|
|
28
|
+
* 発火時刻付き hook を「既に終わったターンの遅着」とみなす古さの上限(ms)。hook は発火から
|
|
29
|
+
* 100〜300ms(高負荷でも数秒)で relay に届くので、終端マーカーよりそれ以上前に発火した hook が
|
|
30
|
+
* マーカーの後に届くことは無い。上限を切らないと、queued 発話の UserPromptSubmit が enqueue 時に
|
|
31
|
+
* 発火する版が現れた場合に、数秒後の dequeue で始まる生きたターンを idle に固定してしまう。
|
|
32
|
+
*/
|
|
33
|
+
const LATE_HOOK_MAX_AGE_MS = 3_000;
|
|
27
34
|
/** ターンを始めない継続 hook か(新ターンの権威は UserPromptSubmit のみ)。 */
|
|
28
35
|
function isContinuationHookEvent(event) {
|
|
29
36
|
return event === "PreToolUse" || event === "PostToolUse";
|
|
30
37
|
}
|
|
31
|
-
/** 既定の claude transcript
|
|
38
|
+
/** 既定の claude transcript 解決(sessions/transcriptPath.ts と同一規則)。 */
|
|
32
39
|
function defaultTranscriptPathFor(meta) {
|
|
33
|
-
|
|
34
|
-
const sessionId = meta.providerSessionId ?? meta.claudeSessionId;
|
|
35
|
-
if (sessionId === undefined || sessionId.length === 0)
|
|
36
|
-
return null;
|
|
37
|
-
return path.join(os.homedir(), ".claude", "projects", claudeProjectSlug(meta.cwd), `${sessionId}.jsonl`);
|
|
40
|
+
return claudeTranscriptPathFor(meta);
|
|
38
41
|
}
|
|
39
42
|
export class SessionHub {
|
|
40
43
|
options;
|
|
@@ -121,7 +124,10 @@ export class SessionHub {
|
|
|
121
124
|
actor.processingSinceMs = heartbeat.sinceMs ?? this.nowMs();
|
|
122
125
|
}
|
|
123
126
|
}
|
|
124
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* 復元時の照合用: transcript 末尾のターン終端マーカー(中断確定 / API エラー終端)の timestamp
|
|
129
|
+
* (claude 会話のみ。不明は null)。
|
|
130
|
+
*/
|
|
125
131
|
trailingInterruptMarkerMs(session) {
|
|
126
132
|
const meta = this.options.metadataStore.get(session);
|
|
127
133
|
if (meta === null || meta.agent === "codex")
|
|
@@ -129,7 +135,21 @@ export class SessionHub {
|
|
|
129
135
|
const transcriptPath = (this.options.transcriptPathFor ?? defaultTranscriptPathFor)(meta);
|
|
130
136
|
if (transcriptPath === null)
|
|
131
137
|
return null;
|
|
132
|
-
return
|
|
138
|
+
return findTrailingTurnEndMarkerMs(transcriptPath);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* chat 注入前の残存テキスト照合用: transcript 末尾に記録済みの直近の発話本文(claude 会話のみ。
|
|
142
|
+
* 不明は null)。中断で入力欄へ書き戻された配送済み発話を Enter で送り直さないための材料
|
|
143
|
+
* (restored-prompt-discard。詳細は `findTrailingUserPromptText`)。
|
|
144
|
+
*/
|
|
145
|
+
trailingUserPromptText(session) {
|
|
146
|
+
const meta = this.options.metadataStore.get(session);
|
|
147
|
+
if (meta === null || meta.agent === "codex")
|
|
148
|
+
return null;
|
|
149
|
+
const transcriptPath = (this.options.transcriptPathFor ?? defaultTranscriptPathFor)(meta);
|
|
150
|
+
if (transcriptPath === null)
|
|
151
|
+
return null;
|
|
152
|
+
return findTrailingUserPromptText(transcriptPath);
|
|
133
153
|
}
|
|
134
154
|
/** 永続化済み設問を復元する。App Server の request handle は再起動を越せないため TUI のみ対象。 */
|
|
135
155
|
restorePendingQuestions() {
|
|
@@ -268,7 +288,7 @@ export class SessionHub {
|
|
|
268
288
|
void this.drainChatQueue(message.session, actor);
|
|
269
289
|
}
|
|
270
290
|
else if (message.type === "session_processing") {
|
|
271
|
-
if (!this.applyProcessing(message.session, message.state, message.event))
|
|
291
|
+
if (!this.applyProcessing(message.session, message.state, message.event, message.atMs))
|
|
272
292
|
return;
|
|
273
293
|
}
|
|
274
294
|
this.broadcast(message);
|
|
@@ -615,7 +635,7 @@ export class SessionHub {
|
|
|
615
635
|
actor.runtimeClaim = null;
|
|
616
636
|
return;
|
|
617
637
|
}
|
|
618
|
-
if (!this.applyProcessing(message.session, message.state, message.event))
|
|
638
|
+
if (!this.applyProcessing(message.session, message.state, message.event, message.atMs))
|
|
619
639
|
return;
|
|
620
640
|
this.broadcast(message);
|
|
621
641
|
}
|
|
@@ -725,11 +745,18 @@ export class SessionHub {
|
|
|
725
745
|
actor.uncertainCodexMessages.clear();
|
|
726
746
|
actor.deletedCodexMessageIds.clear();
|
|
727
747
|
actor.pendingQuestion = null;
|
|
748
|
+
const retiredSubscribers = [...actor.subscribers.keys()];
|
|
728
749
|
actor.focusedBy.clear();
|
|
729
750
|
actor.subscribers.clear();
|
|
730
751
|
this.actors.delete(session);
|
|
731
752
|
this.persistPendingQuestions();
|
|
732
753
|
this.persistChatReceipts();
|
|
754
|
+
// 購読者(engine)へ購読消滅を知らせる(背景購読の台帳同期。一覧向け liveness とは別経路)。
|
|
755
|
+
// actor を消した後に送る: 受け手が同期的に再購読(前面会話の張り直し)しても、消える直前の
|
|
756
|
+
// 古い actor に付いて一緒に捨てられないように(新しい actor が生成される)。
|
|
757
|
+
for (const subscriber of retiredSubscribers) {
|
|
758
|
+
this.sendTo(subscriber, { type: "conversation_retired", session });
|
|
759
|
+
}
|
|
733
760
|
}
|
|
734
761
|
subscribe(client, session, afterSeq, newerThanMs, preview) {
|
|
735
762
|
const actor = this.actor(session);
|
|
@@ -873,7 +900,9 @@ export class SessionHub {
|
|
|
873
900
|
}
|
|
874
901
|
startCodexSharedStream(session, actor, cwd, threadId, newerThanMs) {
|
|
875
902
|
const state = {
|
|
876
|
-
phase: "
|
|
903
|
+
phase: "backfill",
|
|
904
|
+
snapshotReady: false,
|
|
905
|
+
historyDone: false,
|
|
877
906
|
initialItemIds: new Set(),
|
|
878
907
|
initialContentCounts: new Map(),
|
|
879
908
|
buffered: [],
|
|
@@ -882,12 +911,21 @@ export class SessionHub {
|
|
|
882
911
|
scanContentCounts: new Map(),
|
|
883
912
|
fallbackBaselineCounts: new Map(),
|
|
884
913
|
disconnected: false,
|
|
914
|
+
lastModel: null,
|
|
915
|
+
subscribedModel: null,
|
|
916
|
+
scanLastModel: null,
|
|
885
917
|
};
|
|
886
918
|
actor.codexLive = state;
|
|
919
|
+
// 履歴表示は rollout だけで完結する。App Server の購読(daemon 起動・接続・thread/resume・
|
|
920
|
+
// 履歴スナップショット。巨大 thread では items/list が 3 秒で timeout してから復旧接続へ進む)
|
|
921
|
+
// の完了を待ってから rollout を開くと、その間 iOS は「会話を読み込み中…」のまま止まって
|
|
922
|
+
// 見える。rollout の backfill を先に始め、購読は並行して確立する。境界照合(flushCodexBuffer)
|
|
923
|
+
// は history-done と snapshot の両方が揃った時点で行う(順序不定)。
|
|
924
|
+
this.openCodexRollout(session, actor, cwd, threadId, newerThanMs);
|
|
887
925
|
const controller = this.ensureCodexTurnController();
|
|
888
926
|
const subscribe = controller.subscribeSession;
|
|
889
927
|
if (subscribe === undefined) {
|
|
890
|
-
this.
|
|
928
|
+
this.degradeCodexToFallback(session, actor, state, null);
|
|
891
929
|
return;
|
|
892
930
|
}
|
|
893
931
|
void subscribe.call(controller, { session, threadId, cwd }).then((snapshot) => {
|
|
@@ -895,20 +933,61 @@ export class SessionHub {
|
|
|
895
933
|
return;
|
|
896
934
|
state.initialItemIds = snapshot.itemIds;
|
|
897
935
|
state.initialContentCounts = snapshot.contentCounts;
|
|
936
|
+
// 会話を開いていない間の /model や設定変更は thread/settings/updated が誰にも届かない。
|
|
937
|
+
// resume 応答の現在モデルを backfill 完了後に配信し、開き直しで表示を実体へ揃える。
|
|
938
|
+
state.subscribedModel = snapshot.model ?? null;
|
|
898
939
|
if (!snapshot.liveSubscribed) {
|
|
899
|
-
this.options.log?.(
|
|
900
|
-
|
|
940
|
+
this.options.log?.(`Codex App Server の履歴スナップショットが読めない(turn 前の未materialize か履歴読み取り失敗)ため、rollout fallbackへ移行 session=${session} thread=${threadId}` +
|
|
941
|
+
(snapshot.liveSubscriptionError ? `(理由: ${snapshot.liveSubscriptionError})` : ""));
|
|
942
|
+
this.degradeCodexToFallback(session, actor, state, null);
|
|
901
943
|
return;
|
|
902
944
|
}
|
|
903
|
-
state.
|
|
904
|
-
|
|
945
|
+
state.snapshotReady = true;
|
|
946
|
+
if (state.historyDone)
|
|
947
|
+
this.finishCodexBackfill(session, actor, state);
|
|
905
948
|
}, (error) => {
|
|
906
949
|
if (actor.codexLive !== state || actor.subscribers.size === 0)
|
|
907
950
|
return;
|
|
908
|
-
this.
|
|
909
|
-
this.startCodexFallback(session, actor, cwd, threadId, newerThanMs, false);
|
|
951
|
+
this.degradeCodexToFallback(session, actor, state, `Codex App Server 購読失敗、rollout fallback へ移行: ${String(error)}`);
|
|
910
952
|
});
|
|
911
953
|
}
|
|
954
|
+
/**
|
|
955
|
+
* 購読が成立しない(未materialize・履歴読み取り失敗・購読エラー)と分かった時点で、既に開いて
|
|
956
|
+
* いる rollout tail をそのまま一次ソースに降格する。tail を開き直すと履歴を頭から二重再生する
|
|
957
|
+
* ため再利用する(接続断と同じ扱い: history-done で fallback-live へ、到達済みなら即座に)。
|
|
958
|
+
*/
|
|
959
|
+
degradeCodexToFallback(session, actor, state, logMessage) {
|
|
960
|
+
if (logMessage !== null)
|
|
961
|
+
this.options.log?.(logMessage);
|
|
962
|
+
state.disconnected = true;
|
|
963
|
+
state.buffered.length = 0;
|
|
964
|
+
if (state.phase === "backfill" && state.historyDone)
|
|
965
|
+
this.finishCodexBackfill(session, actor, state);
|
|
966
|
+
}
|
|
967
|
+
/** backfill 完了(history-done)と購読の確定(snapshot または fallback)が揃った時点の切り替え。 */
|
|
968
|
+
finishCodexBackfill(session, actor, state) {
|
|
969
|
+
// 履歴末尾の turn_context より新しい設定変更(turn 未実行)は rollout に無い。
|
|
970
|
+
// 購読時の thread モデルで上書きする(同値なら畳む)。fallback でも resume が
|
|
971
|
+
// 成立していれば同じ。
|
|
972
|
+
const subscribedModel = state.subscribedModel;
|
|
973
|
+
state.subscribedModel = null;
|
|
974
|
+
if (state.disconnected) {
|
|
975
|
+
// fallback 確定前に届いた App Server item は、この継続 rollout と同じ内容を
|
|
976
|
+
// 別 streamId で持ち得る。rollout を唯一の一次ソースにした時点で破棄し、
|
|
977
|
+
// history 完了時に flush して会話全体を二重表示しない。
|
|
978
|
+
state.buffered.length = 0;
|
|
979
|
+
state.phase = "fallback-live"; // この tail は既に同じ EOF 境界にいるので継続利用する。
|
|
980
|
+
}
|
|
981
|
+
else {
|
|
982
|
+
this.flushCodexBuffer(session, actor, state);
|
|
983
|
+
state.phase = "live";
|
|
984
|
+
// 本文は以後 App Server のみを採用するが、rollout tail 自体は lifecycle の
|
|
985
|
+
// 副監視として残す。ここで止めると、後続 turn の App Server `turn/completed`
|
|
986
|
+
// 欠落時に terminal event(task_complete / turn_aborted)を観測できず、
|
|
987
|
+
// 処理中状態を自己修復できない。
|
|
988
|
+
}
|
|
989
|
+
this.publishCodexModelMarker(session, actor, state, subscribedModel);
|
|
990
|
+
}
|
|
912
991
|
openCodexRollout(session, actor, cwd, threadId, newerThanMs) {
|
|
913
992
|
if (this.options.tailFactory === undefined)
|
|
914
993
|
return;
|
|
@@ -921,11 +1000,27 @@ export class SessionHub {
|
|
|
921
1000
|
return;
|
|
922
1001
|
const isHistoryDone = payload.type === "chat_output" && payload.streamId === HISTORY_DONE_STREAM_ID;
|
|
923
1002
|
const contentKey = chatContentKey(payload);
|
|
1003
|
+
const isModelMarker = payload.type === "chat_output" && payload.streamId === CODEX_MODEL_STREAM_ID;
|
|
1004
|
+
// rollout の turn_context は「その turn で実際に使ったモデル」の唯一の記録。live 中も
|
|
1005
|
+
// fallback 中も本文とは独立に通し、App Server 通知(onModel)と同値なら畳む。
|
|
1006
|
+
// token 系 marker は App Server の tokenUsage と意味が異なるため従来どおり混ぜない。
|
|
1007
|
+
if (isModelMarker && state.phase !== "backfill") {
|
|
1008
|
+
if (state.phase === "fallback-scan") {
|
|
1009
|
+
// 再走査は履歴を頭から読み直す。旧 turn のモデルを一度配って戻すのではなく、
|
|
1010
|
+
// 走査完了時に最後の値だけを配る。
|
|
1011
|
+
state.scanLastModel = payload.text;
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
this.publishCodexModelMarker(session, actor, state, payload.text);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
924
1017
|
if (state.phase === "live")
|
|
925
1018
|
return; // 正常時の live 本文は App Server のみ。
|
|
926
1019
|
if (state.phase === "fallback-scan") {
|
|
927
1020
|
if (isHistoryDone) {
|
|
928
1021
|
state.phase = "fallback-live";
|
|
1022
|
+
this.publishCodexModelMarker(session, actor, state, state.scanLastModel);
|
|
1023
|
+
state.scanLastModel = null;
|
|
929
1024
|
return; // 通常 backfill の pc:history-done を再送しない。
|
|
930
1025
|
}
|
|
931
1026
|
if (contentKey === null)
|
|
@@ -942,30 +1037,22 @@ export class SessionHub {
|
|
|
942
1037
|
this.publishCodexContent(session, actor, state, payload);
|
|
943
1038
|
return;
|
|
944
1039
|
}
|
|
945
|
-
// App Server
|
|
1040
|
+
// rollout を EOF まで読みつつ、App Server 購読は並行して確立し通知を buffer する。
|
|
946
1041
|
// resume 応答の item ID / content occurrence を境界スナップショットとして使い、
|
|
947
|
-
//
|
|
948
|
-
// item ID が無いため、同文は Set ではなく occurrence count で照合する。
|
|
1042
|
+
// 両方が揃った時点で rollout にまだ無い buffered item だけを flush する。event_msg
|
|
1043
|
+
// 自体には item ID が無いため、同文は Set ではなく occurrence count で照合する。
|
|
1044
|
+
// snapshot がまだなら backfill のまま留まり、EOF 以降に rollout へ追記された行も
|
|
1045
|
+
// この経路で配信する(snapshot 側の occurrence と後で照合されるので重複しない)。
|
|
949
1046
|
this.publishConversationEvent(session, actor, payload);
|
|
950
1047
|
if (contentKey !== null)
|
|
951
1048
|
incrementCount(state.publishedContentCounts, contentKey);
|
|
1049
|
+
if (isModelMarker)
|
|
1050
|
+
state.lastModel = payload.text;
|
|
952
1051
|
if (!isHistoryDone)
|
|
953
1052
|
return;
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
// history 完了時に flush して会話全体を二重表示しない。
|
|
958
|
-
state.buffered.length = 0;
|
|
959
|
-
state.phase = "fallback-live"; // この tail は既に同じ EOF 境界にいるので継続利用する。
|
|
960
|
-
}
|
|
961
|
-
else {
|
|
962
|
-
this.flushCodexBuffer(session, actor, state);
|
|
963
|
-
state.phase = "live";
|
|
964
|
-
// 本文は以後 App Server のみを採用するが、rollout tail 自体は lifecycle の
|
|
965
|
-
// 副監視として残す。ここで止めると、後続 turn の App Server `turn/completed`
|
|
966
|
-
// 欠落時に terminal event(task_complete / turn_aborted)を観測できず、
|
|
967
|
-
// 処理中状態を自己修復できない。
|
|
968
|
-
}
|
|
1053
|
+
state.historyDone = true;
|
|
1054
|
+
if (state.snapshotReady || state.disconnected)
|
|
1055
|
+
this.finishCodexBackfill(session, actor, state);
|
|
969
1056
|
}, (event) => {
|
|
970
1057
|
if (actor.codexLive !== state || event.state !== "done")
|
|
971
1058
|
return;
|
|
@@ -989,7 +1076,7 @@ export class SessionHub {
|
|
|
989
1076
|
// 一次ソースにし、運良く届いた App Server item を backfill buffer へ混ぜて重複させない。
|
|
990
1077
|
if (state.disconnected)
|
|
991
1078
|
return;
|
|
992
|
-
if (state.phase === "
|
|
1079
|
+
if (state.phase === "backfill") {
|
|
993
1080
|
// tool_activity は buffer せず捨てる。履歴は rollout(backfill)側が同じカードを
|
|
994
1081
|
// 供給するのが正で、live 由来と rollout 由来のカードは id が一致しないため
|
|
995
1082
|
// snapshot occurrence 照合に混ぜると開くたびに重複しうる。backfill 中に完了した
|
|
@@ -1053,26 +1140,32 @@ export class SessionHub {
|
|
|
1053
1140
|
const state = actor?.codexLive;
|
|
1054
1141
|
if (actor === undefined || state == null || actor.subscribers.size === 0)
|
|
1055
1142
|
return;
|
|
1056
|
-
|
|
1057
|
-
state.
|
|
1058
|
-
|
|
1143
|
+
const logMessage = `Codex App Server 接続断、rollout fallback へ移行: ${String(error)}`;
|
|
1144
|
+
if (state.phase === "backfill") {
|
|
1145
|
+
// backfill 中(snapshot 待ちを含む)の切断は、開いている rollout tail をそのまま一次ソースにする。
|
|
1146
|
+
this.degradeCodexToFallback(session, actor, state, logMessage);
|
|
1059
1147
|
return;
|
|
1148
|
+
}
|
|
1149
|
+
this.options.log?.(logMessage);
|
|
1150
|
+
state.disconnected = true;
|
|
1060
1151
|
if (state.phase !== "live")
|
|
1061
1152
|
return;
|
|
1062
1153
|
const meta = this.options.metadataStore.get(session);
|
|
1063
1154
|
if (meta === null)
|
|
1064
1155
|
return;
|
|
1065
|
-
this.
|
|
1156
|
+
this.startCodexFallbackRescan(session, actor, meta.cwd, meta.providerSessionId ?? meta.claudeSessionId ?? null, null);
|
|
1066
1157
|
}
|
|
1067
|
-
|
|
1158
|
+
/** live 移行後の接続断: rollout を頭から再走査し、配信済み本文を occurrence 照合で除外して継続する。 */
|
|
1159
|
+
startCodexFallbackRescan(session, actor, cwd, threadId, newerThanMs) {
|
|
1068
1160
|
const state = actor.codexLive;
|
|
1069
1161
|
if (state === null)
|
|
1070
1162
|
return;
|
|
1071
1163
|
actor.tail?.stop();
|
|
1072
|
-
state.phase =
|
|
1164
|
+
state.phase = "fallback-scan";
|
|
1073
1165
|
state.disconnected = true;
|
|
1074
1166
|
state.buffered.length = 0;
|
|
1075
1167
|
state.scanContentCounts.clear();
|
|
1168
|
+
state.scanLastModel = null;
|
|
1076
1169
|
state.fallbackBaselineCounts = new Map(state.publishedContentCounts);
|
|
1077
1170
|
this.openCodexRollout(session, actor, cwd, threadId, newerThanMs);
|
|
1078
1171
|
}
|
|
@@ -1081,6 +1174,16 @@ export class SessionHub {
|
|
|
1081
1174
|
if (key !== null)
|
|
1082
1175
|
incrementCount(state.publishedContentCounts, key);
|
|
1083
1176
|
this.publishConversationEvent(session, actor, payload);
|
|
1177
|
+
if (payload.type === "chat_output" && payload.role === "user" && this.options.imageService !== undefined) {
|
|
1178
|
+
// 本文配信は止めず、完成したサムネを同じ streamId へ後着させる。
|
|
1179
|
+
void this.options.imageService.makeAttachmentsAvailable(payload.text, payload.streamId).then((images) => {
|
|
1180
|
+
// サムネ生成中の離脱/再購読/actor 破棄で、古い会話へ届かせない。
|
|
1181
|
+
if (this.actors.get(session) !== actor || actor.codexLive !== state || actor.subscribers.size === 0)
|
|
1182
|
+
return;
|
|
1183
|
+
for (const image of images)
|
|
1184
|
+
this.publishConversationEvent(session, actor, image);
|
|
1185
|
+
}, (error) => this.options.log?.(`Codex 添付サムネ生成失敗 session=${session}: ${String(error)}`));
|
|
1186
|
+
}
|
|
1084
1187
|
}
|
|
1085
1188
|
publishConversationEvent(session, actor, payload) {
|
|
1086
1189
|
const event = { serverSeq: actor.nextServerSeq++, payload };
|
|
@@ -1137,6 +1240,13 @@ export class SessionHub {
|
|
|
1137
1240
|
ownState.backfilling = false;
|
|
1138
1241
|
if (reason !== "history-done") {
|
|
1139
1242
|
this.options.log?.(`audit backfill-force-finish session=${session} reason=${reason}`);
|
|
1243
|
+
if (meta.agent === "codex") {
|
|
1244
|
+
this.sendTo(client, {
|
|
1245
|
+
type: "conversation_event", session, serverSeq: 0,
|
|
1246
|
+
payload: { type: "chat_output", v: PROTOCOL_V1,
|
|
1247
|
+
streamId: "pc:history-cancelled", role: "system", text: "", eof: true },
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1140
1250
|
}
|
|
1141
1251
|
if (actor.codexLive !== null) {
|
|
1142
1252
|
// codex は rollout(履歴) と app-server(live) が同じ発話を二重に表すため、履歴側が
|
|
@@ -1170,6 +1280,14 @@ export class SessionHub {
|
|
|
1170
1280
|
this.sendTo(client, { type: "conversation_event", session, ...event });
|
|
1171
1281
|
}
|
|
1172
1282
|
}
|
|
1283
|
+
// 後から加わった client の履歴には、turn を伴わない設定変更(境界前に配信済みの
|
|
1284
|
+
// thread/settings/updated)が含まれない。会話の現在モデルを最後に 1 回添えて揃える。
|
|
1285
|
+
const currentModel = actor.codexLive?.lastModel ?? null;
|
|
1286
|
+
if (currentModel !== null) {
|
|
1287
|
+
this.sendTo(client, {
|
|
1288
|
+
type: "conversation_event", session, serverSeq: 0, payload: codexModelMarker(currentModel),
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1173
1291
|
};
|
|
1174
1292
|
// 進捗は「最後に 1 行流れた時刻」で測る。履歴行ごとに setTimeout を張り替えると
|
|
1175
1293
|
// 巨大な transcript でタイマー生成が支配的になるため、タイマーは 1 本のまま残時間を見る。
|
|
@@ -1290,6 +1408,7 @@ export class SessionHub {
|
|
|
1290
1408
|
let actor = this.actors.get(session);
|
|
1291
1409
|
if (actor === undefined) {
|
|
1292
1410
|
actor = { pendingQuestion: null, processingSince: null, processingSinceMs: null, lastInterruptDoneMs: null,
|
|
1411
|
+
lastTurnEndMs: null, turnStartFiredAtMs: null, lastTurnStartMs: null,
|
|
1293
1412
|
focusedBy: new Set(), subscribers: new Map(),
|
|
1294
1413
|
nextServerSeq: 1, replayBuffer: [], tail: null, tailRetryTimer: null,
|
|
1295
1414
|
previewPump: null, backfillTails: new Map(),
|
|
@@ -1472,7 +1591,9 @@ export class SessionHub {
|
|
|
1472
1591
|
}
|
|
1473
1592
|
continue;
|
|
1474
1593
|
}
|
|
1475
|
-
await (this.options.chatInjector?.(message.text, session
|
|
1594
|
+
await (this.options.chatInjector?.(message.text, session, {
|
|
1595
|
+
recordedPromptText: () => this.trailingUserPromptText(session),
|
|
1596
|
+
}) ?? Promise.resolve());
|
|
1476
1597
|
// 明示 kill / reaper kill が注入 await 中に actor を廃棄した場合、同名の新 actorへ
|
|
1477
1598
|
// 古い receipt を復活させない。waiter は retireSession が既に失敗で解放している。
|
|
1478
1599
|
if (this.actors.get(session) !== actor)
|
|
@@ -1535,11 +1656,13 @@ export class SessionHub {
|
|
|
1535
1656
|
ensureCodexTurnController() {
|
|
1536
1657
|
if (this.codexTurnController !== null)
|
|
1537
1658
|
return this.codexTurnController;
|
|
1538
|
-
const appServer = (this.options.codexAppServerFactory ??
|
|
1659
|
+
const appServer = (this.options.codexAppServerFactory ??
|
|
1660
|
+
(() => new CodexAppServerManager({ ...(this.options.log ? { log: this.options.log } : {}) })))();
|
|
1539
1661
|
const create = this.options.codexTurnControllerFactory ??
|
|
1540
1662
|
((options) => new CodexNativeTurnController(options));
|
|
1541
1663
|
this.codexTurnController = create({
|
|
1542
1664
|
appServer,
|
|
1665
|
+
log: (message) => this.options.log?.(message),
|
|
1543
1666
|
onProcessing: (session, state) => {
|
|
1544
1667
|
if (state === "active")
|
|
1545
1668
|
this.activeCodexTurns.add(session);
|
|
@@ -1677,10 +1800,25 @@ export class SessionHub {
|
|
|
1677
1800
|
}
|
|
1678
1801
|
publishCodexMarker(session, streamId, text) {
|
|
1679
1802
|
const actor = this.actor(session);
|
|
1803
|
+
if (streamId === CODEX_MODEL_STREAM_ID && actor.codexLive !== null) {
|
|
1804
|
+
this.publishCodexModelMarker(session, actor, actor.codexLive, text);
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1680
1807
|
this.publishConversationEvent(session, actor, {
|
|
1681
1808
|
type: "chat_output", v: PROTOCOL_V1, streamId, role: "system", text, eof: true,
|
|
1682
1809
|
});
|
|
1683
1810
|
}
|
|
1811
|
+
/**
|
|
1812
|
+
* 利用中モデルの marker を、直前に配信した値と異なるときだけ配信する。App Server 通知
|
|
1813
|
+
* (thread/settings/updated / model/rerouted)・rollout の turn_context・購読時の
|
|
1814
|
+
* thread モデルは同じ値を別経路で運ぶため、ここで 1 本に畳む。
|
|
1815
|
+
*/
|
|
1816
|
+
publishCodexModelMarker(session, actor, state, model) {
|
|
1817
|
+
if (model === null || model.length === 0 || state.lastModel === model)
|
|
1818
|
+
return;
|
|
1819
|
+
state.lastModel = model;
|
|
1820
|
+
this.publishConversationEvent(session, actor, codexModelMarker(model));
|
|
1821
|
+
}
|
|
1684
1822
|
/**
|
|
1685
1823
|
* transcript の中断確定マーカーで処理完了を補完する。Claude Code の Stop hook は利用者の中断
|
|
1686
1824
|
* (Esc / Ctrl-C)では発火しないため、hook だけでは処理中状態がターン終了後も残り、iOS は
|
|
@@ -1699,42 +1837,116 @@ export class SessionHub {
|
|
|
1699
1837
|
* その dismiss と chat queue の drain)。中断はターン終了なので Stop 後と同じ振る舞い。
|
|
1700
1838
|
*/
|
|
1701
1839
|
handleClaudeTurnLifecycle(session, actor, event) {
|
|
1702
|
-
|
|
1703
|
-
|
|
1840
|
+
// API エラー終端(使用量上限 429「You've hit your session limit」等)も中断と同じ扱い: Stop hook が
|
|
1841
|
+
// 発火しないままターンが終わるため、hub の処理中状態がそのまま残る(実機 2026-09-07: 制限到達後の
|
|
1842
|
+
// 会話が停止ボタン点灯のまま固まり、開き直しても履歴再生が走らず送信が queued で止まった)。
|
|
1704
1843
|
if (this.actors.get(session) !== actor)
|
|
1705
1844
|
return;
|
|
1706
|
-
if (actor.processingSince === null)
|
|
1707
|
-
return;
|
|
1708
1845
|
if (this.options.metadataStore.get(session)?.agent === "codex")
|
|
1709
1846
|
return;
|
|
1847
|
+
if (event.kind === "turn_start") {
|
|
1848
|
+
// 発話の観測(履歴再生も含めて最新値を保つ)。処理中状態は動かさない(権威は hook)。
|
|
1849
|
+
actor.lastTurnStartMs = Math.max(actor.lastTurnStartMs ?? 0, event.atMs);
|
|
1850
|
+
return;
|
|
1851
|
+
}
|
|
1852
|
+
if (event.kind !== "interrupted" && event.kind !== "api_error")
|
|
1853
|
+
return;
|
|
1854
|
+
if (actor.processingSince === null) {
|
|
1855
|
+
// 処理中でなくても終端の時刻は覚える: 終端より前に発火した hook(別プロセス起動 + relay で遅着する
|
|
1856
|
+
// UserPromptSubmit / Pre・PostToolUse)は既に終わったターンの残響として無視する
|
|
1857
|
+
// (late-hook-before-turn-end)。連続 API エラー(実測 2026-09-06: エラー行の 69ms 後に queued 発話が
|
|
1858
|
+
// 自動 dequeue され 1.15s 後に再びエラー)で 2 本目のマーカーが新ターンの hook より先に届いても、
|
|
1859
|
+
// その hook の発火時刻はマーカーより古いので処理中へ戻さない。
|
|
1860
|
+
actor.lastTurnEndMs = Math.max(actor.lastTurnEndMs ?? 0, event.atMs);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1710
1863
|
if (actor.processingSinceMs !== null && event.atMs < actor.processingSinceMs)
|
|
1711
1864
|
return;
|
|
1712
|
-
this.options.log?.(
|
|
1713
|
-
actor.
|
|
1865
|
+
this.options.log?.(`${event.kind === "api_error" ? "api-error" : "interrupt"} marker で処理完了を補完 session=${session}`);
|
|
1866
|
+
actor.lastTurnEndMs = Math.max(actor.lastTurnEndMs ?? 0, event.atMs);
|
|
1867
|
+
// 3 秒の残響窓(発火時刻を持たない旧 hook 向けのフォールバック)は利用者の中断だけに武装する。
|
|
1868
|
+
// API エラー後は queued 発話の自動 dequeue や task-notification の再開が数十 ms で始まり得る
|
|
1869
|
+
// (実測 51〜69ms)ため、窓を張ると生きた新ターンの継続 hook を握り潰して idle へ書き戻す。
|
|
1870
|
+
if (event.kind === "interrupted")
|
|
1871
|
+
actor.lastInterruptDoneMs = this.nowMs();
|
|
1714
1872
|
this.applyProcessing(session, "done");
|
|
1715
1873
|
this.broadcast({ type: "session_processing", session, state: "done" });
|
|
1716
1874
|
}
|
|
1875
|
+
/** 遅着 hook が直書きした active の heartbeat を idle へ戻す(その hook の書込のままのときだけ)。 */
|
|
1876
|
+
revertLateHookHeartbeat(session, event, reason) {
|
|
1877
|
+
// 別の書込(新ターンの UserPromptSubmit 等)に置き換わっていれば触らない(生きたターンの reaper
|
|
1878
|
+
// 保護を剥がさない)。
|
|
1879
|
+
try {
|
|
1880
|
+
const latest = readHeartbeat(this.options.heartbeatDir, session);
|
|
1881
|
+
if (latest !== null && latest.state === "active" && latest.event === event) {
|
|
1882
|
+
writeHeartbeat(this.options.heartbeatDir, session, { ts: this.now(), state: "idle", event: reason });
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
catch (error) {
|
|
1886
|
+
this.options.log?.(`heartbeat 書込失敗: ${String(error)}`);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1717
1889
|
/**
|
|
1718
1890
|
* 処理中状態を反映する。戻り値 false は「中断済みターンの残響として無視した」(呼び手は
|
|
1719
1891
|
* broadcast しない)。`event` は hook 名(relay 由来。codex controller / 旧 hook は undefined)。
|
|
1720
1892
|
*/
|
|
1721
|
-
applyProcessing(session, state, event) {
|
|
1893
|
+
applyProcessing(session, state, event, atMs) {
|
|
1722
1894
|
const actor = this.actor(session);
|
|
1723
1895
|
if (state === "active") {
|
|
1896
|
+
// 発火時刻付きの hook(hook プロセスの開始時刻。node 起動遅延を含まない)が transcript の最新の
|
|
1897
|
+
// ターン終端(中断確定 / API エラー)より前(かつ配送遅延として説明できる古さ)なら、既に終わった
|
|
1898
|
+
// ターンの遅着 hook。UserPromptSubmit でも同じ(連続 API エラーで自動 dequeue されたターンの hook
|
|
1899
|
+
// が、その終端マーカーより後に届く実測)。処理中(新ターン確定後)に届く古い hook は処理中を
|
|
1900
|
+
// 続けるだけなので無視しない。
|
|
1901
|
+
// UserPromptSubmit は、終端マーカーより後に新しい発話が transcript に現れていれば新ターン
|
|
1902
|
+
// (queued 発話の dequeue)として採用する。発火時刻だけでは「終わったターンの遅着」と
|
|
1903
|
+
// 「enqueue 時に発火して dequeue まで待った発話」を区別できないため、transcript を権威にする。
|
|
1904
|
+
const newerTurnObserved = actor.lastTurnStartMs !== null && actor.lastTurnEndMs !== null &&
|
|
1905
|
+
actor.lastTurnStartMs > actor.lastTurnEndMs;
|
|
1906
|
+
if (actor.processingSince === null && atMs !== undefined && actor.lastTurnEndMs !== null &&
|
|
1907
|
+
atMs < actor.lastTurnEndMs && actor.lastTurnEndMs - atMs <= LATE_HOOK_MAX_AGE_MS &&
|
|
1908
|
+
!(event === "UserPromptSubmit" && newerTurnObserved)) {
|
|
1909
|
+
this.options.log?.(`audit late-hook-before-turn-end ignored session=${session} event=${event} firedAtMs=${atMs} turnEndMs=${actor.lastTurnEndMs}`);
|
|
1910
|
+
// hook が直書きした active の heartbeat も戻す(reaper の bump 代行で不死化させない)。
|
|
1911
|
+
this.revertLateHookHeartbeat(session, event, "late-hook-before-turn-end");
|
|
1912
|
+
return false;
|
|
1913
|
+
}
|
|
1724
1914
|
// 中断直後に遅着する継続 hook(Pre/PostToolUse)は中断済みターンの残響。hook は別プロセス
|
|
1725
1915
|
// 起動+relay で 100〜300ms 遅れるため、マーカー書込より後に届き得る。採用すると Stop hook の
|
|
1726
1916
|
// 無いターンが永久に処理中へ戻る(停止ボタン再点灯)。新ターンの開始は UserPromptSubmit
|
|
1727
|
-
// だけが権威(中断→即再送信は通常どおり active
|
|
1917
|
+
// だけが権威(中断→即再送信は通常どおり active)。発火時刻の有無に関わらず 3 秒窓を保つ
|
|
1918
|
+
// (Esc で abort されたツールの PostToolUse がマーカーの数百 ms 後に発火し得るため、発火時刻で
|
|
1919
|
+
// 窓を狭めない。実機 09-02 の根治を退行させない)。
|
|
1728
1920
|
if (isContinuationHookEvent(event) && actor.processingSince === null && actor.lastInterruptDoneMs !== null &&
|
|
1729
1921
|
this.nowMs() - actor.lastInterruptDoneMs < LATE_HOOK_AFTER_INTERRUPT_MS) {
|
|
1730
1922
|
this.options.log?.(`audit late-hook-after-interrupt ignored session=${session} event=${event}`);
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1923
|
+
this.revertLateHookHeartbeat(session, event, "late-hook-after-interrupt");
|
|
1924
|
+
return false;
|
|
1925
|
+
}
|
|
1926
|
+
actor.processingSince = this.now();
|
|
1927
|
+
// ターン開始は UserPromptSubmit で確定し、継続 hook では進めない(未確定なら今)。
|
|
1928
|
+
if (event === "UserPromptSubmit")
|
|
1929
|
+
actor.turnStartFiredAtMs = atMs ?? null;
|
|
1930
|
+
actor.processingSinceMs =
|
|
1931
|
+
event === "UserPromptSubmit" || actor.processingSinceMs === null ? this.nowMs() : actor.processingSinceMs;
|
|
1932
|
+
}
|
|
1933
|
+
else {
|
|
1934
|
+
// 前ターンの Stop が新ターンの UserPromptSubmit より後に届いた(queued 発話の自動 dequeue で
|
|
1935
|
+
// 両 hook がほぼ同時に発火し、到着順が入れ替わる)場合、現ターンを落とさない
|
|
1936
|
+
// (late-stop-before-turn-start)。発火時刻は現ターンの UserPromptSubmit の発火時刻と比べる
|
|
1937
|
+
// (hub 到着時刻と比べると、短いターンの正規の Stop が UserPromptSubmit の到着前に発火して
|
|
1938
|
+
// 誤って捨てられる)。
|
|
1939
|
+
if (atMs !== undefined && actor.turnStartFiredAtMs !== null && atMs < actor.turnStartFiredAtMs) {
|
|
1940
|
+
this.options.log?.(`audit late-stop-before-turn-start ignored session=${session} event=${event} firedAtMs=${atMs} turnStartMs=${actor.turnStartFiredAtMs}`);
|
|
1941
|
+
// Stop hook は relay より先に heartbeat を idle で直書きする(sinceMs も落ちる)。無視した Stop の
|
|
1942
|
+
// 書込のままなら現ターンの active(開始時刻付き)へ戻す(hub 再起動時の復元と reaper 保護を
|
|
1943
|
+
// 失わないように)。別の書込に置き換わっていれば触らない。
|
|
1734
1944
|
try {
|
|
1735
1945
|
const latest = readHeartbeat(this.options.heartbeatDir, session);
|
|
1736
|
-
if (latest !== null && latest.state === "
|
|
1737
|
-
writeHeartbeat(this.options.heartbeatDir, session, { ts: this.now(), state: "
|
|
1946
|
+
if (latest !== null && latest.state === "idle" && latest.event === event) {
|
|
1947
|
+
writeHeartbeat(this.options.heartbeatDir, session, { ts: this.now(), state: "active",
|
|
1948
|
+
event: "hub-processing",
|
|
1949
|
+
...(actor.processingSinceMs !== null ? { sinceMs: actor.processingSinceMs } : {}) });
|
|
1738
1950
|
}
|
|
1739
1951
|
}
|
|
1740
1952
|
catch (error) {
|
|
@@ -1742,14 +1954,9 @@ export class SessionHub {
|
|
|
1742
1954
|
}
|
|
1743
1955
|
return false;
|
|
1744
1956
|
}
|
|
1745
|
-
actor.processingSince = this.now();
|
|
1746
|
-
// ターン開始は UserPromptSubmit で確定し、継続 hook では進めない(未確定なら今)。
|
|
1747
|
-
actor.processingSinceMs =
|
|
1748
|
-
event === "UserPromptSubmit" || actor.processingSinceMs === null ? this.nowMs() : actor.processingSinceMs;
|
|
1749
|
-
}
|
|
1750
|
-
else {
|
|
1751
1957
|
actor.processingSince = null;
|
|
1752
1958
|
actor.processingSinceMs = null;
|
|
1959
|
+
actor.turnStartFiredAtMs = null;
|
|
1753
1960
|
}
|
|
1754
1961
|
// 一覧 watch 中は処理開始/完了で pump を起動/停止する(前面購読とは独立)。
|
|
1755
1962
|
this.syncPreview(session, actor);
|
|
@@ -2079,6 +2286,13 @@ function sameCodexRetry(left, right) {
|
|
|
2079
2286
|
left.sandbox === right.sandbox &&
|
|
2080
2287
|
left.threadId === right.threadId && left.cwd === right.cwd;
|
|
2081
2288
|
}
|
|
2289
|
+
/** 利用中モデル通知(`pc:model`)の conversation_event payload。 */
|
|
2290
|
+
function codexModelMarker(model) {
|
|
2291
|
+
return {
|
|
2292
|
+
type: "chat_output", v: PROTOCOL_V1, streamId: CODEX_MODEL_STREAM_ID,
|
|
2293
|
+
role: "system", text: model, eof: true,
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2082
2296
|
function incrementCount(counts, key) {
|
|
2083
2297
|
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
2084
2298
|
}
|