@cocorograph/hub-agent 0.7.19 → 0.7.21
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/extract-paragraph.mjs +25 -0
- package/src/jsonl-live-watchers.mjs +63 -4
- package/src/main.mjs +124 -34
- package/src/tui-permission-bridge.mjs +36 -0
- package/src/usage.mjs +22 -0
package/package.json
CHANGED
|
@@ -86,6 +86,31 @@ export function extractLastAssistantText(paneText) {
|
|
|
86
86
|
return text
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
/**
|
|
90
|
+
* permission/質問カードに出す「直前アシスタント説明」の採用優先順位を決める純関数。
|
|
91
|
+
*
|
|
92
|
+
* 症状4 根治: フック (bundle) 由来の context_text は PreToolUse 発火時点で jsonl が
|
|
93
|
+
* 前ターン止まり (フックがブロック中は claude が現ターンを commit しない=デッドロック)
|
|
94
|
+
* のため「前ターンの古い説明」を掴む取り違えを起こす。一方 hub-agent が tmux
|
|
95
|
+
* capture-pane で抜く liveContext は「いま描画中の現ターン本文」なので回答前に
|
|
96
|
+
* 信頼できる唯一のソース。よって **liveContext を最優先**し、フック由来は後方互換の
|
|
97
|
+
* フォールバックとしてのみ採用する (旧 bundle が context_text を送ってきても汚染しない)。
|
|
98
|
+
* 両方とも空/非文字列なら null (= 説明なしで安全縮退)。
|
|
99
|
+
*
|
|
100
|
+
* @param {unknown} hookContextText フック (bundle) が同梱した context_text (旧経路)
|
|
101
|
+
* @param {unknown} liveContext capture-pane から抽出した現ターン説明
|
|
102
|
+
* @returns {string|null}
|
|
103
|
+
*/
|
|
104
|
+
export function pickContextText(hookContextText, liveContext) {
|
|
105
|
+
const live =
|
|
106
|
+
typeof liveContext === "string" && liveContext.trim() ? liveContext : null
|
|
107
|
+
const hook =
|
|
108
|
+
typeof hookContextText === "string" && hookContextText.trim()
|
|
109
|
+
? hookContextText
|
|
110
|
+
: null
|
|
111
|
+
return live || hook || null
|
|
112
|
+
}
|
|
113
|
+
|
|
89
114
|
export const __test = {
|
|
90
115
|
_MAX_CHARS,
|
|
91
116
|
_MIN_CHARS,
|
|
@@ -48,12 +48,22 @@ export class JsonlLiveWatchers {
|
|
|
48
48
|
* @param {() => Promise<string|undefined>|string|undefined} args.getProjectsRoot
|
|
49
49
|
* - ~/.claude/projects の実効ルート解決 (アカウント切替を反映)
|
|
50
50
|
* @param {number} [args.ttlMs]
|
|
51
|
+
* @param {(info: {session_id: string, cwd: string, toolName: string, tool_use_id: string}) => void} [args.onToolResolved]
|
|
52
|
+
* - jsonl に tool_result が着弾し「あるツールが解決された」と検知したときのコールバック
|
|
53
|
+
* (症状1b 根治: ターミナルのネイティブメニューで回答 / 承認された pending カードを畳む)。
|
|
51
54
|
* @param {import('pino').Logger} [args.logger]
|
|
52
55
|
*/
|
|
53
|
-
constructor({
|
|
56
|
+
constructor({
|
|
57
|
+
send,
|
|
58
|
+
getProjectsRoot,
|
|
59
|
+
ttlMs = WATCHER_TTL_MS,
|
|
60
|
+
onToolResolved,
|
|
61
|
+
logger,
|
|
62
|
+
} = {}) {
|
|
54
63
|
this.send = send
|
|
55
64
|
this.getProjectsRoot = getProjectsRoot
|
|
56
65
|
this.ttlMs = ttlMs
|
|
66
|
+
this.onToolResolved = onToolResolved
|
|
57
67
|
this.logger = logger
|
|
58
68
|
/** @type {Map<string, {watcher: {stop: () => void}, cwd: string, expiresAt: number}>} */
|
|
59
69
|
this._entries = new Map()
|
|
@@ -96,14 +106,33 @@ export class JsonlLiveWatchers {
|
|
|
96
106
|
// フックを通さず jsonl を直接 tail するため、ここで独自にマスクしないと素通しになる。
|
|
97
107
|
// tail は fromEnd=true で監視開始後の追記のみを拾うので、開始前に書かれた tool_use の
|
|
98
108
|
// id→name を既存 jsonl から事前シードし、スキル tool_result の判定取りこぼしを防ぐ。
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
109
|
+
// tool_use(id→name) の事前シード。マスカーとネイティブ回答検知が共用する
|
|
110
|
+
// (tail は fromEnd=true で開始後の追記のみ拾うため、開始前の tool_use 名を先に流し込む)。
|
|
111
|
+
const detect = typeof this.onToolResolved === "function"
|
|
112
|
+
const seed =
|
|
113
|
+
MASK_ENABLED || detect ? await this._seedToolNames(filePath) : new Map()
|
|
114
|
+
const masker = MASK_ENABLED ? createEventMasker(seed) : null
|
|
115
|
+
// 検知用は別 Map (マスカー内部 Map と相互に汚染しないようコピー)。
|
|
116
|
+
const detectNames = detect ? new Map(seed) : null
|
|
102
117
|
const watcher = watchSessionFile({
|
|
103
118
|
filePath,
|
|
104
119
|
fromEnd: true,
|
|
105
120
|
logger: this.logger,
|
|
106
121
|
onEvent: (event) => {
|
|
122
|
+
// ネイティブ回答検知 (症状1b 根治): browser を経由せずターミナルのネイティブメニューで
|
|
123
|
+
// 回答 / 承認されたケースを jsonl の tool_result 着弾で検知する。.decision は browser
|
|
124
|
+
// 回答でしか書かれずネイティブ回答には効かないため、jsonl tool_result が唯一の権威
|
|
125
|
+
// ソース (resume が読む実体と同じ)。マスク前の素 event を見る (構造のみ参照)。
|
|
126
|
+
if (detectNames) {
|
|
127
|
+
try {
|
|
128
|
+
this._detectResolved(event, detectNames, session_id, cwd)
|
|
129
|
+
} catch (err) {
|
|
130
|
+
this.logger?.debug?.(
|
|
131
|
+
{ err: err?.message },
|
|
132
|
+
"jsonl tool_result detect failed",
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
107
136
|
try {
|
|
108
137
|
this.send?.({
|
|
109
138
|
type: "claude.jsonl.event",
|
|
@@ -175,6 +204,36 @@ export class JsonlLiveWatchers {
|
|
|
175
204
|
return seed
|
|
176
205
|
}
|
|
177
206
|
|
|
207
|
+
/**
|
|
208
|
+
* 1 イベントを走査し、(1) assistant の tool_use(id→name) を蓄積、(2) user の tool_result
|
|
209
|
+
* 着弾を検知して onToolResolved を発火する。tool_use と tool_result は別イベントで届くので、
|
|
210
|
+
* id→name を貯めながら tool_use_id 一致で「そのツールが解決された」と判定する
|
|
211
|
+
* (collectToolUseNames はマスカーと共用の純関数)。
|
|
212
|
+
*
|
|
213
|
+
* @param {object} event normalizeHistoryEvent 済みイベント (type + message を持つ)
|
|
214
|
+
* @param {Map<string,string>} toolNames tool_use_id→tool_name の蓄積 Map
|
|
215
|
+
* @param {string} session_id
|
|
216
|
+
* @param {string} cwd
|
|
217
|
+
*/
|
|
218
|
+
_detectResolved(event, toolNames, session_id, cwd) {
|
|
219
|
+
// 新規 tool_use(id→name) を吸収する (assistant イベントのみ反応)。
|
|
220
|
+
collectToolUseNames(event, toolNames)
|
|
221
|
+
if (event?.type !== "user" || !Array.isArray(event.message?.content)) return
|
|
222
|
+
for (const b of event.message.content) {
|
|
223
|
+
if (b && b.type === "tool_result" && typeof b.tool_use_id === "string") {
|
|
224
|
+
const toolName = toolNames.get(b.tool_use_id)
|
|
225
|
+
if (toolName) {
|
|
226
|
+
this.onToolResolved?.({
|
|
227
|
+
session_id,
|
|
228
|
+
cwd,
|
|
229
|
+
toolName,
|
|
230
|
+
tool_use_id: b.tool_use_id,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
178
237
|
async _resolveProjectsRoot() {
|
|
179
238
|
try {
|
|
180
239
|
const r = this.getProjectsRoot?.()
|
package/src/main.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import path from "node:path"
|
|
|
20
20
|
import pino from "pino"
|
|
21
21
|
|
|
22
22
|
import { detectPlatform, readConfig, writeConfig } from "./config.mjs"
|
|
23
|
-
import { extractLastAssistantText } from "./extract-paragraph.mjs"
|
|
23
|
+
import { extractLastAssistantText, pickContextText } from "./extract-paragraph.mjs"
|
|
24
24
|
import { loadPlugins, runHookBroadcast, runHookChain } from "./plugin-loader.mjs"
|
|
25
25
|
import { WsClient } from "./ws-client.mjs"
|
|
26
26
|
import { PtyBridge } from "./pty-bridge.mjs"
|
|
@@ -92,6 +92,7 @@ import {
|
|
|
92
92
|
contextWindowSize,
|
|
93
93
|
getSessionUsages,
|
|
94
94
|
getUsage,
|
|
95
|
+
isFreshUnboundBind,
|
|
95
96
|
recordChatRateLimit,
|
|
96
97
|
turnActiveForCwd,
|
|
97
98
|
} from "./usage.mjs"
|
|
@@ -706,17 +707,18 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
706
707
|
/* ignore */
|
|
707
708
|
}
|
|
708
709
|
}
|
|
709
|
-
// ── 直前アシスタント説明の抽出 (
|
|
710
|
+
// ── 直前アシスタント説明の抽出 (症状4 根治) ─────────────────────────────
|
|
710
711
|
// PreToolUse フック発火時点で jsonl はまだ前ターン止まり (フックがブロック
|
|
711
|
-
// している間 claude
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
//
|
|
715
|
-
//
|
|
716
|
-
//
|
|
717
|
-
//
|
|
712
|
+
// している間 claude は現ターンを jsonl に commit しない=デッドロック構造) の
|
|
713
|
+
// ため、フック側の transcript 抽出は前ターンの古い説明を掴む「取り違え」を
|
|
714
|
+
// 起こす。代わりに hub-agent がここで tmux capture-pane を打ち、TUI のペイン上
|
|
715
|
+
// に既に描画されている「現ターンの assistant 本文段落 (⏺ プレフィックス)」を
|
|
716
|
+
// 抜き出す。これが回答前に説明を届ける唯一の信頼ソースなので、フック由来の
|
|
717
|
+
// context_text の有無に関わらず **常に** capture-pane を試み、pickContextText で
|
|
718
|
+
// liveContext を最優先する (旧 bundle が古い context_text を送ってきても汚染
|
|
719
|
+
// しない)。抽出失敗時は null フォールバック (= 説明なし) で安全縮退。
|
|
718
720
|
let liveContext = null
|
|
719
|
-
if (
|
|
721
|
+
if (cwd) {
|
|
720
722
|
try {
|
|
721
723
|
const sessions = await listTmuxSessions({ logger })
|
|
722
724
|
const match = Array.isArray(sessions)
|
|
@@ -742,9 +744,9 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
742
744
|
tool_name,
|
|
743
745
|
input,
|
|
744
746
|
// 質問/承認カードの直前アシスタント説明 (browser がカード上部に表示)。
|
|
745
|
-
// 優先順位: (1)
|
|
746
|
-
//
|
|
747
|
-
context_text: context_text
|
|
747
|
+
// 優先順位: (1) capture-pane で抽出した現ターンの liveContext、(2) フック由来
|
|
748
|
+
// の context_text (後方互換フォールバック)、(3) null。pickContextText 参照。
|
|
749
|
+
context_text: pickContextText(context_text, liveContext),
|
|
748
750
|
})
|
|
749
751
|
},
|
|
750
752
|
)
|
|
@@ -766,6 +768,33 @@ export async function startDaemon({ version, ptyModule, claudeSdk } = {}) {
|
|
|
766
768
|
send: (obj) => client.send(obj),
|
|
767
769
|
getProjectsRoot: getActiveProjectsRoot,
|
|
768
770
|
logger,
|
|
771
|
+
// ネイティブ回答検知 (症状1b 根治): tail 中の jsonl に tool_result が着弾し「あるツールが
|
|
772
|
+
// ターミナル側で解決された」と分かったら、その session の同名ツールの pending 権限カードを
|
|
773
|
+
// 畳み、browser へ claude.permission.cancel を送って残存カードを消す。browser が先に
|
|
774
|
+
// resolve() 済みなら _pending は既に空で dropBySession は [] を返す (= no-op、cancel も出ない)。
|
|
775
|
+
onToolResolved: ({ session_id, cwd, toolName }) => {
|
|
776
|
+
const dropped =
|
|
777
|
+
tuiPermissionBridge.dropBySession({ session_id, cwd, toolName }) || []
|
|
778
|
+
for (const request_id of dropped) {
|
|
779
|
+
try {
|
|
780
|
+
client.send({
|
|
781
|
+
type: "claude.permission.cancel",
|
|
782
|
+
request_id,
|
|
783
|
+
session_id,
|
|
784
|
+
cwd,
|
|
785
|
+
reason: "answered_in_terminal",
|
|
786
|
+
})
|
|
787
|
+
} catch {
|
|
788
|
+
/* ignore */
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (dropped.length) {
|
|
792
|
+
logger.info(
|
|
793
|
+
{ session_id, cwd, tool: toolName, count: dropped.length },
|
|
794
|
+
"tui permission resolved natively → cancelled browser card",
|
|
795
|
+
)
|
|
796
|
+
}
|
|
797
|
+
},
|
|
769
798
|
})
|
|
770
799
|
jsonlLiveWatchers.start()
|
|
771
800
|
ctx.jsonlLiveWatchers = jsonlLiveWatchers
|
|
@@ -1366,6 +1395,11 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
1366
1395
|
turnActive = true
|
|
1367
1396
|
} else if (chat?.status === "waiting" || chat?.status === "idle") {
|
|
1368
1397
|
turnActive = false
|
|
1398
|
+
} else if (s.cwd && isFreshUnboundBind(boundSessions?.get(s.session_name))) {
|
|
1399
|
+
// 症状2b 根治: fresh bind プレースホルダの間は、別アクティビティ jsonl を mtime 最新で
|
|
1400
|
+
// 取り違える汚染を避けて turn_active を出さない (null=不明)。実セッション id へ adopt
|
|
1401
|
+
// されれば下の通常経路に乗る。青を出さない安全側にのみ倒すため固着青は生まない。
|
|
1402
|
+
turnActive = null
|
|
1369
1403
|
} else if (s.cwd) {
|
|
1370
1404
|
turnActive = await turnActiveForCwd(
|
|
1371
1405
|
s.cwd,
|
|
@@ -1442,6 +1476,15 @@ export function startStateLoop({ client, plugins, logger, intervalMs, claudeBrid
|
|
|
1442
1476
|
const liveNames = new Set(states.map((s) => s.session_name))
|
|
1443
1477
|
for (const name of [...lastByName.keys()]) {
|
|
1444
1478
|
if (liveNames.has(name)) continue
|
|
1479
|
+
// 終端を能動 broadcast (症状3 根治): tmux から消えた = crash/外部 kill/別経路
|
|
1480
|
+
// 終了。explicit kill は kill_session ハンドラが session.gone を出すが、それ以外
|
|
1481
|
+
// の消滅はここが唯一の検知点。frontend が states/lastEvent を忘れてドットを
|
|
1482
|
+
// 'down' に倒せるよう、ローカル Map を forget する前に通知する。
|
|
1483
|
+
try {
|
|
1484
|
+
client.send({ type: "session.gone", session_name: name })
|
|
1485
|
+
} catch {
|
|
1486
|
+
/* ignore */
|
|
1487
|
+
}
|
|
1445
1488
|
invalidateSessionCache(name) // capture/cwd/statusGate/spinnerFreeze/stability (state.mjs)
|
|
1446
1489
|
stallTracker.forget(name) // StallTracker.byName
|
|
1447
1490
|
readinessTracker?.forget?.(name) // ReadinessTracker.byName
|
|
@@ -1752,6 +1795,51 @@ export async function handleQueueFlush(msg, ctx) {
|
|
|
1752
1795
|
})
|
|
1753
1796
|
}
|
|
1754
1797
|
|
|
1798
|
+
/**
|
|
1799
|
+
* tmux.kill_session の処理 (症状3 根治)。要求された session を kill し、要求元へ
|
|
1800
|
+
* tmux.kill_session.result を返したうえで、kill できた各 session について
|
|
1801
|
+
* **session.gone を能動 broadcast** する。
|
|
1802
|
+
*
|
|
1803
|
+
* 不変条件: session の終端 (kill/crash/外部終了) は、同一ホストで実体を観測できる
|
|
1804
|
+
* agent が正本として能動 push する。これが無いと frontend は kill 時点の turn_active=true
|
|
1805
|
+
* を keep-last したままステータスドットが青で固着する。最後の 1 セッションを kill した
|
|
1806
|
+
* 場合 state loop の GC は states.length>0 ガードで発火しないため、explicit kill 経路の
|
|
1807
|
+
* ここから session.gone を出すのが確実 (GC は crash/外部 kill の補完)。
|
|
1808
|
+
*
|
|
1809
|
+
* テストから直接呼べるよう export。kill 実体 (killManySessions) は ctx で差し替え可能。
|
|
1810
|
+
*/
|
|
1811
|
+
export async function handleKillSession(msg, ctx) {
|
|
1812
|
+
const killFn = ctx.killManySessions || killManySessions
|
|
1813
|
+
const names = Array.isArray(msg.session_names)
|
|
1814
|
+
? msg.session_names
|
|
1815
|
+
: msg.session_name
|
|
1816
|
+
? [msg.session_name]
|
|
1817
|
+
: []
|
|
1818
|
+
if (names.length === 0) {
|
|
1819
|
+
ctx.client.send({
|
|
1820
|
+
type: "tmux.kill_session.result",
|
|
1821
|
+
request_id: msg.request_id,
|
|
1822
|
+
killed: [],
|
|
1823
|
+
failed: [{ name: "", reason: "session_name(s) required" }],
|
|
1824
|
+
})
|
|
1825
|
+
return
|
|
1826
|
+
}
|
|
1827
|
+
const r = await killFn(names)
|
|
1828
|
+
ctx.client.send({
|
|
1829
|
+
type: "tmux.kill_session.result",
|
|
1830
|
+
request_id: msg.request_id,
|
|
1831
|
+
killed: r.killed,
|
|
1832
|
+
failed: r.failed,
|
|
1833
|
+
})
|
|
1834
|
+
for (const name of r.killed) {
|
|
1835
|
+
try {
|
|
1836
|
+
ctx.client.send({ type: "session.gone", session_name: name })
|
|
1837
|
+
} catch {
|
|
1838
|
+
/* ignore */
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1755
1843
|
/**
|
|
1756
1844
|
* claude.tui.interrupt の処理 (確認付き中断 = Phase3)。生 ESC を tracked pty.data で best-effort
|
|
1757
1845
|
* 送出する旧経路 (stream 欠落で無言ドロップ + ESC 到達でも止まったか未検証) を、agent が ESC を
|
|
@@ -2177,6 +2265,28 @@ async function dispatch(msg, ctx) {
|
|
|
2177
2265
|
ctx.readinessTracker?.forget(viewName)
|
|
2178
2266
|
invalidateSessionCache(viewName)
|
|
2179
2267
|
}
|
|
2268
|
+
// 回転 = 旧 session_id 境界。旧 session に紐づく pending 権限カードは orphan に
|
|
2269
|
+
// なる (claude は新会話へ移った) ので畳み、browser へ cancel して残存カードを消す
|
|
2270
|
+
// (症状1b 根治)。旧 session_id 完全一致で drop するため、回転直後に新 session で
|
|
2271
|
+
// 出たばかりのカード (payload.session_id === newSessionId) は誤って巻き込まない。
|
|
2272
|
+
if (ctx.tuiPermissionBridge) {
|
|
2273
|
+
const dropped = ctx.tuiPermissionBridge.dropBySession({
|
|
2274
|
+
session_id: viewSid,
|
|
2275
|
+
})
|
|
2276
|
+
for (const request_id of dropped) {
|
|
2277
|
+
try {
|
|
2278
|
+
ctx.client.send({
|
|
2279
|
+
type: "claude.permission.cancel",
|
|
2280
|
+
request_id,
|
|
2281
|
+
session_id: viewSid,
|
|
2282
|
+
cwd: viewCwd,
|
|
2283
|
+
reason: "session_rotated",
|
|
2284
|
+
})
|
|
2285
|
+
} catch {
|
|
2286
|
+
/* ignore */
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2180
2290
|
logger.info(
|
|
2181
2291
|
{ session: viewName, cwd: viewCwd, old: viewSid, new: newSessionId },
|
|
2182
2292
|
"tui session rotated (/clear) → notified browser",
|
|
@@ -3191,27 +3301,7 @@ async function dispatch(msg, ctx) {
|
|
|
3191
3301
|
return
|
|
3192
3302
|
}
|
|
3193
3303
|
case "tmux.kill_session": {
|
|
3194
|
-
|
|
3195
|
-
? msg.session_names
|
|
3196
|
-
: msg.session_name
|
|
3197
|
-
? [msg.session_name]
|
|
3198
|
-
: []
|
|
3199
|
-
if (names.length === 0) {
|
|
3200
|
-
ctx.client.send({
|
|
3201
|
-
type: "tmux.kill_session.result",
|
|
3202
|
-
request_id: msg.request_id,
|
|
3203
|
-
killed: [],
|
|
3204
|
-
failed: [{ name: "", reason: "session_name(s) required" }],
|
|
3205
|
-
})
|
|
3206
|
-
return
|
|
3207
|
-
}
|
|
3208
|
-
const r = await killManySessions(names)
|
|
3209
|
-
ctx.client.send({
|
|
3210
|
-
type: "tmux.kill_session.result",
|
|
3211
|
-
request_id: msg.request_id,
|
|
3212
|
-
killed: r.killed,
|
|
3213
|
-
failed: r.failed,
|
|
3214
|
-
})
|
|
3304
|
+
await handleKillSession(msg, ctx)
|
|
3215
3305
|
return
|
|
3216
3306
|
}
|
|
3217
3307
|
case "skills.request": {
|
|
@@ -147,6 +147,42 @@ export class TuiPermissionBridge extends EventEmitter {
|
|
|
147
147
|
this._seen.delete(request_id)
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* セッションに紐づく pending を **まとめて** 落とす (set/clear 非対称の解消)。
|
|
152
|
+
*
|
|
153
|
+
* `_pending` を畳む経路は元々 `resolve()`(browser 回答) と `drop(request_id)` の 2 本しか
|
|
154
|
+
* 無く、(a) ターミナルでネイティブ回答された / (b) `/clear` で session が回転した /
|
|
155
|
+
* (c) kill された、という「browser を経由しない終端」では pending が 6h TTL まで残り、
|
|
156
|
+
* 復帰時 `listPending`→rehydrate が古いカードを反復再 push していた (症状1b)。本メソッドは
|
|
157
|
+
* その終端を session 同一性で一括 drop するためのもの。
|
|
158
|
+
*
|
|
159
|
+
* マッチ規則は `listPending` (L134-142) と同じ OR セマンティクス: payload.session_id ===
|
|
160
|
+
* session_id **または** payload.cwd === cwd。さらに `toolName` を渡すと payload.tool_name 一致
|
|
161
|
+
* のものだけに絞る (ネイティブ回答検知で「いま解決したツールのカードだけ」を畳む用途)。
|
|
162
|
+
* 落とした request_id を返すので、呼び出し側は browser へ `claude.permission.cancel` を
|
|
163
|
+
* 送ってカードを消せる。
|
|
164
|
+
*
|
|
165
|
+
* `drop(request_id)` との違い: drop は request_id 既知の 1 件 (resume 経路)、dropBySession は
|
|
166
|
+
* session 同一性で選んだ **集合** を落とし、落とした id 一覧を返す。
|
|
167
|
+
*
|
|
168
|
+
* @param {{session_id?: string|null, cwd?: string|null, toolName?: string|null}} q
|
|
169
|
+
* @returns {string[]} 落とした request_id の配列
|
|
170
|
+
*/
|
|
171
|
+
dropBySession({ session_id = null, cwd = null, toolName = null } = {}) {
|
|
172
|
+
const dropped = []
|
|
173
|
+
if (!session_id && !cwd) return dropped
|
|
174
|
+
for (const [request_id, { payload }] of this._pending) {
|
|
175
|
+
const sidOk = session_id && payload.session_id === session_id
|
|
176
|
+
const cwdOk = cwd && payload.cwd === cwd
|
|
177
|
+
if (!(sidOk || cwdOk)) continue
|
|
178
|
+
if (toolName && payload.tool_name !== toolName) continue
|
|
179
|
+
this._pending.delete(request_id)
|
|
180
|
+
this._seen.delete(request_id)
|
|
181
|
+
dropped.push(request_id)
|
|
182
|
+
}
|
|
183
|
+
return dropped
|
|
184
|
+
}
|
|
185
|
+
|
|
150
186
|
/**
|
|
151
187
|
* browser の決定をフックへ返す。対象 request_id なら `.decision` を atomic
|
|
152
188
|
* 書き込みし、要求ファイルを掃除して true を返す。対象外なら false
|
package/src/usage.mjs
CHANGED
|
@@ -603,6 +603,28 @@ export function boundSessionId(boundSessions, name) {
|
|
|
603
603
|
return typeof v === "string" && v && !v.startsWith("fresh:") ? v : null
|
|
604
604
|
}
|
|
605
605
|
|
|
606
|
+
/**
|
|
607
|
+
* その tmux セッションが「fresh bind プレースホルダ (`fresh:<req>`)」の状態か判定する
|
|
608
|
+
* (症状2b 根治)。
|
|
609
|
+
*
|
|
610
|
+
* fresh bind は「新規セッションを開始する」意思表示で、まだ実セッション id へ adopt されて
|
|
611
|
+
* おらず、このペインが走らせている jsonl が確定していない。この状態で turn_active を
|
|
612
|
+
* `resolveTargetJsonl` の mtime 最新フォールバックから導くと、同一 cwd-encode dir に堆積した
|
|
613
|
+
* 別アクティビティ (headless `claude -p` / subagent / 並走セッション / 旧 jsonl) を「最新」と
|
|
614
|
+
* 取り違えて **偽の生成中 (青ドット) を出し、新規セッションで無操作なのに三点リーダーが固着** する。
|
|
615
|
+
*
|
|
616
|
+
* よって呼び出し側 (state loop) は fresh プレースホルダの間 turn_active を null (不明) に倒す。
|
|
617
|
+
* これは安全側 (青を出さない方向) にしか倒さないため、偽陽性の青固着を生むことはない。生成中の
|
|
618
|
+
* in-chat スピナーは frontend が turnOverride / session.event(prompt_submit) で別途駆動するため、
|
|
619
|
+
* fresh セッションの実生成中表示は保たれる (本判定はサイドバー/ヘッダーのドット権威のみに効く)。
|
|
620
|
+
*
|
|
621
|
+
* @param {string|null|undefined} boundVal `boundSessions.get(session_name)` の生値
|
|
622
|
+
* @returns {boolean}
|
|
623
|
+
*/
|
|
624
|
+
export function isFreshUnboundBind(boundVal) {
|
|
625
|
+
return typeof boundVal === "string" && boundVal.startsWith("fresh:")
|
|
626
|
+
}
|
|
627
|
+
|
|
606
628
|
/**
|
|
607
629
|
* 指定 cwd の Claude セッション jsonl から context% (USED %) を算出する。
|
|
608
630
|
*
|