@cocorograph/hub-agent 0.7.91 → 0.7.93

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cocorograph/hub-agent",
3
- "version": "0.7.91",
3
+ "version": "0.7.93",
4
4
  "description": "Hub Hosted Cockpit のローカル常駐 agent。Hub と outbound WSS で接続し、ローカルの tmux/pty を中継する。",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -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
@@ -2557,12 +2557,26 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
2557
2557
  !paneIsProcessing
2558
2558
  ) {
2559
2559
  const liveCtx = codexLiveContextForProducer(codexAuthSession.seq)
2560
- if (typeof liveCtx === "number") contextPct = liveCtx
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
+ }
2561
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
2562
2576
  const turnActiveSource = decideTurnActiveSource({
2563
2577
  chatStatus: chat?.status,
2564
2578
  cwd: s.cwd,
2565
- isFreshUnbound: isFreshUnboundBind(boundSessions?.get(s.session_name)),
2579
+ isFreshUnbound: freshUnbound,
2566
2580
  codexThreadId,
2567
2581
  sessionCliKind,
2568
2582
  })
@@ -3513,19 +3527,21 @@ async function dispatch(msg, ctx) {
3513
3527
  // (main.mjs 内 clearChatSignal 呼び出し箇所参照) と対称に、SDK チャットの
3514
3528
  // attach 時点でも前セッションのチャット信号を引き継がない。
3515
3529
  // - fresh (resume 無し): 新規セッションで session_id はまだ確定しない
3516
- // (SDK が最初の assistant イベントで通知する) ため null。前の信号を破棄し
3517
- // ゼロ値を即時 push してドーナツ/ドットを即座にリセットする (症状④)。
3530
+ // (SDK が最初の assistant イベントで通知する)。ただし null を送ると frontend
3531
+ // は「同一会話で一時的に0%になった」と判断して旧値を保持するため、stream_id
3532
+ // 由来の一意な仮IDを先に送る。実 session_id が来た時点で置換される。
3518
3533
  // - resume: bound session の jsonl から実 context% を解決し、cli_kind='claude' +
3519
3534
  // 実 session_id で identity を確定させて即時 push する (症状③)。
3520
3535
  const resolvedCwd = cwdArg || process.env.HOME || process.cwd()
3521
3536
  if (resolvedCwd) {
3522
3537
  if (!resumeSessionId) {
3538
+ const freshContextSessionId = `fresh:${stream_id}`
3523
3539
  clearChatSignal(resolvedCwd, "claude")
3524
3540
  recordChatActivity(resolvedCwd, "claude", {
3525
3541
  status: "waiting",
3526
3542
  contextPct: 0,
3527
3543
  inputPending: false,
3528
- sessionId: null,
3544
+ sessionId: freshContextSessionId,
3529
3545
  })
3530
3546
  pushImmediateChatState({
3531
3547
  client: ctx.client,
@@ -3533,7 +3549,7 @@ async function dispatch(msg, ctx) {
3533
3549
  logger: ctx.logger,
3534
3550
  cwd: resolvedCwd,
3535
3551
  cliKind: "claude",
3536
- sessionId: null,
3552
+ sessionId: freshContextSessionId,
3537
3553
  contextPct: 0,
3538
3554
  }).catch(() => {})
3539
3555
  } else {
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
- jsonlInfo?.isReset && typeof jsonlInfo.percent === "number"
692
+ freshUnbound
693
+ ? 0
694
+ : jsonlInfo?.isReset && typeof jsonlInfo.percent === "number"
691
695
  ? jsonlInfo.percent
692
696
  : typeof fromStatusLine === "number"
693
697
  ? fromStatusLine