@cocorograph/hub-agent 0.7.90 → 0.7.92
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/package.json +1 -1
- package/src/codex-stream-bridge.mjs +3 -0
- package/src/main.mjs +59 -17
- package/src/tmux.mjs +6 -2
package/package.json
CHANGED
|
@@ -438,6 +438,9 @@ export class CodexStreamBridge extends EventEmitter {
|
|
|
438
438
|
attachId: attachId || null,
|
|
439
439
|
client,
|
|
440
440
|
threadId: null,
|
|
441
|
+
// 新規 thread/start は、最初の usage.response を受け取るまで context 使用量が 0%。
|
|
442
|
+
// state loop が同じ cwd の古い TUI 値を拾って上書きしないための境界マーカー。
|
|
443
|
+
isFresh: !resumeThreadId,
|
|
441
444
|
// T06541 major#3: cwd reattach の in-flight 引き当てキー。thread/resume 完了まで threadId は
|
|
442
445
|
// null なので、並行 cold attach が「同一 (cwd, resumeThreadId)」を検出できるよう resume 対象の
|
|
443
446
|
// thread id を **client 生成前 (同期)** に持たせる。これで後続 attach は _findLiveSessionByCwd で
|
package/src/main.mjs
CHANGED
|
@@ -1146,6 +1146,11 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
1146
1146
|
// ready-for-input 判定: claude プロセスの子プロセスを内省し「Stop フック/ツール実行が
|
|
1147
1147
|
// 終わって真に入力可能になった」エッジを検出する (proc-introspect.mjs)。capture-pane は
|
|
1148
1148
|
// Stop フック実行中を観測できないため、これが「stop の早期消灯/誤フラッシュ」の根治。
|
|
1149
|
+
// TUI の tmux session_name → Claude session_id。state loop 起動より**前**に作る。
|
|
1150
|
+
// 後で作ると state loop には undefined が渡り、バックグラウンド時だけ cwd 最新 JSONL
|
|
1151
|
+
// へフォールバックして別会話の context% を表示する。
|
|
1152
|
+
ctx.tuiReboundSessions = new Map()
|
|
1153
|
+
|
|
1149
1154
|
const readinessTracker = new ReadinessTracker()
|
|
1150
1155
|
// claude.tui.bind ハンドラが「生成中か (isArmed)」を参照して、生成中 claude を rebind の
|
|
1151
1156
|
// respawn-pane -k で kill する「謎停止」を防ぐ (生成中ガード)。
|
|
@@ -1360,11 +1365,6 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
1360
1365
|
)
|
|
1361
1366
|
}
|
|
1362
1367
|
|
|
1363
|
-
// T04784 会話継続バインディングの冪等性ガード: tmux セッション名 → 最後に --resume で
|
|
1364
|
-
// 載せ替えた Claude session_id。TUI ビューが remount のたびに claude.tui.bind を送っても、
|
|
1365
|
-
// 同じ id への再 respawn (= claude 再起動) を抑止する。
|
|
1366
|
-
ctx.tuiReboundSessions = new Map()
|
|
1367
|
-
|
|
1368
1368
|
// session 回転通知 (T04786 後続) の多重送信ガード: 回転キー (tmux 名 or cwd) →
|
|
1369
1369
|
// 最後に push した new_session_id。閲覧ハートビートは 5s 間隔で来るため、フロントが
|
|
1370
1370
|
// 切替を完了し new id でハートビートを送り直すまでの間に来る余分なハートビートで
|
|
@@ -2295,6 +2295,9 @@ export async function pushImmediateChatState({
|
|
|
2295
2295
|
stalled: false,
|
|
2296
2296
|
turn_active: false,
|
|
2297
2297
|
cli_kind: cliKind,
|
|
2298
|
+
// context_pct がどの会話の値かを明示する。frontend は ID が変わったときだけ
|
|
2299
|
+
// 0% を採用し、同一会話の一時的な 0/null は直近の確定値を保持する。
|
|
2300
|
+
context_session_id: sessionId ?? null,
|
|
2298
2301
|
chat_session_id: sessionId ?? null,
|
|
2299
2302
|
})
|
|
2300
2303
|
}
|
|
@@ -2554,12 +2557,26 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2554
2557
|
!paneIsProcessing
|
|
2555
2558
|
) {
|
|
2556
2559
|
const liveCtx = codexLiveContextForProducer(codexAuthSession.seq)
|
|
2557
|
-
if (typeof liveCtx === "number")
|
|
2560
|
+
if (typeof liveCtx === "number") {
|
|
2561
|
+
contextPct = liveCtx
|
|
2562
|
+
} else if (codexAuthSession.isFresh === true) {
|
|
2563
|
+
// 新規 Codex thread は thread/start の時点で ID だけ確定し、最初の
|
|
2564
|
+
// usage.response までは使用量がない。この間に tmux の旧 pane 値を
|
|
2565
|
+
// 同じ新 thread ID で送ると、即時 push した 0% を打ち消してしまう。
|
|
2566
|
+
contextPct = 0
|
|
2567
|
+
}
|
|
2558
2568
|
}
|
|
2569
|
+
// fresh bind は実 session_id がまだ確定していない。tmux.list の statusLine / cwd 最新
|
|
2570
|
+
// JSONL は直前会話の値を返し得るため、この間は必ず 0% を正本にする。これをしないと
|
|
2571
|
+
// bind の即時 0% snapshot の直後に state loop が旧値を同じ fresh:<id> で再送して
|
|
2572
|
+
// ドーナツを復活させる。
|
|
2573
|
+
const freshUnbound =
|
|
2574
|
+
sessionCliKind === "claude" && isFreshUnboundBind(boundSessions?.get(s.session_name))
|
|
2575
|
+
if (freshUnbound) contextPct = 0
|
|
2559
2576
|
const turnActiveSource = decideTurnActiveSource({
|
|
2560
2577
|
chatStatus: chat?.status,
|
|
2561
2578
|
cwd: s.cwd,
|
|
2562
|
-
isFreshUnbound:
|
|
2579
|
+
isFreshUnbound: freshUnbound,
|
|
2563
2580
|
codexThreadId,
|
|
2564
2581
|
sessionCliKind,
|
|
2565
2582
|
})
|
|
@@ -2610,10 +2627,13 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2610
2627
|
// (cwd, cliKind) キー化により「チャット信号の cliKind == 行の cliKind」が恒真に
|
|
2611
2628
|
// なったので、常に非 null の構造値を送る方が真に厳しい (= 情報量が多い) 上位互換。
|
|
2612
2629
|
//
|
|
2613
|
-
//
|
|
2614
|
-
//
|
|
2630
|
+
// context_session_id は「この context_pct がどの会話の値か」。チャットの session/thread を
|
|
2631
|
+
// 優先し、Claude TUI 専用行では bind 済みの session_id(fresh は fresh:<request_id>)を使う。
|
|
2632
|
+
// frontend はこれを境界として、新規会話の 0% と同一会話の一時的な 0/null を区別する。
|
|
2615
2633
|
const chatCliKind = sessionCliKind
|
|
2616
|
-
const
|
|
2634
|
+
const contextSessionId =
|
|
2635
|
+
chat?.sessionId ??
|
|
2636
|
+
(sessionCliKind === "claude" ? boundSessions?.get(s.session_name) ?? null : codexThreadId)
|
|
2617
2637
|
|
|
2618
2638
|
const prev = lastByName.get(s.session_name)
|
|
2619
2639
|
if (
|
|
@@ -2627,7 +2647,7 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2627
2647
|
prev.stalled !== stalled ||
|
|
2628
2648
|
prev.turn_active !== turnActive ||
|
|
2629
2649
|
prev.cli_kind !== chatCliKind ||
|
|
2630
|
-
prev.
|
|
2650
|
+
prev.context_session_id !== contextSessionId
|
|
2631
2651
|
) {
|
|
2632
2652
|
lastByName.set(s.session_name, {
|
|
2633
2653
|
status,
|
|
@@ -2639,7 +2659,9 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2639
2659
|
stalled,
|
|
2640
2660
|
turn_active: turnActive,
|
|
2641
2661
|
cli_kind: chatCliKind,
|
|
2642
|
-
|
|
2662
|
+
context_session_id: contextSessionId,
|
|
2663
|
+
// 旧クライアント互換。新規 consumer は context_session_id を使う。
|
|
2664
|
+
chat_session_id: contextSessionId,
|
|
2643
2665
|
})
|
|
2644
2666
|
// 計装 (2026-06-19, 既定 OFF=2026-06-20): 状態変化時に値を記録する。常時 info はログ肥大の
|
|
2645
2667
|
// 主因のため HUB_AGENT_STATE_TRACE=1 のときだけ出す。childBusy/outputActive を分けて出し、
|
|
@@ -2658,7 +2680,7 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2658
2680
|
context_pct: contextPct,
|
|
2659
2681
|
turn_active: turnActive,
|
|
2660
2682
|
cli_kind: chatCliKind,
|
|
2661
|
-
|
|
2683
|
+
context_session_id: contextSessionId,
|
|
2662
2684
|
},
|
|
2663
2685
|
"session.state push",
|
|
2664
2686
|
)
|
|
@@ -2680,9 +2702,11 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
2680
2702
|
turn_active: turnActive,
|
|
2681
2703
|
// cli_kind (T05945): セッション名由来の構造値 ('claude' | 'codex')。frontend の
|
|
2682
2704
|
// 単一セレクタ (useCockpitSessionDisplay) が「この push はどの CLI 行のものか」を
|
|
2683
|
-
// 判定する唯一の軸。
|
|
2705
|
+
// 判定する唯一の軸。context_session_id は context% の会話 ID。
|
|
2684
2706
|
cli_kind: chatCliKind,
|
|
2685
|
-
|
|
2707
|
+
context_session_id: contextSessionId,
|
|
2708
|
+
// 旧クライアント互換。新規 consumer は context_session_id を使う。
|
|
2709
|
+
chat_session_id: contextSessionId,
|
|
2686
2710
|
})
|
|
2687
2711
|
}
|
|
2688
2712
|
// 消滅セッションの GC (perf監査/メモリリーク対策): tmux から消えたセッションの状態 Map を
|
|
@@ -4172,7 +4196,11 @@ async function dispatch(msg, ctx) {
|
|
|
4172
4196
|
// 非 fresh (resume/auto) の bind = ユーザーが既存会話を選んだ → 保留中の +新規 defer は破棄。
|
|
4173
4197
|
if (!fresh) ctx.pendingFreshRespawn.delete(sessionName)
|
|
4174
4198
|
let rebind = { ok: false, skipped: false }
|
|
4175
|
-
|
|
4199
|
+
// 新規会話の 0% は会話切替として扱うため、request_id を送れない旧 client
|
|
4200
|
+
// 経由でも bind ごとに固有IDを作る。空の `fresh:` を再利用すると、2回目以降の
|
|
4201
|
+
// 新規会話が同一会話扱いになり、直前値が残る。
|
|
4202
|
+
const freshBindId = request_id || randomUUID()
|
|
4203
|
+
const bindKey = fresh ? `fresh:${freshBindId}` : targetId
|
|
4176
4204
|
const recordDeferredFresh = () =>
|
|
4177
4205
|
ctx.pendingFreshRespawn.set(sessionName, {
|
|
4178
4206
|
cwd,
|
|
@@ -4281,6 +4309,19 @@ async function dispatch(msg, ctx) {
|
|
|
4281
4309
|
noCache: true,
|
|
4282
4310
|
logger,
|
|
4283
4311
|
})
|
|
4312
|
+
// bind が確定している復帰は、capture-pane の見た目や cwd 最新ファイルでなく
|
|
4313
|
+
// **指定 session_id の JSONL** から context% を即時解決する。fresh はまだ実 ID
|
|
4314
|
+
// が無いため 0% と fresh:<request_id> を対にして送る。
|
|
4315
|
+
const contextSessionId = fresh ? bindKey : targetId
|
|
4316
|
+
const boundContext =
|
|
4317
|
+
targetId && cwd
|
|
4318
|
+
? await jsonlContextForCwd(cwd, targetId).catch(() => null)
|
|
4319
|
+
: null
|
|
4320
|
+
const contextPct = fresh
|
|
4321
|
+
? 0
|
|
4322
|
+
: typeof boundContext?.percent === "number"
|
|
4323
|
+
? boundContext.percent
|
|
4324
|
+
: snap.context_pct
|
|
4284
4325
|
// 【問題1 根治 2026-06-22】配る status はプロセス実体の busy を権威に解決する
|
|
4285
4326
|
// (resolveBindSnapshotStatus)。capture(SPINNER_LINE_RE)の偽陽性に勝たせる。
|
|
4286
4327
|
// - busy=true (ターン進行中): capture gap で snap.status が waiting に化けても
|
|
@@ -4296,7 +4337,8 @@ async function dispatch(msg, ctx) {
|
|
|
4296
4337
|
type: "session.state",
|
|
4297
4338
|
session_name: sessionName,
|
|
4298
4339
|
status: snapStatus,
|
|
4299
|
-
context_pct:
|
|
4340
|
+
context_pct: contextPct,
|
|
4341
|
+
context_session_id: contextSessionId ?? null,
|
|
4300
4342
|
permission_mode: snap.permission_mode,
|
|
4301
4343
|
stable:
|
|
4302
4344
|
!busy && snapStatus !== "processing" && snap.stable === true,
|
package/src/tmux.mjs
CHANGED
|
@@ -29,7 +29,7 @@ import {
|
|
|
29
29
|
getSessionCwd,
|
|
30
30
|
} from "./state.mjs"
|
|
31
31
|
import { baseSessionName, sessionNameFor } from "./session-names.mjs"
|
|
32
|
-
import { boundSessionId, getSessionUsages, jsonlContextForCwd } from "./usage.mjs"
|
|
32
|
+
import { boundSessionId, getSessionUsages, isFreshUnboundBind, jsonlContextForCwd } from "./usage.mjs"
|
|
33
33
|
|
|
34
34
|
const execFileP = promisify(execFile)
|
|
35
35
|
|
|
@@ -681,13 +681,17 @@ export async function listSessions(opts = {}) {
|
|
|
681
681
|
// tmux 上の bind 台帳が無いため、これが無いと cold-start (session.state snapshot 未取得)
|
|
682
682
|
// の poll 応答で cwd の別アクティビティ jsonl を「最新」と取り違える cross-activity 汚染が
|
|
683
683
|
// 起き得る (2026-07 発覚。TUI 版は本行の boundSessions 解決で既に根治済みだった)。
|
|
684
|
+
const boundValue = opts.boundSessions?.get(s.name)
|
|
685
|
+
const freshUnbound = isFreshUnboundBind(boundValue)
|
|
684
686
|
const boundId =
|
|
685
687
|
boundSessionId(opts.boundSessions, s.name) ||
|
|
686
688
|
(cwd && opts.claudeBridge?.liveSessionIdForCwd?.(cwd)) ||
|
|
687
689
|
null
|
|
688
690
|
const jsonlInfo = cwd ? await jsonlContextForCwd(cwd, boundId) : null
|
|
689
691
|
const context_pct =
|
|
690
|
-
|
|
692
|
+
freshUnbound
|
|
693
|
+
? 0
|
|
694
|
+
: jsonlInfo?.isReset && typeof jsonlInfo.percent === "number"
|
|
691
695
|
? jsonlInfo.percent
|
|
692
696
|
: typeof fromStatusLine === "number"
|
|
693
697
|
? fromStatusLine
|